├── .document ├── .github ├── release.yml ├── dependabot.yml └── workflows │ ├── sync-ruby.yml │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── bin ├── setup └── console ├── Gemfile ├── .git-blame-ignore-revs ├── ext └── win32 │ └── resolv │ ├── extconf.rb │ ├── lib │ └── resolv.rb │ └── resolv.c ├── test ├── lib │ └── helper.rb └── resolv │ ├── test_mdns.rb │ ├── test_win32_config.rb │ ├── test_addr.rb │ ├── test_resource.rb │ ├── test_svcb_https.rb │ └── test_dns.rb ├── Rakefile ├── resolv.gemspec ├── BSDL ├── README.md ├── COPYING └── lib └── resolv.rb /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | lib/ 5 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - dependencies # Added by Dependabot 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /.bundle/ 3 | /.yardoc 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | Gemfile.lock 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | gem "test-unit-ruby-core" 8 | gem "ruby-core-tasks", github: "ruby/ruby-core-tasks" 9 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This is a file used by GitHub to ignore the following commits on `git blame`. 2 | # 3 | # You can also do the same thing in your local repository with: 4 | # $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs 5 | 6 | # Expand tabs 7 | 9ae210665a8524554e7c868d3ec67af54b0958bb 8 | -------------------------------------------------------------------------------- /ext/win32/resolv/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | if RUBY_ENGINE == "ruby" and have_library('iphlpapi', 'GetNetworkParams', ['windows.h', 'iphlpapi.h']) 3 | have_library('advapi32', 'RegGetValueW', ['windows.h']) 4 | create_makefile('win32/resolv') 5 | else 6 | File.write('Makefile', "all clean install:\n\t@echo Done: $(@)\n") 7 | end 8 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | if RUBY_PLATFORM =~ /mswin|mingw/ 5 | # "win32/resolv" is installation path by Ruby installer. 6 | # We should load that file manually for testing with Windows platform. 7 | require_relative "../../ext/win32/resolv/lib/resolv" 8 | end 9 | 10 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | if RUBY_ENGINE == "ruby" 5 | require "ruby-core/extensiontask" 6 | helper = Bundler::GemHelper.instance 7 | extask = RubyCore::ExtensionTask.new(helper.gemspec) 8 | task :test => :compile 9 | end 10 | 11 | Rake::TestTask.new(:test) do |t| 12 | t.libs.unshift(*extask.libs) if extask 13 | t.libs << "test/lib" 14 | t.ruby_opts << "-rhelper" 15 | t.test_files = FileList["test/**/test_*.rb"] 16 | end 17 | 18 | task :default => :test 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/resolv/test_win32_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test/unit' 4 | require 'resolv' 5 | 6 | if defined?(Win32::Resolve) 7 | class TestWin32Config < Test::Unit::TestCase 8 | def test_get_item_property_string 9 | # Test reading a string registry value 10 | result = Win32::Resolv.send(:get_hosts_dir) 11 | 12 | # Should return a string (empty or with a path) 13 | assert_instance_of String, result 14 | end 15 | 16 | # Test reading a non-existent registry key 17 | def test_nonexistent_key 18 | assert_nil(Win32::Resolv.send(:tcpip_params) {|reg| reg.open('NonExistentKeyThatShouldNotExist')}) 19 | end 20 | 21 | # Test reading a non-existent registry value 22 | def test_nonexistent_value 23 | assert_nil(Win32::Resolv.send(:tcpip_params) {|reg| reg.value('NonExistentKeyThatShouldNotExist')}) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /resolv.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = name 10 | spec.version = version 11 | spec.authors = ["Tanaka Akira"] 12 | spec.email = ["akr@fsij.org"] 13 | 14 | spec.summary = %q{Thread-aware DNS resolver library in Ruby.} 15 | spec.description = %q{Thread-aware DNS resolver library in Ruby.} 16 | spec.homepage = "https://github.com/ruby/resolv" 17 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 18 | spec.licenses = ["Ruby", "BSD-2-Clause"] 19 | spec.extensions << "ext/win32/resolv/extconf.rb" 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}] 25 | spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0") 26 | spec.bindir = "exe" 27 | spec.executables = [] 28 | spec.require_paths = ["lib"] 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/resolv' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/resolv 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 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 | ```bash 18 | bundle install 19 | ``` 20 | 21 | Or install it yourself as: 22 | 23 | ```bash 24 | gem install resolv 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```ruby 30 | p Resolv.getaddress "www.ruby-lang.org" 31 | p Resolv.getname "210.251.121.214" 32 | 33 | Resolv::DNS.open do |dns| 34 | ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A 35 | p ress.map(&:address) 36 | ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX 37 | p ress.map { |r| [r.exchange.to_s, r.preference] } 38 | end 39 | ``` 40 | 41 | ## Development 42 | 43 | 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. 44 | 45 | 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). 46 | 47 | ## Contributing 48 | 49 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/resolv. 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby-jruby 10 | min_version: 2.5 11 | 12 | build: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest, windows-latest ] 19 | include: 20 | - ruby: mswin 21 | os: windows-latest 22 | exclude: 23 | - ruby: 2.5 24 | os: macos-latest 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v6 28 | - name: Set up Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - name: Run test 34 | run: bundle exec rake test 35 | timeout-minutes: 3 36 | continue-on-error: ${{ startsWith(matrix.ruby, 'jruby') }} 37 | - name: Build package 38 | id: build 39 | shell: bash 40 | run: | 41 | if ruby -e 'exit RUBY_VERSION>="3.0."'; then 42 | bundle exec rake build 43 | set pkg/*.gem 44 | echo pkg=$1 >> $GITHUB_OUTPUT 45 | fi 46 | - name: Install gem 47 | run: | 48 | gem install ${{ steps.build.outputs.pkg }} 49 | ruby -rresolv -e 'puts $LOADED_FEATURES.grep(/resolv/)' 50 | ruby -rresolv -e 'puts Resolv::VERSION' 51 | if: ${{ steps.build.outputs.pkg }} 52 | continue-on-error: ${{ startsWith(matrix.ruby, 'jruby') }} 53 | -------------------------------------------------------------------------------- /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 | 32 | bug17524 = "[ruby-core:101992]" 33 | assert_match(Resolv::IPv6::Regex, "FE80::20D:3AFF:FE7D:9760%ruby_3.0.0-1", bug17524) 34 | assert_match(Resolv::IPv6::Regex, "fe80::1%ruby_3.0.0-1", bug17524) 35 | end 36 | 37 | def test_valid_socket_ip_address_list 38 | Socket.ip_address_list.each do |addr| 39 | ip = addr.ip_address 40 | assert_match(Resolv::AddressRegex, ip) 41 | assert_equal(ip, Resolv.getaddress(ip)) 42 | end 43 | end 44 | 45 | def test_invalid_byte_comment 46 | bug9273 = '[ruby-core:59239] [Bug #9273]' 47 | Tempfile.create('resolv_test_addr_') do |tmpfile| 48 | tmpfile.print("\xff\x00\x40") 49 | tmpfile.close 50 | hosts = Resolv::Hosts.new(tmpfile.path) 51 | assert_nothing_raised(ArgumentError, bug9273) do 52 | hosts.each_address("") {break} 53 | end 54 | end 55 | end 56 | 57 | def test_hosts_by_command 58 | Dir.mktmpdir do |dir| 59 | Dir.chdir(dir) do 60 | hosts = Resolv::Hosts.new("|echo error") 61 | assert_raise(Errno::ENOENT, Errno::EINVAL) do 62 | hosts.each_name("") {} 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /ext/win32/resolv/lib/resolv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | =begin 3 | = Win32 DNS and DHCP I/F 4 | 5 | =end 6 | 7 | require 'win32/resolv.so' 8 | 9 | module Win32 10 | module Resolv 11 | # Error at Win32 API 12 | class Error < StandardError 13 | # +code+ Win32 Error code 14 | # +message+ Formatted message for +code+ 15 | def initialize(code, message) 16 | super(message) 17 | @code = code 18 | end 19 | 20 | # Win32 error code 21 | attr_reader :code 22 | end 23 | 24 | def self.get_hosts_path 25 | path = get_hosts_dir 26 | path = File.expand_path('hosts', path) 27 | File.exist?(path) ? path : nil 28 | end 29 | 30 | def self.get_resolv_info 31 | search, nameserver = get_info 32 | if search.empty? 33 | search = nil 34 | else 35 | search.delete("") 36 | search.uniq! 37 | end 38 | if nameserver.empty? 39 | nameserver = nil 40 | else 41 | nameserver.delete("") 42 | nameserver.delete("0.0.0.0") 43 | nameserver.uniq! 44 | end 45 | [ search, nameserver ] 46 | end 47 | 48 | class << self 49 | private 50 | def get_hosts_dir 51 | tcpip_params do |params| 52 | params.value('DataBasePath') 53 | end 54 | end 55 | 56 | def get_info 57 | search = nil 58 | nameserver = get_dns_server_list 59 | 60 | tcpip_params do |params| 61 | slist = params.value('SearchList') 62 | search = slist.split(/,\s*/) if slist and !slist.empty? 63 | 64 | if add_search = search.nil? 65 | search = [] 66 | domain = params.value('Domain') 67 | 68 | if domain and !domain.empty? 69 | search = [ domain ] 70 | udmnd = params.value('UseDomainNameDevolution') 71 | if udmnd&.nonzero? 72 | if /^\w+\./ =~ domain 73 | devo = $' 74 | end 75 | end 76 | end 77 | end 78 | 79 | params.open('Interfaces') do |reg| 80 | reg.each_key do |iface| 81 | next unless ns = %w[NameServer DhcpNameServer].find do |key| 82 | ns = iface.value(key) 83 | break ns.split(/[,\s]\s*/) if ns and !ns.empty? 84 | end 85 | 86 | next if (nameserver & ns).empty? 87 | 88 | if add_search 89 | [ 'Domain', 'DhcpDomain' ].each do |key| 90 | dom = iface.value(key) 91 | if dom and !dom.empty? 92 | search.concat(dom.split(/,\s*/)) 93 | break 94 | end 95 | end 96 | end 97 | end 98 | end 99 | 100 | search << devo if add_search and devo 101 | end 102 | [ search.uniq, nameserver.uniq ] 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /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 | 27 | def test_srv_no_compress 28 | # Domain name in SRV RDATA should not be compressed 29 | issue29 = 'https://github.com/ruby/resolv/issues/29' 30 | m = Resolv::DNS::Message.new(0) 31 | m.add_answer('example.com', 0, Resolv::DNS::Resource::IN::SRV.new(0, 0, 0, 'www.example.com')) 32 | assert_equal "\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x07example\x03com\x00\x00\x21\x00\x01\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00", m.encode, issue29 33 | end 34 | end 35 | 36 | class TestResolvResourceCAA < Test::Unit::TestCase 37 | def test_caa_roundtrip 38 | raw_msg = "\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x03new\x07example\x03com\x00\x01\x01\x00\x01\x00\x00\x00\x00\x00\x16\x00\x05issueca1.example.net\xC0\x0C\x01\x01\x00\x01\x00\x00\x00\x00\x00\x0C\x80\x03tbsUnknown".b 39 | 40 | m = Resolv::DNS::Message.new(0) 41 | m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(0, 'issue', 'ca1.example.net')) 42 | m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(128, 'tbs', 'Unknown')) 43 | assert_equal raw_msg, m.encode 44 | 45 | m = Resolv::DNS::Message.decode(raw_msg) 46 | assert_equal 2, m.answer.size 47 | _, _, caa0 = m.answer[0] 48 | assert_equal 0, caa0.flags 49 | assert_equal false, caa0.critical? 50 | assert_equal 'issue', caa0.tag 51 | assert_equal 'ca1.example.net', caa0.value 52 | _, _, caa1 = m.answer[1] 53 | assert_equal true, caa1.critical? 54 | assert_equal 128, caa1.flags 55 | assert_equal 'tbs', caa1.tag 56 | assert_equal 'Unknown', caa1.value 57 | end 58 | 59 | def test_caa_stackoverflow 60 | # gathered in the wild 61 | raw_msg = "\x8D\x32\x81\x80\x00\x01\x00\x0B\x00\x00\x00\x00\x0Dstackoverflow\x03com\x00\x01\x01\x00\x01\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x13\x00\x05issuecomodoca.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x00\x05issuedigicert.com; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x16\x00\x05issueletsencrypt.org\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x29\x00\x05issuepki.goog; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x12\x00\x05issuesectigo.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x17\x00\x09issuewildcomodoca.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x31\x00\x09issuewilddigicert.com; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x1A\x00\x09issuewildletsencrypt.org\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x00\x09issuewildpki.goog; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x16\x00\x09issuewildsectigo.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x80\x05iodefmailto:sysadmin-team@stackoverflow.com".b 62 | 63 | m = Resolv::DNS::Message.decode(raw_msg) 64 | assert_equal 11, m.answer.size 65 | _, _, caa3 = m.answer[3] 66 | assert_equal 0, caa3.flags 67 | assert_equal 'issue', caa3.tag 68 | assert_equal 'pki.goog; cansignhttpexchanges=yes', caa3.value 69 | _, _, caa8 = m.answer[8] 70 | assert_equal 0, caa8.flags 71 | assert_equal 'issuewild', caa8.tag 72 | assert_equal 'pki.goog; cansignhttpexchanges=yes', caa8.value 73 | _, _, caa10 = m.answer[10] 74 | assert_equal 128, caa10.flags 75 | assert_equal 'iodef', caa10.tag 76 | assert_equal 'mailto:sysadmin-team@stackoverflow.com', caa10.value 77 | end 78 | 79 | def test_caa_flags 80 | assert_equal 255, 81 | Resolv::DNS::Resource::IN::CAA.new(255, 'issue', 'ca1.example.net').flags 82 | assert_raise(ArgumentError) do 83 | Resolv::DNS::Resource::IN::CAA.new(256, 'issue', 'ca1.example.net') 84 | end 85 | 86 | assert_raise(ArgumentError) do 87 | Resolv::DNS::Resource::IN::CAA.new(-1, 'issue', 'ca1.example.net') 88 | end 89 | end 90 | 91 | def test_caa_tag 92 | assert_raise(ArgumentError, 'Empty tag should be rejected') do 93 | Resolv::DNS::Resource::IN::CAA.new(0, '', 'ca1.example.net') 94 | end 95 | 96 | assert_equal '123456789012345', 97 | Resolv::DNS::Resource::IN::CAA.new(0, '123456789012345', 'ca1.example.net').tag 98 | assert_raise(ArgumentError, 'Tag longer than 15 bytes should be rejected') do 99 | Resolv::DNS::Resource::IN::CAA.new(0, '1234567890123456', 'ca1.example.net') 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/resolv/test_svcb_https.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'resolv' 4 | 5 | class TestResolvSvcbHttps < Test::Unit::TestCase 6 | # Wraps a RR in answer section 7 | def wrap_rdata(rrtype, rrclass, rdata) 8 | [ 9 | "\x00\x00\x00\x00", # ID/FLAGS 10 | [0, 1, 0, 0].pack('nnnn'), # QDCOUNT/ANCOUNT/NSCOUNT/ARCOUNT 11 | "\x07example\x03com\x00", # NAME 12 | [rrtype, rrclass, 0, rdata.bytesize].pack('nnNn'), # TYPE/CLASS/TTL/RDLENGTH 13 | rdata, 14 | ].join.b 15 | end 16 | 17 | def test_svcparams 18 | params = Resolv::DNS::SvcParams.new([Resolv::DNS::SvcParam::Mandatory.new([1])]) 19 | 20 | assert_equal 1, params.count 21 | 22 | params.add Resolv::DNS::SvcParam::NoDefaultALPN.new 23 | params.add Resolv::DNS::SvcParam::ALPN.new(%w[h2 h3]) 24 | 25 | assert_equal 3, params.count 26 | 27 | assert_equal [1], params[:mandatory].keys 28 | assert_equal [1], params[0].keys 29 | 30 | assert_equal %w[h2 h3], params[:alpn].protocol_ids 31 | assert_equal %w[h2 h3], params[1].protocol_ids 32 | 33 | params.delete :mandatory 34 | params.delete :alpn 35 | 36 | assert_equal 1, params.count 37 | 38 | assert_nil params[:mandatory] 39 | assert_nil params[1] 40 | 41 | ary = params.each.to_a 42 | 43 | assert_instance_of Resolv::DNS::SvcParam::NoDefaultALPN, ary.first 44 | end 45 | 46 | def test_svcb 47 | rr = Resolv::DNS::Resource::IN::SVCB.new(0, 'example.com.') 48 | 49 | assert_equal 0, rr.priority 50 | assert rr.alias_mode? 51 | assert !rr.service_mode? 52 | assert_equal Resolv::DNS::Name.create('example.com.'), rr.target 53 | assert rr.params.empty? 54 | 55 | rr = Resolv::DNS::Resource::IN::SVCB.new(16, 'example.com.', [ 56 | Resolv::DNS::SvcParam::ALPN.new(%w[h2 h3]), 57 | ]) 58 | 59 | assert_equal 16, rr.priority 60 | assert !rr.alias_mode? 61 | assert rr.service_mode? 62 | 63 | assert_equal 1, rr.params.count 64 | assert_instance_of Resolv::DNS::SvcParam::ALPN, rr.params[:alpn] 65 | end 66 | 67 | def test_svcb_encode_order 68 | msg = Resolv::DNS::Message.new(0) 69 | msg.add_answer( 70 | 'example.com.', 0, 71 | Resolv::DNS::Resource::IN::SVCB.new(16, 'foo.example.org.', [ 72 | Resolv::DNS::SvcParam::ALPN.new(%w[h2 h3-19]), 73 | Resolv::DNS::SvcParam::Mandatory.new([4, 1]), 74 | Resolv::DNS::SvcParam::IPv4Hint.new(['192.0.2.1']), 75 | ]) 76 | ) 77 | 78 | expected = wrap_rdata 64, 1, "\x00\x10\x03foo\x07example\x03org\x00" + 79 | "\x00\x00\x00\x04\x00\x01\x00\x04" + 80 | "\x00\x01\x00\x09\x02h2\x05h3-19" + 81 | "\x00\x04\x00\x04\xc0\x00\x02\x01" 82 | 83 | assert_equal expected, msg.encode 84 | end 85 | 86 | ## Test vectors from [RFC9460] 87 | 88 | def test_alias_mode 89 | wire = wrap_rdata 65, 1, "\x00\x00\x03foo\x07example\x03com\x00" 90 | msg = Resolv::DNS::Message.decode(wire) 91 | _, _, rr = msg.answer.first 92 | 93 | assert_equal 0, rr.priority 94 | assert_equal Resolv::DNS::Name.create('foo.example.com.'), rr.target 95 | assert_equal 0, rr.params.count 96 | 97 | assert_equal wire, msg.encode 98 | end 99 | 100 | def test_target_name_is_root 101 | wire = wrap_rdata 64, 1, "\x00\x01\x00" 102 | msg = Resolv::DNS::Message.decode(wire) 103 | _, _, rr = msg.answer.first 104 | 105 | assert_equal 1, rr.priority 106 | assert_equal Resolv::DNS::Name.create('.'), rr.target 107 | assert_equal 0, rr.params.count 108 | 109 | assert_equal wire, msg.encode 110 | end 111 | 112 | def test_specifies_port 113 | wire = wrap_rdata 64, 1, "\x00\x10\x03foo\x07example\x03com\x00" + 114 | "\x00\x03\x00\x02\x00\x35" 115 | msg = Resolv::DNS::Message.decode(wire) 116 | _, _, rr = msg.answer.first 117 | 118 | assert_equal 16, rr.priority 119 | assert_equal Resolv::DNS::Name.create('foo.example.com.'), rr.target 120 | assert_equal 1, rr.params.count 121 | assert_equal 53, rr.params[:port].port 122 | 123 | assert_equal wire, msg.encode 124 | end 125 | 126 | def test_generic_key 127 | wire = wrap_rdata 64, 1, "\x00\x01\x03foo\x07example\x03com\x00" + 128 | "\x02\x9b\x00\x05hello" 129 | msg = Resolv::DNS::Message.decode(wire) 130 | _, _, rr = msg.answer.first 131 | 132 | assert_equal 1, rr.priority 133 | assert_equal Resolv::DNS::Name.create('foo.example.com.'), rr.target 134 | assert_equal 1, rr.params.count 135 | assert_equal 'hello', rr.params[:key667].value 136 | 137 | assert_equal wire, msg.encode 138 | end 139 | 140 | def test_two_ipv6hints 141 | wire = wrap_rdata 64, 1, "\x00\x01\x03foo\x07example\x03com\x00" + 142 | "\x00\x06\x00\x20" + 143 | ("\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + 144 | "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x53\x00\x01") 145 | msg = Resolv::DNS::Message.decode(wire) 146 | _, _, rr = msg.answer.first 147 | 148 | assert_equal 1, rr.priority 149 | assert_equal Resolv::DNS::Name.create('foo.example.com.'), rr.target 150 | assert_equal 1, rr.params.count 151 | assert_equal [Resolv::IPv6.create('2001:db8::1'), Resolv::IPv6.create('2001:db8::53:1')], 152 | rr.params[:ipv6hint].addresses 153 | 154 | assert_equal wire, msg.encode 155 | end 156 | 157 | def test_ipv6hint_embedded_ipv4 158 | wire = wrap_rdata 64, 1, "\x00\x01\x07example\x03com\x00" + 159 | "\x00\x06\x00\x10\x20\x01\x0d\xb8\x01\x22\x03\x44\x00\x00\x00\x00\xc0\x00\x02\x21" 160 | msg = Resolv::DNS::Message.decode(wire) 161 | _, _, rr = msg.answer.first 162 | 163 | assert_equal 1, rr.priority 164 | assert_equal Resolv::DNS::Name.create('example.com.'), rr.target 165 | assert_equal 1, rr.params.count 166 | assert_equal [Resolv::IPv6.create('2001:db8:122:344::192.0.2.33')], 167 | rr.params[:ipv6hint].addresses 168 | 169 | assert_equal wire, msg.encode 170 | end 171 | 172 | def test_mandatory_alpn_ipv4hint 173 | wire = wrap_rdata 64, 1, "\x00\x10\x03foo\x07example\x03org\x00" + 174 | "\x00\x00\x00\x04\x00\x01\x00\x04" + 175 | "\x00\x01\x00\x09\x02h2\x05h3-19" + 176 | "\x00\x04\x00\x04\xc0\x00\x02\x01" 177 | msg = Resolv::DNS::Message.decode(wire) 178 | _, _, rr = msg.answer.first 179 | 180 | assert_equal 16, rr.priority 181 | assert_equal Resolv::DNS::Name.create('foo.example.org.'), rr.target 182 | assert_equal 3, rr.params.count 183 | assert_equal [1, 4], rr.params[:mandatory].keys 184 | assert_equal ['h2', 'h3-19'], rr.params[:alpn].protocol_ids 185 | assert_equal [Resolv::IPv4.create('192.0.2.1')], rr.params[:ipv4hint].addresses 186 | 187 | assert_equal wire, msg.encode 188 | end 189 | 190 | def test_alpn_comma_backslash 191 | wire = wrap_rdata 64, 1, "\x00\x10\x03foo\x07example\x03org\x00" + 192 | "\x00\x01\x00\x0c\x08f\\oo,bar\x02h2" 193 | msg = Resolv::DNS::Message.decode(wire) 194 | _, _, rr = msg.answer.first 195 | 196 | assert_equal 16, rr.priority 197 | assert_equal Resolv::DNS::Name.create('foo.example.org.'), rr.target 198 | assert_equal 1, rr.params.count 199 | assert_equal ['f\oo,bar', 'h2'], rr.params[:alpn].protocol_ids 200 | 201 | assert_equal wire, msg.encode 202 | end 203 | 204 | ## For [RFC9461] 205 | 206 | def test_dohpath 207 | wire = wrap_rdata 64, 1, "\x00\x01\x03one\x03one\x03one\x03one\x00" + 208 | "\x00\x01\x00\x03\x02h2" + 209 | "\x00\x03\x00\x02\x01\xbb" + 210 | "\x00\x04\x00\x08\x01\x01\x01\x01\x01\x00\x00\x01" + 211 | "\x00\x06\x00\x20" + 212 | ("\x26\x06\x47\x00\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11" + 213 | "\x26\x06\x47\x00\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x01") + 214 | "\x00\x07\x00\x10/dns-query{?dns}" 215 | msg = Resolv::DNS::Message.decode(wire) 216 | _, _, rr = msg.answer.first 217 | 218 | assert_equal 1, rr.priority 219 | assert_equal Resolv::DNS::Name.create('one.one.one.one.'), rr.target 220 | assert_equal 5, rr.params.count 221 | assert_equal ['h2'], rr.params[:alpn].protocol_ids 222 | assert_equal 443, rr.params[:port].port 223 | assert_equal [Resolv::IPv4.create('1.1.1.1'), Resolv::IPv4.create('1.0.0.1')], 224 | rr.params[:ipv4hint].addresses 225 | assert_equal [Resolv::IPv6.create('2606:4700:4700::1111'), Resolv::IPv6.create('2606:4700:4700::1001')], 226 | rr.params[:ipv6hint].addresses 227 | assert_equal '/dns-query{?dns}', rr.params[:dohpath].template 228 | 229 | assert_equal wire, msg.encode 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /ext/win32/resolv/resolv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #ifndef NTDDI_VERSION 6 | #define NTDDI_VERSION 0x06000000 7 | #endif 8 | #include 9 | 10 | #ifndef numberof 11 | #define numberof(array) ((int)(sizeof(array) / sizeof((array)[0]))) 12 | #endif 13 | 14 | static VALUE 15 | w32error_make_error(DWORD e) 16 | { 17 | char buffer[512], *p; 18 | DWORD source = 0; 19 | VALUE args[2]; 20 | if (!FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | 21 | FORMAT_MESSAGE_IGNORE_INSERTS, &source, e, 22 | MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), 23 | buffer, sizeof(buffer), NULL)) { 24 | snprintf(buffer, sizeof(buffer), "Unknown Error %lu", (unsigned long)e); 25 | } 26 | p = buffer; 27 | while ((p = strpbrk(p, "\r\n")) != NULL) { 28 | memmove(p, p + 1, strlen(p)); 29 | if (!p[1]) { 30 | p[0] = '\0'; 31 | break; 32 | } 33 | } 34 | args[0] = ULONG2NUM(e); 35 | args[1] = rb_str_new_cstr(buffer); 36 | return rb_class_new_instance(2, args, rb_path2class("Win32::Resolv::Error")); 37 | } 38 | 39 | static void 40 | w32error_check(DWORD e) 41 | { 42 | if (e != NO_ERROR) { 43 | rb_exc_raise(w32error_make_error(e)); 44 | } 45 | } 46 | 47 | static VALUE 48 | wchar_to_utf8(const WCHAR *w, int n) 49 | { 50 | int clen = WideCharToMultiByte(CP_UTF8, 0, w, n, NULL, 0, NULL, NULL); 51 | VALUE str = rb_enc_str_new(NULL, clen, rb_utf8_encoding()); 52 | WideCharToMultiByte(CP_UTF8, 0, w, n, RSTRING_PTR(str), clen, NULL, NULL); 53 | return str; 54 | } 55 | 56 | static VALUE 57 | get_dns_server_list(VALUE self) 58 | { 59 | FIXED_INFO *fixedinfo = NULL; 60 | ULONG buflen = 0; 61 | DWORD ret; 62 | VALUE buf, nameservers = Qnil; 63 | 64 | ret = GetNetworkParams(NULL, &buflen); 65 | if (ret != ERROR_BUFFER_OVERFLOW) w32error_check(ret); 66 | fixedinfo = ALLOCV(buf, buflen); 67 | ret = GetNetworkParams(fixedinfo, &buflen); 68 | if (ret == NO_ERROR) { 69 | const IP_ADDR_STRING *ipaddr = &fixedinfo->DnsServerList; 70 | nameservers = rb_ary_new(); 71 | do { 72 | const char *s = ipaddr->IpAddress.String; 73 | if (!*s) continue; 74 | if (strcmp(s, "0.0.0.0") == 0) continue; 75 | rb_ary_push(nameservers, rb_str_new_cstr(s)); 76 | } while ((ipaddr = ipaddr->Next) != NULL); 77 | } 78 | ALLOCV_END(buf); 79 | w32error_check(ret); 80 | 81 | return nameservers; 82 | } 83 | 84 | 85 | static const WCHAR TCPIP_Params[] = L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"; 86 | 87 | static void 88 | hkey_finalize(void *p) 89 | { 90 | RegCloseKey((HKEY)p); 91 | } 92 | 93 | static const rb_data_type_t hkey_type = { 94 | "RegKey", 95 | {0, hkey_finalize}, 96 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, 97 | }; 98 | 99 | static VALUE 100 | hkey_close(VALUE self) 101 | { 102 | RegCloseKey((HKEY)DATA_PTR(self)); 103 | DATA_PTR(self) = 0; 104 | return self; 105 | } 106 | 107 | static VALUE reg_key_class; 108 | 109 | static VALUE 110 | reg_open_key(VALUE klass, HKEY hkey, const WCHAR *wname) 111 | { 112 | VALUE k = TypedData_Wrap_Struct(klass, &hkey_type, NULL); 113 | DWORD e = RegOpenKeyExW(hkey, wname, 0, KEY_READ, (HKEY *)&DATA_PTR(k)); 114 | if (e == ERROR_FILE_NOT_FOUND) return Qnil; 115 | w32error_check(e); 116 | return rb_ensure(rb_yield, k, hkey_close, k); 117 | } 118 | 119 | static VALUE 120 | tcpip_params_open(VALUE klass) 121 | { 122 | return reg_open_key(reg_key_class, HKEY_LOCAL_MACHINE, TCPIP_Params); 123 | } 124 | 125 | static int 126 | to_wname(VALUE *name, WCHAR *wname, int wlen) 127 | { 128 | const char *n = StringValueCStr(*name); 129 | int nlen = RSTRING_LEN(*name); 130 | int len = MultiByteToWideChar(CP_UTF8, 0, n, nlen, wname, wlen - 1); 131 | if (len == 0) w32error_check(GetLastError()); 132 | if (len >= wlen) rb_raise(rb_eArgError, "too long name"); 133 | wname[len] = L'\0'; 134 | return len; 135 | } 136 | 137 | static VALUE 138 | reg_open(VALUE self, VALUE name) 139 | { 140 | HKEY hkey = DATA_PTR(self); 141 | WCHAR wname[256]; 142 | to_wname(&name, wname, numberof(wname)); 143 | return reg_open_key(CLASS_OF(self), hkey, wname); 144 | } 145 | 146 | 147 | static VALUE 148 | reg_each_key(VALUE self) 149 | { 150 | WCHAR wname[256]; 151 | HKEY hkey = DATA_PTR(self); 152 | VALUE k = TypedData_Wrap_Struct(CLASS_OF(self), &hkey_type, NULL); 153 | DWORD i, e, n; 154 | for (i = 0; n = numberof(wname), (e = RegEnumKeyExW(hkey, i, wname, &n, NULL, NULL, NULL, NULL)) == ERROR_SUCCESS; i++) { 155 | e = RegOpenKeyExW(hkey, wname, 0, KEY_READ, (HKEY *)&DATA_PTR(k)); 156 | w32error_check(e); 157 | rb_ensure(rb_yield, k, hkey_close, k); 158 | } 159 | if (e != ERROR_NO_MORE_ITEMS) w32error_check(e); 160 | return self; 161 | } 162 | 163 | static inline DWORD 164 | swap_dw(DWORD x) 165 | { 166 | #if defined(_MSC_VER) 167 | return _byteswap_ulong(x); 168 | #else 169 | return __builtin_bswap32(x); 170 | #endif 171 | } 172 | 173 | static VALUE 174 | reg_value(VALUE self, VALUE name) 175 | { 176 | HKEY hkey = DATA_PTR(self); 177 | DWORD type = 0, size = 0, e; 178 | VALUE result, value_buffer; 179 | void *buffer; 180 | WCHAR wname[256]; 181 | to_wname(&name, wname, numberof(wname)); 182 | e = RegGetValueW(hkey, NULL, wname, RRF_RT_ANY, &type, NULL, &size); 183 | if (e == ERROR_FILE_NOT_FOUND) return Qnil; 184 | w32error_check(e); 185 | # define get_value_2nd(data, dsize) do { \ 186 | DWORD type2 = type; \ 187 | w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_ANY, &type2, data, dsize)); \ 188 | if (type != type2) { \ 189 | rb_raise(rb_eRuntimeError, "registry value type changed %lu -> %lu", \ 190 | (unsigned long)type, (unsigned long)type2); \ 191 | } \ 192 | } while (0) 193 | 194 | switch (type) { 195 | case REG_DWORD: case REG_DWORD_BIG_ENDIAN: 196 | { 197 | DWORD d; 198 | if (size != sizeof(d)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", (unsigned long)size); 199 | w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_REG_DWORD, &type, &d, &size)); 200 | if (type == REG_DWORD_BIG_ENDIAN) d = swap_dw(d); 201 | return ULONG2NUM(d); 202 | } 203 | case REG_QWORD: 204 | { 205 | QWORD q; 206 | if (size != sizeof(q)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", (unsigned long)size); 207 | w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_REG_QWORD, &type, &q, &size)); 208 | return ULL2NUM(q); 209 | } 210 | case REG_SZ: case REG_MULTI_SZ: case REG_EXPAND_SZ: 211 | if (size % sizeof(WCHAR)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", (unsigned long)size); 212 | buffer = ALLOCV_N(char, value_buffer, size); 213 | get_value_2nd(buffer, &size); 214 | if (type == REG_MULTI_SZ) { 215 | const WCHAR *w = (WCHAR *)buffer; 216 | result = rb_ary_new(); 217 | size /= sizeof(WCHAR); 218 | size -= 1; 219 | for (size_t i = 0; i < size; ++i) { 220 | int n = lstrlenW(w+i); 221 | rb_ary_push(result, wchar_to_utf8(w+i, n)); 222 | i += n; 223 | } 224 | } 225 | else { 226 | result = wchar_to_utf8((WCHAR *)buffer, lstrlenW((WCHAR *)buffer)); 227 | } 228 | ALLOCV_END(value_buffer); 229 | break; 230 | default: 231 | result = rb_str_new(0, size); 232 | get_value_2nd(RSTRING_PTR(result), &size); 233 | rb_str_set_len(result, size); 234 | break; 235 | } 236 | return result; 237 | } 238 | 239 | void 240 | InitVM_resolv(void) 241 | { 242 | VALUE mWin32 = rb_define_module("Win32"); 243 | VALUE resolv = rb_define_module_under(mWin32, "Resolv"); 244 | VALUE singl = rb_singleton_class(resolv); 245 | VALUE regkey = rb_define_class_under(resolv, "registry key", rb_cObject); 246 | 247 | reg_key_class = regkey; 248 | rb_undef_alloc_func(regkey); 249 | rb_define_private_method(singl, "get_dns_server_list", get_dns_server_list, 0); 250 | rb_define_private_method(singl, "tcpip_params", tcpip_params_open, 0); 251 | rb_define_method(regkey, "open", reg_open, 1); 252 | rb_define_method(regkey, "each_key", reg_each_key, 0); 253 | rb_define_method(regkey, "value", reg_value, 1); 254 | } 255 | 256 | void 257 | Init_resolv(void) 258 | { 259 | InitVM(resolv); 260 | } 261 | -------------------------------------------------------------------------------- /test/resolv/test_dns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'resolv' 4 | require 'socket' 5 | require 'tempfile' 6 | 7 | class Object # :nodoc: 8 | def stub name, val_or_callable, &block 9 | new_name = "__minitest_stub__#{name}" 10 | 11 | metaclass = class << self; self; end 12 | 13 | if respond_to? name and not methods.map(&:to_s).include? name.to_s then 14 | metaclass.send :define_method, name do |*args| 15 | super(*args) 16 | end 17 | end 18 | 19 | metaclass.send :alias_method, new_name, name 20 | 21 | metaclass.send :define_method, name do |*args| 22 | if val_or_callable.respond_to? :call then 23 | val_or_callable.call(*args) 24 | else 25 | val_or_callable 26 | end 27 | end 28 | 29 | yield self 30 | ensure 31 | metaclass.send :undef_method, name 32 | metaclass.send :alias_method, name, new_name 33 | metaclass.send :undef_method, new_name 34 | end unless method_defined?(:stub) # lib/rubygems/test_case.rb also has the same method definition 35 | end 36 | 37 | class TestResolvDNS < Test::Unit::TestCase 38 | def setup 39 | @save_do_not_reverse_lookup = BasicSocket.do_not_reverse_lookup 40 | BasicSocket.do_not_reverse_lookup = true 41 | end 42 | 43 | def teardown 44 | BasicSocket.do_not_reverse_lookup = @save_do_not_reverse_lookup 45 | end 46 | 47 | def with_tcp(host, port) 48 | t = TCPServer.new(host, port) 49 | begin 50 | t.listen(1) 51 | yield t 52 | ensure 53 | t.close 54 | end 55 | end 56 | 57 | def with_udp(host, port) 58 | u = UDPSocket.new 59 | begin 60 | u.bind(host, port) 61 | yield u 62 | ensure 63 | u.close 64 | end 65 | end 66 | 67 | def with_udp_and_tcp(host, port) 68 | if port == 0 69 | # Automatic port; we might need to retry until we find a port which is free on both UDP _and_ TCP. 70 | retries_remaining = 10 71 | ts = [] 72 | us = [] 73 | begin 74 | begin 75 | us << UDPSocket.new 76 | us.last.bind(host, 0) 77 | _, udp_port, _, _ = us.last.addr 78 | ts << TCPServer.new(host, udp_port) 79 | ts.last.listen(1) 80 | rescue Errno::EADDRINUSE, Errno::EACCES 81 | # ADDRINUSE is what should get thrown if we try and bind a port which is already bound on UNIXen, 82 | # but windows can sometimes throw EACCESS. 83 | # See: https://stackoverflow.com/questions/48478869/cannot-bind-to-some-ports-due-to-permission-denied 84 | retries_remaining -= 1 85 | retry if retries_remaining > 0 86 | # Windows and MinGW CI can't bind to the same port with ten retries. 87 | omit if /mswin|mingw/ =~ RUBY_PLATFORM 88 | raise 89 | end 90 | 91 | # If we get to this point, we have a valid t & u socket 92 | yield us.last, ts.last 93 | ensure 94 | ts.each(&:close) 95 | us.each(&:close) 96 | end 97 | else 98 | # Explicitly specified port, don't retry the bind. 99 | with_udp(host, port) do |u| 100 | with_tcp(host, port) do |t| 101 | yield u, t 102 | end 103 | end 104 | end 105 | end 106 | 107 | # [ruby-core:65836] 108 | def test_resolve_with_2_ndots 109 | conf = Resolv::DNS::Config.new :nameserver => ['127.0.0.1'], :ndots => 2 110 | assert conf.single? 111 | 112 | candidates = [] 113 | conf.resolv('example.com') { |candidate, *args| 114 | candidates << candidate 115 | raise Resolv::DNS::Config::NXDomain 116 | } 117 | n = Resolv::DNS::Name.create 'example.com.' 118 | assert_equal n, candidates.last 119 | end 120 | 121 | def test_query_ipv4_address 122 | begin 123 | OpenSSL 124 | rescue LoadError 125 | omit 'autoload problem. see [ruby-dev:45021][Bug #5786]' 126 | end if defined?(OpenSSL) 127 | 128 | with_udp('127.0.0.1', 0) {|u| 129 | _, server_port, _, server_address = u.addr 130 | begin 131 | client_thread = Thread.new { 132 | Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns| 133 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 134 | } 135 | } 136 | server_thread = Thread.new { 137 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} 138 | id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 139 | qr = (word2 & 0x8000) >> 15 140 | opcode = (word2 & 0x7800) >> 11 141 | aa = (word2 & 0x0400) >> 10 142 | tc = (word2 & 0x0200) >> 9 143 | rd = (word2 & 0x0100) >> 8 144 | ra = (word2 & 0x0080) >> 7 145 | z = (word2 & 0x0070) >> 4 146 | rcode = word2 & 0x000f 147 | rest = msg[12..-1] 148 | assert_equal(0, qr) # 0:query 1:response 149 | assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS 150 | assert_equal(0, aa) # Authoritative Answer 151 | assert_equal(0, tc) # TrunCation 152 | assert_equal(1, rd) # Recursion Desired 153 | assert_equal(0, ra) # Recursion Available 154 | assert_equal(0, z) # Reserved for future use 155 | assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused 156 | assert_equal(1, qdcount) # number of entries in the question section. 157 | assert_equal(0, ancount) # number of entries in the answer section. 158 | assert_equal(0, nscount) # number of entries in the authority records section. 159 | assert_equal(0, arcount) # number of entries in the additional records section. 160 | name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C") 161 | assert_operator(rest, :start_with?, name) 162 | rest = rest[name.length..-1] 163 | assert_equal(4, rest.length) 164 | qtype, _ = rest.unpack("nn") 165 | assert_equal(1, qtype) # A 166 | assert_equal(1, qtype) # IN 167 | id = id 168 | qr = 1 169 | opcode = opcode 170 | aa = 0 171 | tc = 0 172 | rd = rd 173 | ra = 1 174 | z = 0 175 | rcode = 0 176 | qdcount = 0 177 | ancount = 1 178 | nscount = 0 179 | arcount = 0 180 | word2 = (qr << 15) | 181 | (opcode << 11) | 182 | (aa << 10) | 183 | (tc << 9) | 184 | (rd << 8) | 185 | (ra << 7) | 186 | (z << 4) | 187 | rcode 188 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 189 | type = 1 190 | klass = 1 191 | ttl = 3600 192 | rdlength = 4 193 | rdata = [192,0,2,1].pack("CCCC") # 192.0.2.1 (TEST-NET address) RFC 3330 194 | rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*") 195 | msg << rr 196 | u.send(msg, 0, client_address, client_port) 197 | } 198 | result, _ = assert_join_threads([client_thread, server_thread]) 199 | assert_instance_of(Array, result) 200 | assert_equal(1, result.length) 201 | rr = result[0] 202 | assert_instance_of(Resolv::DNS::Resource::IN::A, rr) 203 | assert_instance_of(Resolv::IPv4, rr.address) 204 | assert_equal("192.0.2.1", rr.address.to_s) 205 | assert_equal(3600, rr.ttl) 206 | end 207 | } 208 | end 209 | 210 | def test_query_ipv4_address_truncated_tcp_fallback 211 | begin 212 | OpenSSL 213 | rescue LoadError 214 | skip 'autoload problem. see [ruby-dev:45021][Bug #5786]' 215 | end if defined?(OpenSSL) 216 | 217 | num_records = 50 218 | 219 | with_udp_and_tcp('127.0.0.1', 0) {|u, t| 220 | _, server_port, _, server_address = u.addr 221 | client_thread = Thread.new { 222 | Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns| 223 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 224 | } 225 | } 226 | udp_server_thread = Thread.new { 227 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} 228 | id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 229 | qr = (word2 & 0x8000) >> 15 230 | opcode = (word2 & 0x7800) >> 11 231 | aa = (word2 & 0x0400) >> 10 232 | tc = (word2 & 0x0200) >> 9 233 | rd = (word2 & 0x0100) >> 8 234 | ra = (word2 & 0x0080) >> 7 235 | z = (word2 & 0x0070) >> 4 236 | rcode = word2 & 0x000f 237 | rest = msg[12..-1] 238 | assert_equal(0, qr) # 0:query 1:response 239 | assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS 240 | assert_equal(0, aa) # Authoritative Answer 241 | assert_equal(0, tc) # TrunCation 242 | assert_equal(1, rd) # Recursion Desired 243 | assert_equal(0, ra) # Recursion Available 244 | assert_equal(0, z) # Reserved for future use 245 | assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused 246 | assert_equal(1, qdcount) # number of entries in the question section. 247 | assert_equal(0, ancount) # number of entries in the answer section. 248 | assert_equal(0, nscount) # number of entries in the authority records section. 249 | assert_equal(0, arcount) # number of entries in the additional records section. 250 | name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C") 251 | assert_operator(rest, :start_with?, name) 252 | rest = rest[name.length..-1] 253 | assert_equal(4, rest.length) 254 | qtype, _ = rest.unpack("nn") 255 | assert_equal(1, qtype) # A 256 | assert_equal(1, qtype) # IN 257 | id = id 258 | qr = 1 259 | opcode = opcode 260 | aa = 0 261 | tc = 1 262 | rd = rd 263 | ra = 1 264 | z = 0 265 | rcode = 0 266 | qdcount = 0 267 | ancount = num_records 268 | nscount = 0 269 | arcount = 0 270 | word2 = (qr << 15) | 271 | (opcode << 11) | 272 | (aa << 10) | 273 | (tc << 9) | 274 | (rd << 8) | 275 | (ra << 7) | 276 | (z << 4) | 277 | rcode 278 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 279 | type = 1 280 | klass = 1 281 | ttl = 3600 282 | rdlength = 4 283 | num_records.times do |i| 284 | rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330 285 | rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*") 286 | msg << rr 287 | end 288 | u.send(msg[0...512], 0, client_address, client_port) 289 | } 290 | tcp_server_thread = Thread.new { 291 | ct = t.accept 292 | msg = ct.recv(512) 293 | msg.slice!(0..1) # Size (only for TCP) 294 | id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 295 | qr = (word2 & 0x8000) >> 15 296 | opcode = (word2 & 0x7800) >> 11 297 | aa = (word2 & 0x0400) >> 10 298 | tc = (word2 & 0x0200) >> 9 299 | rd = (word2 & 0x0100) >> 8 300 | ra = (word2 & 0x0080) >> 7 301 | z = (word2 & 0x0070) >> 4 302 | rcode = word2 & 0x000f 303 | rest = msg[12..-1] 304 | assert_equal(0, qr) # 0:query 1:response 305 | assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS 306 | assert_equal(0, aa) # Authoritative Answer 307 | assert_equal(0, tc) # TrunCation 308 | assert_equal(1, rd) # Recursion Desired 309 | assert_equal(0, ra) # Recursion Available 310 | assert_equal(0, z) # Reserved for future use 311 | assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused 312 | assert_equal(1, qdcount) # number of entries in the question section. 313 | assert_equal(0, ancount) # number of entries in the answer section. 314 | assert_equal(0, nscount) # number of entries in the authority records section. 315 | assert_equal(0, arcount) # number of entries in the additional records section. 316 | name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C") 317 | assert_operator(rest, :start_with?, name) 318 | rest = rest[name.length..-1] 319 | assert_equal(4, rest.length) 320 | qtype, _ = rest.unpack("nn") 321 | assert_equal(1, qtype) # A 322 | assert_equal(1, qtype) # IN 323 | id = id 324 | qr = 1 325 | opcode = opcode 326 | aa = 0 327 | tc = 0 328 | rd = rd 329 | ra = 1 330 | z = 0 331 | rcode = 0 332 | qdcount = 0 333 | ancount = num_records 334 | nscount = 0 335 | arcount = 0 336 | word2 = (qr << 15) | 337 | (opcode << 11) | 338 | (aa << 10) | 339 | (tc << 9) | 340 | (rd << 8) | 341 | (ra << 7) | 342 | (z << 4) | 343 | rcode 344 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 345 | type = 1 346 | klass = 1 347 | ttl = 3600 348 | rdlength = 4 349 | num_records.times do |i| 350 | rdata = [192,0,2,i].pack("CCCC") # 192.0.2.x (TEST-NET address) RFC 3330 351 | rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*") 352 | msg << rr 353 | end 354 | msg = "#{[msg.bytesize].pack("n")}#{msg}" # Prefix with size 355 | ct.send(msg, 0) 356 | ct.close 357 | } 358 | result, _ = assert_join_threads([client_thread, udp_server_thread, tcp_server_thread]) 359 | assert_instance_of(Array, result) 360 | assert_equal(50, result.length) 361 | result.each_with_index do |rr, i| 362 | assert_instance_of(Resolv::DNS::Resource::IN::A, rr) 363 | assert_instance_of(Resolv::IPv4, rr.address) 364 | assert_equal("192.0.2.#{i}", rr.address.to_s) 365 | assert_equal(3600, rr.ttl) 366 | end 367 | } 368 | end 369 | 370 | def test_query_ipv4_duplicate_responses 371 | begin 372 | OpenSSL 373 | rescue LoadError 374 | omit 'autoload problem. see [ruby-dev:45021][Bug #5786]' 375 | end if defined?(OpenSSL) 376 | 377 | with_udp('127.0.0.1', 0) {|u| 378 | _, server_port, _, server_address = u.addr 379 | begin 380 | client_thread = Thread.new { 381 | Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => ['bad1.com', 'bad2.com', 'good.com'], ndots: 5, use_ipv6: false) {|dns| 382 | dns.getaddress("example") 383 | } 384 | } 385 | server_thread = Thread.new { 386 | 3.times do 387 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} 388 | id, flags, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 389 | 390 | qr = (flags & 0x8000) >> 15 391 | opcode = (flags & 0x7800) >> 11 392 | aa = (flags & 0x0400) >> 10 393 | tc = (flags & 0x0200) >> 9 394 | rd = (flags & 0x0100) >> 8 395 | ra = (flags & 0x0080) >> 7 396 | z = (flags & 0x0070) >> 4 397 | rcode = flags & 0x000f 398 | _rest = msg[12..-1] 399 | 400 | questions = msg.bytes[12..-1] 401 | labels = [] 402 | idx = 0 403 | while idx < questions.length-5 404 | size = questions[idx] 405 | labels << questions[idx+1..idx+size].pack('c*') 406 | idx += size+1 407 | end 408 | hostname = labels.join('.') 409 | 410 | if hostname == "example.good.com" 411 | id = id 412 | qr = 1 413 | opcode = opcode 414 | aa = 0 415 | tc = 0 416 | rd = rd 417 | ra = 1 418 | z = 0 419 | rcode = 0 420 | qdcount = 1 421 | ancount = 1 422 | nscount = 0 423 | arcount = 0 424 | word2 = (qr << 15) | 425 | (opcode << 11) | 426 | (aa << 10) | 427 | (tc << 9) | 428 | (rd << 8) | 429 | (ra << 7) | 430 | (z << 4) | 431 | rcode 432 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 433 | msg << questions.pack('c*') 434 | type = 1 435 | klass = 1 436 | ttl = 3600 437 | rdlength = 4 438 | rdata = [52,0,2,1].pack("CCCC") 439 | rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*") 440 | msg << rr 441 | rdata = [52,0,2,2].pack("CCCC") 442 | rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*") 443 | msg << rr 444 | 445 | u.send(msg, 0, client_address, client_port) 446 | else 447 | id = id 448 | qr = 1 449 | opcode = opcode 450 | aa = 0 451 | tc = 0 452 | rd = rd 453 | ra = 1 454 | z = 0 455 | rcode = 3 456 | qdcount = 1 457 | ancount = 0 458 | nscount = 0 459 | arcount = 0 460 | word2 = (qr << 15) | 461 | (opcode << 11) | 462 | (aa << 10) | 463 | (tc << 9) | 464 | (rd << 8) | 465 | (ra << 7) | 466 | (z << 4) | 467 | rcode 468 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 469 | msg << questions.pack('c*') 470 | 471 | u.send(msg, 0, client_address, client_port) 472 | u.send(msg, 0, client_address, client_port) 473 | end 474 | end 475 | } 476 | result, _ = assert_join_threads([client_thread, server_thread]) 477 | assert_instance_of(Resolv::IPv4, result) 478 | assert_equal("52.0.2.1", result.to_s) 479 | end 480 | } 481 | end 482 | 483 | def test_query_ipv4_address_timeout 484 | with_udp('127.0.0.1', 0) {|u| 485 | _, port , _, host = u.addr 486 | start = nil 487 | rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 488 | dns.timeouts = 0.1 489 | start = Time.now 490 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 491 | } 492 | t2 = Time.now 493 | diff = t2 - start 494 | assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)" 495 | assert_operator 0.1, :<=, diff 496 | 497 | rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 498 | dns.timeouts = [ 0.1, 0.2 ] 499 | start = Time.now 500 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 501 | } 502 | t2 = Time.now 503 | diff = t2 - start 504 | assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)" 505 | assert_operator 0.3, :<=, diff 506 | } 507 | end 508 | 509 | def test_no_server 510 | omit if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning 511 | u = UDPSocket.new 512 | u.bind("127.0.0.1", 0) 513 | _, port, _, host = u.addr 514 | u.close 515 | # A race condition here. 516 | # Another program may use the port. 517 | # But no way to prevent it. 518 | begin 519 | Timeout.timeout(5) do 520 | Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 521 | assert_equal([], dns.getresources("test-no-server.example.org", Resolv::DNS::Resource::IN::A)) 522 | } 523 | end 524 | rescue Timeout::Error 525 | if RUBY_PLATFORM.match?(/mingw/) 526 | # cannot repo locally 527 | omit 'Timeout Error on MinGW CI' 528 | elsif macos?([26,1]..[]) 529 | omit 'Timeout Error on macOS 26.1+' 530 | else 531 | raise Timeout::Error 532 | end 533 | end 534 | end 535 | 536 | def test_invalid_byte_comment 537 | bug9273 = '[ruby-core:59239] [Bug #9273]' 538 | Tempfile.create('resolv_test_dns_') do |tmpfile| 539 | tmpfile.print("\xff\x00\x40") 540 | tmpfile.close 541 | assert_nothing_raised(ArgumentError, bug9273) do 542 | Resolv::DNS::Config.parse_resolv_conf(tmpfile.path) 543 | end 544 | end 545 | end 546 | 547 | def test_resolv_conf_by_command 548 | Dir.mktmpdir do |dir| 549 | Dir.chdir(dir) do 550 | assert_raise(Errno::ENOENT, Errno::EINVAL) do 551 | Resolv::DNS::Config.parse_resolv_conf("|echo foo") 552 | end 553 | end 554 | end 555 | end 556 | 557 | def test_dots_diffences 558 | name1 = Resolv::DNS::Name.create("example.org") 559 | name2 = Resolv::DNS::Name.create("ex.ampl.eo.rg") 560 | assert_not_equal(name1, name2, "different dots") 561 | end 562 | 563 | def test_case_insensitive_name 564 | bug10550 = '[ruby-core:66498] [Bug #10550]' 565 | lower = Resolv::DNS::Name.create("ruby-lang.org") 566 | upper = Resolv::DNS::Name.create("Ruby-Lang.org") 567 | assert_equal(lower, upper, bug10550) 568 | end 569 | 570 | def test_ipv6_name 571 | addr = Resolv::IPv6.new("\0"*16) 572 | labels = addr.to_name.to_a 573 | expected = (['0'] * 32 + ['ip6', 'arpa']).map {|label| Resolv::DNS::Label::Str.new(label) } 574 | assert_equal(expected, labels) 575 | end 576 | 577 | def test_ipv6_create 578 | ref = '[Bug #11910] [ruby-core:72559]' 579 | assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1'), ref 580 | assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1:127.0.0.1'), ref 581 | end 582 | 583 | def test_ipv6_to_s 584 | test_cases = [ 585 | ["2001::abcd:abcd:abcd", "2001::ABcd:abcd:ABCD"], 586 | ["2001:db8::1", "2001:db8::0:1"], 587 | ["::", "0:0:0:0:0:0:0:0"], 588 | ["2001::", "2001::0"], 589 | ["2001:db8:0:1:1:1:1:1", "2001:db8:0:1:1:1:1:1"], # RFC 5952 Section 4.2.2. 590 | ["2001:db8::1:1:1:1", "2001:db8:0:0:1:1:1:1"], 591 | ["1::1:0:0:0:1", "1:0:0:1:0:0:0:1"], 592 | ["1::1:0:0:1", "1:0:0:0:1:0:0:1"], 593 | ] 594 | 595 | test_cases.each do |expected, ipv6| 596 | assert_equal expected, Resolv::IPv6.create(ipv6).to_s 597 | end 598 | end 599 | 600 | def test_ipv6_should_be_16 601 | ref = '[rubygems:1626]' 602 | 603 | broken_message = 604 | "\0\0\0\0\0\0\0\0\0\0\0\1" \ 605 | "\x03ns2\bdnsimple\x03com\x00" \ 606 | "\x00\x1C\x00\x01\x00\x02OD" \ 607 | "\x00\x10$\x00\xCB\x00 I\x00\x01\x00\x00\x00\x00" 608 | 609 | e = assert_raise_with_message(Resolv::DNS::DecodeError, /IPv6 address must be 16 bytes/, ref) do 610 | Resolv::DNS::Message.decode broken_message 611 | end 612 | assert_kind_of(ArgumentError, e.cause) 613 | end 614 | 615 | def test_too_big_label_address 616 | n = 2000 617 | m = Resolv::DNS::Message::MessageEncoder.new {|msg| 618 | 2.times { 619 | n.times {|i| msg.put_labels(["foo#{i}"]) } 620 | } 621 | } 622 | Resolv::DNS::Message::MessageDecoder.new(m.to_s) {|msg| 623 | 2.times { 624 | n.times {|i| 625 | assert_equal(["foo#{i}"], msg.get_labels.map {|label| label.to_s }) 626 | } 627 | } 628 | } 629 | assert_operator(2**14, :<, m.to_s.length) 630 | end 631 | 632 | def test_too_long_address 633 | too_long_address_message = [0, 0, 1, 0, 0, 0].pack("n*") + "\x01x" * 129 + [0, 0, 0].pack("cnn") 634 | assert_raise_with_message(Resolv::DNS::DecodeError, /name label data exceed 255 octets/) do 635 | Resolv::DNS::Message.decode too_long_address_message 636 | end 637 | end 638 | 639 | def assert_no_fd_leak 640 | socket = assert_throw(self) do |tag| 641 | Resolv::DNS.stub(:bind_random_port, ->(s, *) {throw(tag, s)}) do 642 | yield.getname("8.8.8.8") 643 | end 644 | end 645 | 646 | assert_predicate(socket, :closed?, "file descriptor leaked") 647 | end 648 | 649 | def test_no_fd_leak_connected 650 | assert_no_fd_leak {Resolv::DNS.new(nameserver_port: [['127.0.0.1', 53]])} 651 | end 652 | 653 | def test_no_fd_leak_unconnected 654 | assert_no_fd_leak {Resolv::DNS.new} 655 | end 656 | 657 | def test_each_name 658 | dns = Resolv::DNS.new 659 | def dns.each_resource(name, typeclass) 660 | yield typeclass.new(name) 661 | end 662 | 663 | dns.each_name('127.0.0.1') do |ptr| 664 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 665 | end 666 | dns.each_name(Resolv::IPv4.create('127.0.0.1')) do |ptr| 667 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 668 | end 669 | dns.each_name('::1') do |ptr| 670 | 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) 671 | end 672 | dns.each_name(Resolv::IPv6.create('::1')) do |ptr| 673 | 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) 674 | end 675 | dns.each_name(Resolv::DNS::Name.create('1.0.0.127.in-addr.arpa.')) do |ptr| 676 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 677 | end 678 | assert_raise(Resolv::ResolvError) { dns.each_name('example.com') } 679 | end 680 | 681 | def test_unreachable_server 682 | unreachable_ip = '127.0.0.1' 683 | sock = UDPSocket.new 684 | sock.connect(unreachable_ip, 53) 685 | begin 686 | sock.send('1', 0) 687 | rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH 688 | else 689 | omit('cannot test unreachable server, as IP used is reachable') 690 | end 691 | 692 | config = { 693 | :nameserver => [unreachable_ip], 694 | :search => ['lan'], 695 | :ndots => 1 696 | } 697 | r = Resolv.new([Resolv::DNS.new(config)]) 698 | assert_equal([], r.getaddresses('www.google.com')) 699 | 700 | config[:raise_timeout_errors] = true 701 | r = Resolv.new([Resolv::DNS.new(config)]) 702 | assert_raise(Resolv::ResolvError) { r.getaddresses('www.google.com') } 703 | ensure 704 | sock&.close 705 | end 706 | 707 | def test_multiple_servers_with_timeout_and_truncated_tcp_fallback 708 | begin 709 | OpenSSL 710 | rescue LoadError 711 | skip 'autoload problem. see [ruby-dev:45021][Bug #5786]' 712 | end if defined?(OpenSSL) 713 | 714 | num_records = 50 715 | 716 | with_udp_and_tcp('127.0.0.1', 0) do |u1, t1| 717 | with_udp_and_tcp('127.0.0.1', 0) do |u2,t2| 718 | u2.close # XXX: u2 UDP socket is not used, but using #with_udp_and_tcp to enable Windows EACCES workaround 719 | _, server1_port, _, server1_address = u1.addr 720 | _, server2_port, _, server2_address = t2.addr 721 | 722 | client_thread = Thread.new do 723 | Resolv::DNS.open(nameserver_port: [[server1_address, server1_port], [server2_address, server2_port]]) do |dns| 724 | dns.timeouts = [0.1, 0.2] 725 | dns.getresources('foo.example.org', Resolv::DNS::Resource::IN::A) 726 | end 727 | end 728 | 729 | udp_server1_thread = Thread.new do 730 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) { u1.recvfrom(4096) } 731 | id, word2, _qdcount, _ancount, _nscount, _arcount = msg.unpack('nnnnnn') 732 | opcode = (word2 & 0x7800) >> 11 733 | rd = (word2 & 0x0100) >> 8 734 | name = [3, 'foo', 7, 'example', 3, 'org', 0].pack('Ca*Ca*Ca*C') 735 | qr = 1 736 | aa = 0 737 | tc = 1 738 | ra = 1 739 | z = 0 740 | rcode = 0 741 | qdcount = 0 742 | ancount = num_records 743 | nscount = 0 744 | arcount = 0 745 | word2 = (qr << 15) | 746 | (opcode << 11) | 747 | (aa << 10) | 748 | (tc << 9) | 749 | (rd << 8) | 750 | (ra << 7) | 751 | (z << 4) | 752 | rcode 753 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack('nnnnnn') 754 | type = 1 755 | klass = 1 756 | ttl = 3600 757 | rdlength = 4 758 | num_records.times do |i| 759 | rdata = [192, 0, 2, i].pack('CCCC') # 192.0.2.x (TEST-NET address) RFC 3330 760 | rr = [name, type, klass, ttl, rdlength, rdata].pack('a*nnNna*') 761 | msg << rr 762 | end 763 | u1.send(msg[0...512], 0, client_address, client_port) 764 | end 765 | 766 | tcp_server1_thread = Thread.new do 767 | # Keep this socket open so that the client experiences a timeout 768 | t1.accept 769 | end 770 | 771 | tcp_server2_thread = Thread.new do 772 | ct = t2.accept 773 | msg = ct.recv(512) 774 | msg.slice!(0..1) # Size (only for TCP) 775 | id, word2, _qdcount, _ancount, _nscount, _arcount = msg.unpack('nnnnnn') 776 | rd = (word2 & 0x0100) >> 8 777 | opcode = (word2 & 0x7800) >> 11 778 | name = [3, 'foo', 7, 'example', 3, 'org', 0].pack('Ca*Ca*Ca*C') 779 | qr = 1 780 | aa = 0 781 | tc = 0 782 | ra = 1 783 | z = 0 784 | rcode = 0 785 | qdcount = 0 786 | ancount = num_records 787 | nscount = 0 788 | arcount = 0 789 | word2 = (qr << 15) | 790 | (opcode << 11) | 791 | (aa << 10) | 792 | (tc << 9) | 793 | (rd << 8) | 794 | (ra << 7) | 795 | (z << 4) | 796 | rcode 797 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack('nnnnnn') 798 | type = 1 799 | klass = 1 800 | ttl = 3600 801 | rdlength = 4 802 | num_records.times do |i| 803 | rdata = [192, 0, 2, i].pack('CCCC') # 192.0.2.x (TEST-NET address) RFC 3330 804 | rr = [name, type, klass, ttl, rdlength, rdata].pack('a*nnNna*') 805 | msg << rr 806 | end 807 | msg = "#{[msg.bytesize].pack('n')}#{msg}" # Prefix with size 808 | ct.send(msg, 0) 809 | ct.close 810 | end 811 | result, _, tcp_server1_socket, = assert_join_threads([client_thread, udp_server1_thread, tcp_server1_thread, tcp_server2_thread]) 812 | assert_instance_of(Array, result) 813 | assert_equal(50, result.length) 814 | result.each_with_index do |rr, i| 815 | assert_instance_of(Resolv::DNS::Resource::IN::A, rr) 816 | assert_instance_of(Resolv::IPv4, rr.address) 817 | assert_equal("192.0.2.#{i}", rr.address.to_s) 818 | assert_equal(3600, rr.ttl) 819 | end 820 | ensure 821 | tcp_server1_socket&.close 822 | end 823 | end 824 | end 825 | 826 | def test_tcp_connection_closed_before_length 827 | with_tcp('127.0.0.1', 0) do |t| 828 | _, server_port, _, server_address = t.addr 829 | 830 | server_thread = Thread.new do 831 | ct = t.accept 832 | ct.recv(512) 833 | ct.close 834 | end 835 | 836 | client_thread = Thread.new do 837 | requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) 838 | begin 839 | msg = Resolv::DNS::Message.new 840 | msg.add_question('example.org', Resolv::DNS::Resource::IN::A) 841 | sender = requester.sender(msg, msg) 842 | assert_raise(Resolv::ResolvTimeout) do 843 | requester.request(sender, 2) 844 | end 845 | ensure 846 | requester.close 847 | end 848 | end 849 | 850 | server_thread.join 851 | client_thread.join 852 | end 853 | end 854 | 855 | def test_tcp_connection_closed_after_length 856 | with_tcp('127.0.0.1', 0) do |t| 857 | _, server_port, _, server_address = t.addr 858 | 859 | server_thread = Thread.new do 860 | ct = t.accept 861 | ct.recv(512) 862 | ct.send([100].pack('n'), 0) 863 | ct.close 864 | end 865 | 866 | client_thread = Thread.new do 867 | requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) 868 | begin 869 | msg = Resolv::DNS::Message.new 870 | msg.add_question('example.org', Resolv::DNS::Resource::IN::A) 871 | sender = requester.sender(msg, msg) 872 | assert_raise(Resolv::ResolvTimeout) do 873 | requester.request(sender, 2) 874 | end 875 | ensure 876 | requester.close 877 | end 878 | end 879 | 880 | server_thread.join 881 | client_thread.join 882 | end 883 | end 884 | 885 | def test_tcp_connection_closed_with_partial_length_prefix 886 | with_tcp('127.0.0.1', 0) do |t| 887 | _, server_port, _, server_address = t.addr 888 | 889 | server_thread = Thread.new do 890 | ct = t.accept 891 | ct.recv(512) 892 | ct.write "A" # 1 byte 893 | ct.close 894 | end 895 | 896 | client_thread = Thread.new do 897 | requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) 898 | begin 899 | msg = Resolv::DNS::Message.new 900 | msg.add_question('example.org', Resolv::DNS::Resource::IN::A) 901 | sender = requester.sender(msg, msg) 902 | assert_raise(Resolv::ResolvTimeout) do 903 | requester.request(sender, 2) 904 | end 905 | ensure 906 | requester.close 907 | end 908 | end 909 | 910 | server_thread.join 911 | client_thread.join 912 | end 913 | end 914 | 915 | def test_tcp_connection_closed_with_partial_message_body 916 | with_tcp('127.0.0.1', 0) do |t| 917 | _, server_port, _, server_address = t.addr 918 | 919 | server_thread = Thread.new do 920 | ct = t.accept 921 | ct.recv(512) 922 | ct.write([10].pack('n')) # length 10 923 | ct.write "12345" # 5 bytes (partial) 924 | ct.close 925 | end 926 | 927 | client_thread = Thread.new do 928 | requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) 929 | begin 930 | msg = Resolv::DNS::Message.new 931 | msg.add_question('example.org', Resolv::DNS::Resource::IN::A) 932 | sender = requester.sender(msg, msg) 933 | assert_raise(Resolv::ResolvTimeout) do 934 | requester.request(sender, 2) 935 | end 936 | ensure 937 | requester.close 938 | end 939 | end 940 | 941 | server_thread.join 942 | client_thread.join 943 | end 944 | end 945 | end 946 | -------------------------------------------------------------------------------- /lib/resolv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'socket' 4 | require 'timeout' 5 | require 'io/wait' 6 | require 'securerandom' 7 | require 'rbconfig' 8 | 9 | # Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can 10 | # handle multiple DNS requests concurrently without blocking the entire Ruby 11 | # interpreter. 12 | # 13 | # See also resolv-replace.rb to replace the libc resolver with Resolv. 14 | # 15 | # Resolv can look up various DNS resources using the DNS module directly. 16 | # 17 | # Examples: 18 | # 19 | # p Resolv.getaddress "www.ruby-lang.org" 20 | # p Resolv.getname "210.251.121.214" 21 | # 22 | # Resolv::DNS.open do |dns| 23 | # ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A 24 | # p ress.map(&:address) 25 | # ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX 26 | # p ress.map { |r| [r.exchange.to_s, r.preference] } 27 | # end 28 | # 29 | # 30 | # == Bugs 31 | # 32 | # * NIS is not supported. 33 | # * /etc/nsswitch.conf is not supported. 34 | 35 | class Resolv 36 | 37 | # The version string 38 | VERSION = "0.7.0" 39 | 40 | ## 41 | # Looks up the first IP address for +name+. 42 | 43 | def self.getaddress(name) 44 | DefaultResolver.getaddress(name) 45 | end 46 | 47 | ## 48 | # Looks up all IP address for +name+. 49 | 50 | def self.getaddresses(name) 51 | DefaultResolver.getaddresses(name) 52 | end 53 | 54 | ## 55 | # Iterates over all IP addresses for +name+. 56 | 57 | def self.each_address(name, &block) 58 | DefaultResolver.each_address(name, &block) 59 | end 60 | 61 | ## 62 | # Looks up the hostname of +address+. 63 | 64 | def self.getname(address) 65 | DefaultResolver.getname(address) 66 | end 67 | 68 | ## 69 | # Looks up all hostnames for +address+. 70 | 71 | def self.getnames(address) 72 | DefaultResolver.getnames(address) 73 | end 74 | 75 | ## 76 | # Iterates over all hostnames for +address+. 77 | 78 | def self.each_name(address, &proc) 79 | DefaultResolver.each_name(address, &proc) 80 | end 81 | 82 | ## 83 | # Creates a new Resolv using +resolvers+. 84 | # 85 | # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and 86 | # and a DNS resolver. If +resolvers+ is a hash, uses the hash as 87 | # configuration for the DNS resolver. 88 | 89 | def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil)) 90 | if !keyword_not_set && !arg_not_set 91 | warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1 92 | end 93 | 94 | @resolvers = case resolvers 95 | when Hash, nil 96 | [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))] 97 | else 98 | resolvers 99 | end 100 | end 101 | 102 | ## 103 | # Looks up the first IP address for +name+. 104 | 105 | def getaddress(name) 106 | each_address(name) {|address| return address} 107 | raise ResolvError.new("no address for #{name}") 108 | end 109 | 110 | ## 111 | # Looks up all IP address for +name+. 112 | 113 | def getaddresses(name) 114 | ret = [] 115 | each_address(name) {|address| ret << address} 116 | return ret 117 | end 118 | 119 | ## 120 | # Iterates over all IP addresses for +name+. 121 | 122 | def each_address(name) 123 | if AddressRegex =~ name 124 | yield name 125 | return 126 | end 127 | yielded = false 128 | @resolvers.each {|r| 129 | r.each_address(name) {|address| 130 | yield address.to_s 131 | yielded = true 132 | } 133 | return if yielded 134 | } 135 | end 136 | 137 | ## 138 | # Looks up the hostname of +address+. 139 | 140 | def getname(address) 141 | each_name(address) {|name| return name} 142 | raise ResolvError.new("no name for #{address}") 143 | end 144 | 145 | ## 146 | # Looks up all hostnames for +address+. 147 | 148 | def getnames(address) 149 | ret = [] 150 | each_name(address) {|name| ret << name} 151 | return ret 152 | end 153 | 154 | ## 155 | # Iterates over all hostnames for +address+. 156 | 157 | def each_name(address) 158 | yielded = false 159 | @resolvers.each {|r| 160 | r.each_name(address) {|name| 161 | yield name.to_s 162 | yielded = true 163 | } 164 | return if yielded 165 | } 166 | end 167 | 168 | ## 169 | # Indicates a failure to resolve a name or address. 170 | 171 | class ResolvError < StandardError; end 172 | 173 | ## 174 | # Indicates a timeout resolving a name or address. 175 | 176 | class ResolvTimeout < Timeout::Error; end 177 | 178 | ## 179 | # Resolv::Hosts is a hostname resolver that uses the system hosts file. 180 | 181 | class Hosts 182 | if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ 183 | begin 184 | require 'win32/resolv' unless defined?(Win32::Resolv) 185 | hosts = Win32::Resolv.get_hosts_path || IO::NULL 186 | rescue LoadError 187 | end 188 | end 189 | # The default file name for host names 190 | DefaultFileName = hosts || '/etc/hosts' 191 | 192 | ## 193 | # Creates a new Resolv::Hosts, using +filename+ for its data source. 194 | 195 | def initialize(filename = DefaultFileName) 196 | @filename = filename 197 | @mutex = Thread::Mutex.new 198 | @initialized = nil 199 | end 200 | 201 | def lazy_initialize # :nodoc: 202 | @mutex.synchronize { 203 | unless @initialized 204 | @name2addr = {} 205 | @addr2name = {} 206 | File.open(@filename, 'rb') {|f| 207 | f.each {|line| 208 | line.sub!(/#.*/, '') 209 | addr, *hostnames = line.split(/\s+/) 210 | next unless addr 211 | (@addr2name[addr] ||= []).concat(hostnames) 212 | hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr} 213 | } 214 | } 215 | @name2addr.each {|name, arr| arr.reverse!} 216 | @initialized = true 217 | end 218 | } 219 | self 220 | end 221 | 222 | ## 223 | # Gets the IP address of +name+ from the hosts file. 224 | 225 | def getaddress(name) 226 | each_address(name) {|address| return address} 227 | raise ResolvError.new("#{@filename} has no name: #{name}") 228 | end 229 | 230 | ## 231 | # Gets all IP addresses for +name+ from the hosts file. 232 | 233 | def getaddresses(name) 234 | ret = [] 235 | each_address(name) {|address| ret << address} 236 | return ret 237 | end 238 | 239 | ## 240 | # Iterates over all IP addresses for +name+ retrieved from the hosts file. 241 | 242 | def each_address(name, &proc) 243 | lazy_initialize 244 | @name2addr[name]&.each(&proc) 245 | end 246 | 247 | ## 248 | # Gets the hostname of +address+ from the hosts file. 249 | 250 | def getname(address) 251 | each_name(address) {|name| return name} 252 | raise ResolvError.new("#{@filename} has no address: #{address}") 253 | end 254 | 255 | ## 256 | # Gets all hostnames for +address+ from the hosts file. 257 | 258 | def getnames(address) 259 | ret = [] 260 | each_name(address) {|name| ret << name} 261 | return ret 262 | end 263 | 264 | ## 265 | # Iterates over all hostnames for +address+ retrieved from the hosts file. 266 | 267 | def each_name(address, &proc) 268 | lazy_initialize 269 | @addr2name[address]&.each(&proc) 270 | end 271 | end 272 | 273 | ## 274 | # Resolv::DNS is a DNS stub resolver. 275 | # 276 | # Information taken from the following places: 277 | # 278 | # * STD0013 279 | # * RFC 1035 280 | # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters 281 | # * etc. 282 | 283 | class DNS 284 | 285 | ## 286 | # Default DNS Port 287 | 288 | Port = 53 289 | 290 | ## 291 | # Default DNS UDP packet size 292 | 293 | UDPSize = 512 294 | 295 | ## 296 | # Creates a new DNS resolver. See Resolv::DNS.new for argument details. 297 | # 298 | # Yields the created DNS resolver to the block, if given, otherwise 299 | # returns it. 300 | 301 | def self.open(*args) 302 | dns = new(*args) 303 | return dns unless block_given? 304 | begin 305 | yield dns 306 | ensure 307 | dns.close 308 | end 309 | end 310 | 311 | ## 312 | # Creates a new DNS resolver. 313 | # 314 | # +config_info+ can be: 315 | # 316 | # nil:: Uses /etc/resolv.conf. 317 | # String:: Path to a file using /etc/resolv.conf's format. 318 | # Hash:: Must contain :nameserver, :search and :ndots keys. 319 | # :nameserver_port can be used to specify port number of nameserver address. 320 | # :raise_timeout_errors can be used to raise timeout errors 321 | # as exceptions instead of treating the same as an NXDOMAIN response. 322 | # 323 | # The value of :nameserver should be an address string or 324 | # an array of address strings. 325 | # - :nameserver => '8.8.8.8' 326 | # - :nameserver => ['8.8.8.8', '8.8.4.4'] 327 | # 328 | # The value of :nameserver_port should be an array of 329 | # pair of nameserver address and port number. 330 | # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] 331 | # 332 | # Example: 333 | # 334 | # Resolv::DNS.new(:nameserver => ['210.251.121.21'], 335 | # :search => ['ruby-lang.org'], 336 | # :ndots => 1) 337 | 338 | def initialize(config_info=nil) 339 | @mutex = Thread::Mutex.new 340 | @config = Config.new(config_info) 341 | @initialized = nil 342 | end 343 | 344 | # Sets the resolver timeouts. This may be a single positive number 345 | # or an array of positive numbers representing timeouts in seconds. 346 | # If an array is specified, a DNS request will retry and wait for 347 | # each successive interval in the array until a successful response 348 | # is received. Specifying +nil+ reverts to the default timeouts: 349 | # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] 350 | # 351 | # Example: 352 | # 353 | # dns.timeouts = 3 354 | # 355 | def timeouts=(values) 356 | @config.timeouts = values 357 | end 358 | 359 | def lazy_initialize # :nodoc: 360 | @mutex.synchronize { 361 | unless @initialized 362 | @config.lazy_initialize 363 | @initialized = true 364 | end 365 | } 366 | self 367 | end 368 | 369 | ## 370 | # Closes the DNS resolver. 371 | 372 | def close 373 | @mutex.synchronize { 374 | if @initialized 375 | @initialized = false 376 | end 377 | } 378 | end 379 | 380 | ## 381 | # Gets the IP address of +name+ from the DNS resolver. 382 | # 383 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will 384 | # be a Resolv::IPv4 or Resolv::IPv6 385 | 386 | def getaddress(name) 387 | each_address(name) {|address| return address} 388 | raise ResolvError.new("DNS result has no information for #{name}") 389 | end 390 | 391 | ## 392 | # Gets all IP addresses for +name+ from the DNS resolver. 393 | # 394 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 395 | # be a Resolv::IPv4 or Resolv::IPv6 396 | 397 | def getaddresses(name) 398 | ret = [] 399 | each_address(name) {|address| ret << address} 400 | return ret 401 | end 402 | 403 | ## 404 | # Iterates over all IP addresses for +name+ retrieved from the DNS 405 | # resolver. 406 | # 407 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 408 | # be a Resolv::IPv4 or Resolv::IPv6 409 | 410 | def each_address(name) 411 | if use_ipv6? 412 | each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} 413 | end 414 | each_resource(name, Resource::IN::A) {|resource| yield resource.address} 415 | end 416 | 417 | def use_ipv6? # :nodoc: 418 | @config.lazy_initialize unless @config.instance_variable_get(:@initialized) 419 | 420 | use_ipv6 = @config.use_ipv6? 421 | unless use_ipv6.nil? 422 | return use_ipv6 423 | end 424 | 425 | begin 426 | list = Socket.ip_address_list 427 | rescue NotImplementedError 428 | return true 429 | end 430 | list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } 431 | end 432 | private :use_ipv6? 433 | 434 | ## 435 | # Gets the hostname for +address+ from the DNS resolver. 436 | # 437 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 438 | # name will be a Resolv::DNS::Name. 439 | 440 | def getname(address) 441 | each_name(address) {|name| return name} 442 | raise ResolvError.new("DNS result has no information for #{address}") 443 | end 444 | 445 | ## 446 | # Gets all hostnames for +address+ from the DNS resolver. 447 | # 448 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 449 | # names will be Resolv::DNS::Name instances. 450 | 451 | def getnames(address) 452 | ret = [] 453 | each_name(address) {|name| ret << name} 454 | return ret 455 | end 456 | 457 | ## 458 | # Iterates over all hostnames for +address+ retrieved from the DNS 459 | # resolver. 460 | # 461 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 462 | # names will be Resolv::DNS::Name instances. 463 | 464 | def each_name(address) 465 | case address 466 | when Name 467 | ptr = address 468 | when IPv4, IPv6 469 | ptr = address.to_name 470 | when IPv4::Regex 471 | ptr = IPv4.create(address).to_name 472 | when IPv6::Regex 473 | ptr = IPv6.create(address).to_name 474 | else 475 | raise ResolvError.new("cannot interpret as address: #{address}") 476 | end 477 | each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name} 478 | end 479 | 480 | ## 481 | # Look up the +typeclass+ DNS resource of +name+. 482 | # 483 | # +name+ must be a Resolv::DNS::Name or a String. 484 | # 485 | # +typeclass+ should be one of the following: 486 | # 487 | # * Resolv::DNS::Resource::IN::A 488 | # * Resolv::DNS::Resource::IN::AAAA 489 | # * Resolv::DNS::Resource::IN::ANY 490 | # * Resolv::DNS::Resource::IN::CNAME 491 | # * Resolv::DNS::Resource::IN::HINFO 492 | # * Resolv::DNS::Resource::IN::MINFO 493 | # * Resolv::DNS::Resource::IN::MX 494 | # * Resolv::DNS::Resource::IN::NS 495 | # * Resolv::DNS::Resource::IN::PTR 496 | # * Resolv::DNS::Resource::IN::SOA 497 | # * Resolv::DNS::Resource::IN::TXT 498 | # * Resolv::DNS::Resource::IN::WKS 499 | # 500 | # Returned resource is represented as a Resolv::DNS::Resource instance, 501 | # i.e. Resolv::DNS::Resource::IN::A. 502 | 503 | def getresource(name, typeclass) 504 | each_resource(name, typeclass) {|resource| return resource} 505 | raise ResolvError.new("DNS result has no information for #{name}") 506 | end 507 | 508 | ## 509 | # Looks up all +typeclass+ DNS resources for +name+. See #getresource for 510 | # argument details. 511 | 512 | def getresources(name, typeclass) 513 | ret = [] 514 | each_resource(name, typeclass) {|resource| ret << resource} 515 | return ret 516 | end 517 | 518 | ## 519 | # Iterates over all +typeclass+ DNS resources for +name+. See 520 | # #getresource for argument details. 521 | 522 | def each_resource(name, typeclass, &proc) 523 | fetch_resource(name, typeclass) {|reply, reply_name| 524 | extract_resources(reply, reply_name, typeclass, &proc) 525 | } 526 | end 527 | 528 | # :stopdoc: 529 | 530 | def fetch_resource(name, typeclass) 531 | lazy_initialize 532 | truncated = {} 533 | requesters = {} 534 | udp_requester = begin 535 | make_udp_requester 536 | rescue Errno::EACCES 537 | # fall back to TCP 538 | end 539 | senders = {} 540 | 541 | begin 542 | @config.resolv(name) do |candidate, tout, nameserver, port| 543 | msg = Message.new 544 | msg.rd = 1 545 | msg.add_question(candidate, typeclass) 546 | 547 | requester = requesters.fetch([nameserver, port]) do 548 | if !truncated[candidate] && udp_requester 549 | udp_requester 550 | else 551 | requesters[[nameserver, port]] = make_tcp_requester(nameserver, port) 552 | end 553 | end 554 | 555 | unless sender = senders[[candidate, requester, nameserver, port]] 556 | sender = requester.sender(msg, candidate, nameserver, port) 557 | next if !sender 558 | senders[[candidate, requester, nameserver, port]] = sender 559 | end 560 | reply, reply_name = requester.request(sender, tout) 561 | case reply.rcode 562 | when RCode::NoError 563 | if reply.tc == 1 and not Requester::TCP === requester 564 | # Retry via TCP: 565 | truncated[candidate] = true 566 | redo 567 | else 568 | yield(reply, reply_name) 569 | end 570 | return 571 | when RCode::NXDomain 572 | raise Config::NXDomain.new(reply_name.to_s) 573 | else 574 | raise Config::OtherResolvError.new(reply_name.to_s) 575 | end 576 | end 577 | ensure 578 | udp_requester&.close 579 | requesters.each_value { |requester| requester&.close } 580 | end 581 | end 582 | 583 | def make_udp_requester # :nodoc: 584 | nameserver_port = @config.nameserver_port 585 | if nameserver_port.length == 1 586 | Requester::ConnectedUDP.new(*nameserver_port[0]) 587 | else 588 | Requester::UnconnectedUDP.new(*nameserver_port) 589 | end 590 | end 591 | 592 | def make_tcp_requester(host, port) # :nodoc: 593 | return Requester::TCP.new(host, port) 594 | rescue Errno::ECONNREFUSED 595 | # Treat a refused TCP connection attempt to a nameserver like a timeout, 596 | # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a 597 | # hint to try the next nameserver: 598 | raise ResolvTimeout 599 | end 600 | 601 | def extract_resources(msg, name, typeclass) # :nodoc: 602 | if typeclass < Resource::ANY 603 | n0 = Name.create(name) 604 | msg.each_resource {|n, ttl, data| 605 | yield data if n0 == n 606 | } 607 | end 608 | yielded = false 609 | n0 = Name.create(name) 610 | msg.each_resource {|n, ttl, data| 611 | if n0 == n 612 | case data 613 | when typeclass 614 | yield data 615 | yielded = true 616 | when Resource::CNAME 617 | n0 = data.name 618 | end 619 | end 620 | } 621 | return if yielded 622 | msg.each_resource {|n, ttl, data| 623 | if n0 == n 624 | case data 625 | when typeclass 626 | yield data 627 | end 628 | end 629 | } 630 | end 631 | 632 | def self.random(arg) # :nodoc: 633 | begin 634 | SecureRandom.random_number(arg) 635 | rescue NotImplementedError 636 | rand(arg) 637 | end 638 | end 639 | 640 | RequestID = {} # :nodoc: 641 | RequestIDMutex = Thread::Mutex.new # :nodoc: 642 | 643 | def self.allocate_request_id(host, port) # :nodoc: 644 | id = nil 645 | RequestIDMutex.synchronize { 646 | h = (RequestID[[host, port]] ||= {}) 647 | begin 648 | id = random(0x0000..0xffff) 649 | end while h[id] 650 | h[id] = true 651 | } 652 | id 653 | end 654 | 655 | def self.free_request_id(host, port, id) # :nodoc: 656 | RequestIDMutex.synchronize { 657 | key = [host, port] 658 | if h = RequestID[key] 659 | h.delete id 660 | if h.empty? 661 | RequestID.delete key 662 | end 663 | end 664 | } 665 | end 666 | 667 | case RUBY_PLATFORM 668 | when *[ 669 | # https://www.rfc-editor.org/rfc/rfc6056.txt 670 | # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations 671 | /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/, 672 | /darwin/, # the same as FreeBSD 673 | ] then 674 | def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: 675 | udpsock.bind(bind_host, 0) 676 | end 677 | else 678 | # Sequential port assignment 679 | def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: 680 | # Ephemeral port number range recommended by RFC 6056 681 | port = random(1024..65535) 682 | udpsock.bind(bind_host, port) 683 | rescue Errno::EADDRINUSE, # POSIX 684 | Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5) 685 | Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4). 686 | retry 687 | end 688 | end 689 | 690 | class Requester # :nodoc: 691 | def initialize 692 | @senders = {} 693 | @socks = nil 694 | end 695 | 696 | def request(sender, tout) 697 | start = Process.clock_gettime(Process::CLOCK_MONOTONIC) 698 | timelimit = start + tout 699 | begin 700 | sender.send 701 | rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this 702 | Errno::ENETUNREACH 703 | raise ResolvTimeout 704 | end 705 | while true 706 | before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) 707 | timeout = timelimit - before_select 708 | if timeout <= 0 709 | raise ResolvTimeout 710 | end 711 | if @socks.size == 1 712 | select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil 713 | else 714 | select_result = IO.select(@socks, nil, nil, timeout) 715 | end 716 | if !select_result 717 | after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) 718 | next if after_select < timelimit 719 | raise ResolvTimeout 720 | end 721 | begin 722 | reply, from = recv_reply(select_result[0]) 723 | rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD 724 | Errno::ECONNRESET, # Windows 725 | EOFError 726 | # No name server running on the server? 727 | # Don't wait anymore. 728 | raise ResolvTimeout 729 | end 730 | begin 731 | msg = Message.decode(reply) 732 | rescue DecodeError 733 | next # broken DNS message ignored 734 | end 735 | if sender == sender_for(from, msg) 736 | break 737 | else 738 | # unexpected DNS message ignored 739 | end 740 | end 741 | return msg, sender.data 742 | end 743 | 744 | def sender_for(addr, msg) 745 | @senders[[addr,msg.id]] 746 | end 747 | 748 | def close 749 | socks = @socks 750 | @socks = nil 751 | socks&.each(&:close) 752 | end 753 | 754 | class Sender # :nodoc: 755 | def initialize(msg, data, sock) 756 | @msg = msg 757 | @data = data 758 | @sock = sock 759 | end 760 | end 761 | 762 | class UnconnectedUDP < Requester # :nodoc: 763 | def initialize(*nameserver_port) 764 | super() 765 | @nameserver_port = nameserver_port 766 | @initialized = false 767 | @mutex = Thread::Mutex.new 768 | end 769 | 770 | def lazy_initialize 771 | @mutex.synchronize { 772 | next if @initialized 773 | @initialized = true 774 | @socks_hash = {} 775 | @socks = [] 776 | @nameserver_port.each {|host, port| 777 | if host.index(':') 778 | bind_host = "::" 779 | af = Socket::AF_INET6 780 | else 781 | bind_host = "0.0.0.0" 782 | af = Socket::AF_INET 783 | end 784 | next if @socks_hash[bind_host] 785 | begin 786 | sock = UDPSocket.new(af) 787 | rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT 788 | next # The kernel doesn't support the address family. 789 | end 790 | @socks << sock 791 | @socks_hash[bind_host] = sock 792 | sock.do_not_reverse_lookup = true 793 | DNS.bind_random_port(sock, bind_host) 794 | } 795 | } 796 | self 797 | end 798 | 799 | def recv_reply(readable_socks) 800 | lazy_initialize 801 | reply, from = readable_socks[0].recvfrom(UDPSize) 802 | return reply, [from[3],from[1]] 803 | end 804 | 805 | def sender(msg, data, host, port=Port) 806 | host = Addrinfo.ip(host).ip_address 807 | lazy_initialize 808 | sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] 809 | return nil if !sock 810 | service = [host, port] 811 | id = DNS.allocate_request_id(host, port) 812 | request = msg.encode 813 | request[0,2] = [id].pack('n') 814 | return @senders[[service, id]] = 815 | Sender.new(request, data, sock, host, port) 816 | end 817 | 818 | def close 819 | @mutex.synchronize { 820 | if @initialized 821 | super 822 | @senders.each_key {|service, id| 823 | DNS.free_request_id(service[0], service[1], id) 824 | } 825 | @initialized = false 826 | end 827 | } 828 | end 829 | 830 | class Sender < Requester::Sender # :nodoc: 831 | def initialize(msg, data, sock, host, port) 832 | super(msg, data, sock) 833 | @host = host 834 | @port = port 835 | end 836 | attr_reader :data 837 | 838 | def send 839 | raise "@sock is nil." if @sock.nil? 840 | @sock.send(@msg, 0, @host, @port) 841 | end 842 | end 843 | end 844 | 845 | class ConnectedUDP < Requester # :nodoc: 846 | def initialize(host, port=Port) 847 | super() 848 | @host = host 849 | @port = port 850 | @mutex = Thread::Mutex.new 851 | @initialized = false 852 | end 853 | 854 | def lazy_initialize 855 | @mutex.synchronize { 856 | next if @initialized 857 | @initialized = true 858 | is_ipv6 = @host.index(':') 859 | sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) 860 | @socks = [sock] 861 | sock.do_not_reverse_lookup = true 862 | DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") 863 | sock.connect(@host, @port) 864 | } 865 | self 866 | end 867 | 868 | def recv_reply(readable_socks) 869 | lazy_initialize 870 | reply = readable_socks[0].recv(UDPSize) 871 | return reply, nil 872 | end 873 | 874 | def sender(msg, data, host=@host, port=@port) 875 | lazy_initialize 876 | unless host == @host && port == @port 877 | raise RequestError.new("host/port don't match: #{host}:#{port}") 878 | end 879 | id = DNS.allocate_request_id(@host, @port) 880 | request = msg.encode 881 | request[0,2] = [id].pack('n') 882 | return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) 883 | end 884 | 885 | def close 886 | @mutex.synchronize do 887 | if @initialized 888 | super 889 | @senders.each_key {|from, id| 890 | DNS.free_request_id(@host, @port, id) 891 | } 892 | @initialized = false 893 | end 894 | end 895 | end 896 | 897 | class Sender < Requester::Sender # :nodoc: 898 | def send 899 | raise "@sock is nil." if @sock.nil? 900 | @sock.send(@msg, 0) 901 | end 902 | attr_reader :data 903 | end 904 | end 905 | 906 | class MDNSOneShot < UnconnectedUDP # :nodoc: 907 | def sender(msg, data, host, port=Port) 908 | lazy_initialize 909 | id = DNS.allocate_request_id(host, port) 910 | request = msg.encode 911 | request[0,2] = [id].pack('n') 912 | sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] 913 | return @senders[id] = 914 | UnconnectedUDP::Sender.new(request, data, sock, host, port) 915 | end 916 | 917 | def sender_for(addr, msg) 918 | lazy_initialize 919 | @senders[msg.id] 920 | end 921 | end 922 | 923 | class TCP < Requester # :nodoc: 924 | def initialize(host, port=Port) 925 | super() 926 | @host = host 927 | @port = port 928 | sock = TCPSocket.new(@host, @port) 929 | @socks = [sock] 930 | @senders = {} 931 | end 932 | 933 | def recv_reply(readable_socks) 934 | len_data = readable_socks[0].read(2) 935 | raise EOFError if len_data.nil? || len_data.bytesize != 2 936 | len = len_data.unpack('n')[0] 937 | reply = @socks[0].read(len) 938 | raise EOFError if reply.nil? || reply.bytesize != len 939 | return reply, nil 940 | end 941 | 942 | def sender(msg, data, host=@host, port=@port) 943 | unless host == @host && port == @port 944 | raise RequestError.new("host/port don't match: #{host}:#{port}") 945 | end 946 | id = DNS.allocate_request_id(@host, @port) 947 | request = msg.encode 948 | request[0,2] = [request.length, id].pack('nn') 949 | return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) 950 | end 951 | 952 | class Sender < Requester::Sender # :nodoc: 953 | def send 954 | @sock.print(@msg) 955 | @sock.flush 956 | end 957 | attr_reader :data 958 | end 959 | 960 | def close 961 | super 962 | @senders.each_key {|from,id| 963 | DNS.free_request_id(@host, @port, id) 964 | } 965 | end 966 | end 967 | 968 | ## 969 | # Indicates a problem with the DNS request. 970 | 971 | class RequestError < StandardError 972 | end 973 | end 974 | 975 | class Config # :nodoc: 976 | def initialize(config_info=nil) 977 | @mutex = Thread::Mutex.new 978 | @config_info = config_info 979 | @initialized = nil 980 | @timeouts = nil 981 | end 982 | 983 | def timeouts=(values) 984 | if values 985 | values = Array(values) 986 | values.each do |t| 987 | Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric" 988 | t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive" 989 | end 990 | @timeouts = values 991 | else 992 | @timeouts = nil 993 | end 994 | end 995 | 996 | def Config.parse_resolv_conf(filename) 997 | nameserver = [] 998 | search = nil 999 | ndots = 1 1000 | File.open(filename, 'rb') {|f| 1001 | f.each {|line| 1002 | line.sub!(/[#;].*/, '') 1003 | keyword, *args = line.split(/\s+/) 1004 | next unless keyword 1005 | case keyword 1006 | when 'nameserver' 1007 | nameserver.concat(args.each(&:freeze)) 1008 | when 'domain' 1009 | next if args.empty? 1010 | search = [args[0].freeze] 1011 | when 'search' 1012 | next if args.empty? 1013 | search = args.each(&:freeze) 1014 | when 'options' 1015 | args.each {|arg| 1016 | case arg 1017 | when /\Andots:(\d+)\z/ 1018 | ndots = $1.to_i 1019 | end 1020 | } 1021 | end 1022 | } 1023 | } 1024 | return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze 1025 | end 1026 | 1027 | def Config.default_config_hash(filename="/etc/resolv.conf") 1028 | if File.exist? filename 1029 | Config.parse_resolv_conf(filename) 1030 | elsif defined?(Win32::Resolv) 1031 | search, nameserver = Win32::Resolv.get_resolv_info 1032 | config_hash = {} 1033 | config_hash[:nameserver] = nameserver if nameserver 1034 | config_hash[:search] = [search].flatten if search 1035 | config_hash 1036 | else 1037 | {} 1038 | end 1039 | end 1040 | 1041 | def lazy_initialize 1042 | @mutex.synchronize { 1043 | unless @initialized 1044 | @nameserver_port = [] 1045 | @use_ipv6 = nil 1046 | @search = nil 1047 | @ndots = 1 1048 | case @config_info 1049 | when nil 1050 | config_hash = Config.default_config_hash 1051 | when String 1052 | config_hash = Config.parse_resolv_conf(@config_info) 1053 | when Hash 1054 | config_hash = @config_info.dup 1055 | if String === config_hash[:nameserver] 1056 | config_hash[:nameserver] = [config_hash[:nameserver]] 1057 | end 1058 | if String === config_hash[:search] 1059 | config_hash[:search] = [config_hash[:search]] 1060 | end 1061 | else 1062 | raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") 1063 | end 1064 | if config_hash.include? :nameserver 1065 | @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } 1066 | end 1067 | if config_hash.include? :nameserver_port 1068 | @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } 1069 | end 1070 | if config_hash.include? :use_ipv6 1071 | @use_ipv6 = config_hash[:use_ipv6] 1072 | end 1073 | @search = config_hash[:search] if config_hash.include? :search 1074 | @ndots = config_hash[:ndots] if config_hash.include? :ndots 1075 | @raise_timeout_errors = config_hash[:raise_timeout_errors] 1076 | 1077 | if @nameserver_port.empty? 1078 | @nameserver_port << ['0.0.0.0', Port] 1079 | end 1080 | if @search 1081 | @search = @search.map {|arg| Label.split(arg) } 1082 | else 1083 | hostname = Socket.gethostname 1084 | if /\./ =~ hostname 1085 | @search = [Label.split($')] 1086 | else 1087 | @search = [[]] 1088 | end 1089 | end 1090 | 1091 | if !@nameserver_port.kind_of?(Array) || 1092 | @nameserver_port.any? {|ns_port| 1093 | !(Array === ns_port) || 1094 | ns_port.length != 2 1095 | !(String === ns_port[0]) || 1096 | !(Integer === ns_port[1]) 1097 | } 1098 | raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") 1099 | end 1100 | 1101 | if !@search.kind_of?(Array) || 1102 | !@search.all? {|ls| ls.all? {|l| Label::Str === l } } 1103 | raise ArgumentError.new("invalid search config: #{@search.inspect}") 1104 | end 1105 | 1106 | if !@ndots.kind_of?(Integer) 1107 | raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}") 1108 | end 1109 | 1110 | @initialized = true 1111 | end 1112 | } 1113 | self 1114 | end 1115 | 1116 | def single? 1117 | lazy_initialize 1118 | if @nameserver_port.length == 1 1119 | return @nameserver_port[0] 1120 | else 1121 | return nil 1122 | end 1123 | end 1124 | 1125 | def nameserver_port 1126 | @nameserver_port 1127 | end 1128 | 1129 | def use_ipv6? 1130 | @use_ipv6 1131 | end 1132 | 1133 | def generate_candidates(name) 1134 | candidates = nil 1135 | name = Name.create(name) 1136 | if name.absolute? 1137 | candidates = [name] 1138 | else 1139 | if @ndots <= name.length - 1 1140 | candidates = [Name.new(name.to_a)] 1141 | else 1142 | candidates = [] 1143 | end 1144 | candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) 1145 | fname = Name.create("#{name}.") 1146 | if !candidates.include?(fname) 1147 | candidates << fname 1148 | end 1149 | end 1150 | return candidates 1151 | end 1152 | 1153 | InitialTimeout = 5 1154 | 1155 | def generate_timeouts 1156 | ts = [InitialTimeout] 1157 | ts << ts[-1] * 2 / @nameserver_port.length 1158 | ts << ts[-1] * 2 1159 | ts << ts[-1] * 2 1160 | return ts 1161 | end 1162 | 1163 | def resolv(name) 1164 | candidates = generate_candidates(name) 1165 | timeouts = @timeouts || generate_timeouts 1166 | timeout_error = false 1167 | begin 1168 | candidates.each {|candidate| 1169 | begin 1170 | timeouts.each {|tout| 1171 | @nameserver_port.each {|nameserver, port| 1172 | begin 1173 | yield candidate, tout, nameserver, port 1174 | rescue ResolvTimeout 1175 | end 1176 | } 1177 | } 1178 | timeout_error = true 1179 | raise ResolvError.new("DNS resolv timeout: #{name}") 1180 | rescue NXDomain 1181 | end 1182 | } 1183 | rescue ResolvError 1184 | raise if @raise_timeout_errors && timeout_error 1185 | end 1186 | end 1187 | 1188 | ## 1189 | # Indicates no such domain was found. 1190 | 1191 | class NXDomain < ResolvError 1192 | end 1193 | 1194 | ## 1195 | # Indicates some other unhandled resolver error was encountered. 1196 | 1197 | class OtherResolvError < ResolvError 1198 | end 1199 | end 1200 | 1201 | module OpCode # :nodoc: 1202 | Query = 0 1203 | IQuery = 1 1204 | Status = 2 1205 | Notify = 4 1206 | Update = 5 1207 | end 1208 | 1209 | module RCode # :nodoc: 1210 | NoError = 0 1211 | FormErr = 1 1212 | ServFail = 2 1213 | NXDomain = 3 1214 | NotImp = 4 1215 | Refused = 5 1216 | YXDomain = 6 1217 | YXRRSet = 7 1218 | NXRRSet = 8 1219 | NotAuth = 9 1220 | NotZone = 10 1221 | BADVERS = 16 1222 | BADSIG = 16 1223 | BADKEY = 17 1224 | BADTIME = 18 1225 | BADMODE = 19 1226 | BADNAME = 20 1227 | BADALG = 21 1228 | end 1229 | 1230 | ## 1231 | # Indicates that the DNS response was unable to be decoded. 1232 | 1233 | class DecodeError < StandardError 1234 | end 1235 | 1236 | ## 1237 | # Indicates that the DNS request was unable to be encoded. 1238 | 1239 | class EncodeError < StandardError 1240 | end 1241 | 1242 | module Label # :nodoc: 1243 | def self.split(arg) 1244 | labels = [] 1245 | arg.scan(/[^\.]+/) {labels << Str.new($&)} 1246 | return labels 1247 | end 1248 | 1249 | class Str # :nodoc: 1250 | def initialize(string) 1251 | @string = string 1252 | # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] 1253 | # This assumes @string is given in ASCII compatible encoding. 1254 | @downcase = string.b.downcase 1255 | end 1256 | attr_reader :string, :downcase 1257 | 1258 | def to_s 1259 | return @string 1260 | end 1261 | 1262 | def inspect 1263 | return "#<#{self.class} #{self}>" 1264 | end 1265 | 1266 | def ==(other) 1267 | return self.class == other.class && @downcase == other.downcase 1268 | end 1269 | 1270 | def eql?(other) 1271 | return self == other 1272 | end 1273 | 1274 | def hash 1275 | return @downcase.hash 1276 | end 1277 | end 1278 | end 1279 | 1280 | ## 1281 | # A representation of a DNS name. 1282 | 1283 | class Name 1284 | 1285 | ## 1286 | # Creates a new DNS name from +arg+. +arg+ can be: 1287 | # 1288 | # Name:: returns +arg+. 1289 | # String:: Creates a new Name. 1290 | 1291 | def self.create(arg) 1292 | case arg 1293 | when Name 1294 | return arg 1295 | when String 1296 | return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false) 1297 | else 1298 | raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}") 1299 | end 1300 | end 1301 | 1302 | def initialize(labels, absolute=true) # :nodoc: 1303 | labels = labels.map {|label| 1304 | case label 1305 | when String then Label::Str.new(label) 1306 | when Label::Str then label 1307 | else 1308 | raise ArgumentError, "unexpected label: #{label.inspect}" 1309 | end 1310 | } 1311 | @labels = labels 1312 | @absolute = absolute 1313 | end 1314 | 1315 | def inspect # :nodoc: 1316 | "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" 1317 | end 1318 | 1319 | ## 1320 | # True if this name is absolute. 1321 | 1322 | def absolute? 1323 | return @absolute 1324 | end 1325 | 1326 | def ==(other) # :nodoc: 1327 | return false unless Name === other 1328 | return false unless @absolute == other.absolute? 1329 | return @labels == other.to_a 1330 | end 1331 | 1332 | alias eql? == # :nodoc: 1333 | 1334 | ## 1335 | # Returns true if +other+ is a subdomain. 1336 | # 1337 | # Example: 1338 | # 1339 | # domain = Resolv::DNS::Name.create("y.z") 1340 | # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true 1341 | # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true 1342 | # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false 1343 | # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false 1344 | # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false 1345 | # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false 1346 | # 1347 | 1348 | def subdomain_of?(other) 1349 | raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other 1350 | return false if @absolute != other.absolute? 1351 | other_len = other.length 1352 | return false if @labels.length <= other_len 1353 | return @labels[-other_len, other_len] == other.to_a 1354 | end 1355 | 1356 | def hash # :nodoc: 1357 | return @labels.hash ^ @absolute.hash 1358 | end 1359 | 1360 | def to_a # :nodoc: 1361 | return @labels 1362 | end 1363 | 1364 | def length # :nodoc: 1365 | return @labels.length 1366 | end 1367 | 1368 | def [](i) # :nodoc: 1369 | return @labels[i] 1370 | end 1371 | 1372 | ## 1373 | # returns the domain name as a string. 1374 | # 1375 | # The domain name doesn't have a trailing dot even if the name object is 1376 | # absolute. 1377 | # 1378 | # Example: 1379 | # 1380 | # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z" 1381 | # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z" 1382 | 1383 | def to_s 1384 | return @labels.join('.') 1385 | end 1386 | end 1387 | 1388 | class Message # :nodoc: 1389 | @@identifier = -1 1390 | 1391 | def initialize(id = (@@identifier += 1) & 0xffff) 1392 | @id = id 1393 | @qr = 0 1394 | @opcode = 0 1395 | @aa = 0 1396 | @tc = 0 1397 | @rd = 0 # recursion desired 1398 | @ra = 0 # recursion available 1399 | @rcode = 0 1400 | @question = [] 1401 | @answer = [] 1402 | @authority = [] 1403 | @additional = [] 1404 | end 1405 | 1406 | attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode 1407 | attr_reader :question, :answer, :authority, :additional 1408 | 1409 | def ==(other) 1410 | return @id == other.id && 1411 | @qr == other.qr && 1412 | @opcode == other.opcode && 1413 | @aa == other.aa && 1414 | @tc == other.tc && 1415 | @rd == other.rd && 1416 | @ra == other.ra && 1417 | @rcode == other.rcode && 1418 | @question == other.question && 1419 | @answer == other.answer && 1420 | @authority == other.authority && 1421 | @additional == other.additional 1422 | end 1423 | 1424 | def add_question(name, typeclass) 1425 | @question << [Name.create(name), typeclass] 1426 | end 1427 | 1428 | def each_question 1429 | @question.each {|name, typeclass| 1430 | yield name, typeclass 1431 | } 1432 | end 1433 | 1434 | def add_answer(name, ttl, data) 1435 | @answer << [Name.create(name), ttl, data] 1436 | end 1437 | 1438 | def each_answer 1439 | @answer.each {|name, ttl, data| 1440 | yield name, ttl, data 1441 | } 1442 | end 1443 | 1444 | def add_authority(name, ttl, data) 1445 | @authority << [Name.create(name), ttl, data] 1446 | end 1447 | 1448 | def each_authority 1449 | @authority.each {|name, ttl, data| 1450 | yield name, ttl, data 1451 | } 1452 | end 1453 | 1454 | def add_additional(name, ttl, data) 1455 | @additional << [Name.create(name), ttl, data] 1456 | end 1457 | 1458 | def each_additional 1459 | @additional.each {|name, ttl, data| 1460 | yield name, ttl, data 1461 | } 1462 | end 1463 | 1464 | def each_resource 1465 | each_answer {|name, ttl, data| yield name, ttl, data} 1466 | each_authority {|name, ttl, data| yield name, ttl, data} 1467 | each_additional {|name, ttl, data| yield name, ttl, data} 1468 | end 1469 | 1470 | def encode 1471 | return MessageEncoder.new {|msg| 1472 | msg.put_pack('nnnnnn', 1473 | @id, 1474 | (@qr & 1) << 15 | 1475 | (@opcode & 15) << 11 | 1476 | (@aa & 1) << 10 | 1477 | (@tc & 1) << 9 | 1478 | (@rd & 1) << 8 | 1479 | (@ra & 1) << 7 | 1480 | (@rcode & 15), 1481 | @question.length, 1482 | @answer.length, 1483 | @authority.length, 1484 | @additional.length) 1485 | @question.each {|q| 1486 | name, typeclass = q 1487 | msg.put_name(name) 1488 | msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue) 1489 | } 1490 | [@answer, @authority, @additional].each {|rr| 1491 | rr.each {|r| 1492 | name, ttl, data = r 1493 | msg.put_name(name) 1494 | msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl) 1495 | msg.put_length16 {data.encode_rdata(msg)} 1496 | } 1497 | } 1498 | }.to_s 1499 | end 1500 | 1501 | class MessageEncoder # :nodoc: 1502 | def initialize 1503 | @data = ''.dup 1504 | @names = {} 1505 | yield self 1506 | end 1507 | 1508 | def to_s 1509 | return @data 1510 | end 1511 | 1512 | def put_bytes(d) 1513 | @data << d 1514 | end 1515 | 1516 | def put_pack(template, *d) 1517 | @data << d.pack(template) 1518 | end 1519 | 1520 | def put_length16 1521 | length_index = @data.length 1522 | @data << "\0\0" 1523 | data_start = @data.length 1524 | yield 1525 | data_end = @data.length 1526 | @data[length_index, 2] = [data_end - data_start].pack("n") 1527 | end 1528 | 1529 | def put_string(d) 1530 | self.put_pack("C", d.length) 1531 | @data << d 1532 | end 1533 | 1534 | def put_string_list(ds) 1535 | ds.each {|d| 1536 | self.put_string(d) 1537 | } 1538 | end 1539 | 1540 | def put_name(d, compress: true) 1541 | put_labels(d.to_a, compress: compress) 1542 | end 1543 | 1544 | def put_labels(d, compress: true) 1545 | d.each_index {|i| 1546 | domain = d[i..-1] 1547 | if compress && idx = @names[domain] 1548 | self.put_pack("n", 0xc000 | idx) 1549 | return 1550 | else 1551 | if @data.length < 0x4000 1552 | @names[domain] = @data.length 1553 | end 1554 | self.put_label(d[i]) 1555 | end 1556 | } 1557 | @data << "\0" 1558 | end 1559 | 1560 | def put_label(d) 1561 | self.put_string(d.to_s) 1562 | end 1563 | end 1564 | 1565 | def Message.decode(m) 1566 | o = Message.new(0) 1567 | MessageDecoder.new(m) {|msg| 1568 | id, flag, qdcount, ancount, nscount, arcount = 1569 | msg.get_unpack('nnnnnn') 1570 | o.id = id 1571 | o.tc = (flag >> 9) & 1 1572 | o.rcode = flag & 15 1573 | return o unless o.tc.zero? 1574 | 1575 | o.qr = (flag >> 15) & 1 1576 | o.opcode = (flag >> 11) & 15 1577 | o.aa = (flag >> 10) & 1 1578 | o.rd = (flag >> 8) & 1 1579 | o.ra = (flag >> 7) & 1 1580 | (1..qdcount).each { 1581 | name, typeclass = msg.get_question 1582 | o.add_question(name, typeclass) 1583 | } 1584 | (1..ancount).each { 1585 | name, ttl, data = msg.get_rr 1586 | o.add_answer(name, ttl, data) 1587 | } 1588 | (1..nscount).each { 1589 | name, ttl, data = msg.get_rr 1590 | o.add_authority(name, ttl, data) 1591 | } 1592 | (1..arcount).each { 1593 | name, ttl, data = msg.get_rr 1594 | o.add_additional(name, ttl, data) 1595 | } 1596 | } 1597 | return o 1598 | end 1599 | 1600 | class MessageDecoder # :nodoc: 1601 | def initialize(data) 1602 | @data = data 1603 | @index = 0 1604 | @limit = data.bytesize 1605 | yield self 1606 | end 1607 | 1608 | def inspect 1609 | "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" 1610 | end 1611 | 1612 | def get_length16 1613 | len, = self.get_unpack('n') 1614 | save_limit = @limit 1615 | @limit = @index + len 1616 | d = yield(len) 1617 | if @index < @limit 1618 | raise DecodeError.new("junk exists") 1619 | elsif @limit < @index 1620 | raise DecodeError.new("limit exceeded") 1621 | end 1622 | @limit = save_limit 1623 | return d 1624 | end 1625 | 1626 | def get_bytes(len = @limit - @index) 1627 | raise DecodeError.new("limit exceeded") if @limit < @index + len 1628 | d = @data.byteslice(@index, len) 1629 | @index += len 1630 | return d 1631 | end 1632 | 1633 | def get_unpack(template) 1634 | len = 0 1635 | template.each_byte {|byte| 1636 | byte = "%c" % byte 1637 | case byte 1638 | when ?c, ?C 1639 | len += 1 1640 | when ?n 1641 | len += 2 1642 | when ?N 1643 | len += 4 1644 | else 1645 | raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'") 1646 | end 1647 | } 1648 | raise DecodeError.new("limit exceeded") if @limit < @index + len 1649 | arr = @data.unpack("@#{@index}#{template}") 1650 | @index += len 1651 | return arr 1652 | end 1653 | 1654 | def get_string 1655 | raise DecodeError.new("limit exceeded") if @limit <= @index 1656 | len = @data.getbyte(@index) 1657 | raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len 1658 | d = @data.byteslice(@index + 1, len) 1659 | @index += 1 + len 1660 | return d 1661 | end 1662 | 1663 | def get_string_list 1664 | strings = [] 1665 | while @index < @limit 1666 | strings << self.get_string 1667 | end 1668 | strings 1669 | end 1670 | 1671 | def get_list 1672 | [].tap do |values| 1673 | while @index < @limit 1674 | values << yield 1675 | end 1676 | end 1677 | end 1678 | 1679 | def get_name 1680 | return Name.new(self.get_labels) 1681 | end 1682 | 1683 | def get_labels 1684 | prev_index = @index 1685 | save_index = nil 1686 | d = [] 1687 | size = -1 1688 | while true 1689 | raise DecodeError.new("limit exceeded") if @limit <= @index 1690 | case @data.getbyte(@index) 1691 | when 0 1692 | @index += 1 1693 | if save_index 1694 | @index = save_index 1695 | end 1696 | return d 1697 | when 192..255 1698 | idx = self.get_unpack('n')[0] & 0x3fff 1699 | if prev_index <= idx 1700 | raise DecodeError.new("non-backward name pointer") 1701 | end 1702 | prev_index = idx 1703 | if !save_index 1704 | save_index = @index 1705 | end 1706 | @index = idx 1707 | else 1708 | l = self.get_label 1709 | d << l 1710 | size += 1 + l.string.bytesize 1711 | raise DecodeError.new("name label data exceed 255 octets") if size > 255 1712 | end 1713 | end 1714 | end 1715 | 1716 | def get_label 1717 | return Label::Str.new(self.get_string) 1718 | end 1719 | 1720 | def get_question 1721 | name = self.get_name 1722 | type, klass = self.get_unpack("nn") 1723 | return name, Resource.get_class(type, klass) 1724 | end 1725 | 1726 | def get_rr 1727 | name = self.get_name 1728 | type, klass, ttl = self.get_unpack('nnN') 1729 | typeclass = Resource.get_class(type, klass) 1730 | res = self.get_length16 do 1731 | begin 1732 | typeclass.decode_rdata self 1733 | rescue => e 1734 | raise DecodeError, e.message, e.backtrace 1735 | end 1736 | end 1737 | res.instance_variable_set :@ttl, ttl 1738 | return name, ttl, res 1739 | end 1740 | end 1741 | end 1742 | 1743 | ## 1744 | # SvcParams for service binding RRs. [RFC9460] 1745 | 1746 | class SvcParams 1747 | include Enumerable 1748 | 1749 | ## 1750 | # Create a list of SvcParams with the given initial content. 1751 | # 1752 | # +params+ has to be an enumerable of +SvcParam+s. 1753 | # If its content has +SvcParam+s with the duplicate key, 1754 | # the one appears last takes precedence. 1755 | 1756 | def initialize(params = []) 1757 | @params = {} 1758 | 1759 | params.each do |param| 1760 | add param 1761 | end 1762 | end 1763 | 1764 | ## 1765 | # Get SvcParam for the given +key+ in this list. 1766 | 1767 | def [](key) 1768 | @params[canonical_key(key)] 1769 | end 1770 | 1771 | ## 1772 | # Get the number of SvcParams in this list. 1773 | 1774 | def count 1775 | @params.count 1776 | end 1777 | 1778 | ## 1779 | # Get whether this list is empty. 1780 | 1781 | def empty? 1782 | @params.empty? 1783 | end 1784 | 1785 | ## 1786 | # Add the SvcParam +param+ to this list, overwriting the existing one with the same key. 1787 | 1788 | def add(param) 1789 | @params[param.class.key_number] = param 1790 | end 1791 | 1792 | ## 1793 | # Remove the +SvcParam+ with the given +key+ and return it. 1794 | 1795 | def delete(key) 1796 | @params.delete(canonical_key(key)) 1797 | end 1798 | 1799 | ## 1800 | # Enumerate the +SvcParam+s in this list. 1801 | 1802 | def each(&block) 1803 | return enum_for(:each) unless block 1804 | @params.each_value(&block) 1805 | end 1806 | 1807 | def encode(msg) # :nodoc: 1808 | @params.keys.sort.each do |key| 1809 | msg.put_pack('n', key) 1810 | msg.put_length16 do 1811 | @params.fetch(key).encode(msg) 1812 | end 1813 | end 1814 | end 1815 | 1816 | def self.decode(msg) # :nodoc: 1817 | params = msg.get_list do 1818 | key, = msg.get_unpack('n') 1819 | msg.get_length16 do 1820 | SvcParam::ClassHash[key].decode(msg) 1821 | end 1822 | end 1823 | 1824 | return self.new(params) 1825 | end 1826 | 1827 | private 1828 | 1829 | def canonical_key(key) # :nodoc: 1830 | case key 1831 | when Integer 1832 | key 1833 | when /\Akey(\d+)\z/ 1834 | Integer($1) 1835 | when Symbol 1836 | SvcParam::ClassHash[key].key_number 1837 | else 1838 | raise TypeError, 'key must be either String or Symbol' 1839 | end 1840 | end 1841 | end 1842 | 1843 | ## 1844 | # Base class for SvcParam. [RFC9460] 1845 | 1846 | class SvcParam 1847 | 1848 | ## 1849 | # Get the presentation name of the SvcParamKey. 1850 | 1851 | def self.key_name 1852 | const_get(:KeyName) 1853 | end 1854 | 1855 | ## 1856 | # Get the registered number of the SvcParamKey. 1857 | 1858 | def self.key_number 1859 | const_get(:KeyNumber) 1860 | end 1861 | 1862 | ClassHash = Hash.new do |h, key| # :nodoc: 1863 | case key 1864 | when Integer 1865 | Generic.create(key) 1866 | when /\Akey(?\d+)\z/ 1867 | Generic.create(key.to_int) 1868 | when Symbol 1869 | raise KeyError, "unknown key #{key}" 1870 | else 1871 | raise TypeError, 'key must be either String or Symbol' 1872 | end 1873 | end 1874 | 1875 | ## 1876 | # Generic SvcParam abstract class. 1877 | 1878 | class Generic < SvcParam 1879 | 1880 | ## 1881 | # SvcParamValue in wire-format byte string. 1882 | 1883 | attr_reader :value 1884 | 1885 | ## 1886 | # Create generic SvcParam 1887 | 1888 | def initialize(value) 1889 | @value = value 1890 | end 1891 | 1892 | def encode(msg) # :nodoc: 1893 | msg.put_bytes(@value) 1894 | end 1895 | 1896 | def self.decode(msg) # :nodoc: 1897 | return self.new(msg.get_bytes) 1898 | end 1899 | 1900 | def self.create(key_number) 1901 | c = Class.new(Generic) 1902 | key_name = :"key#{key_number}" 1903 | c.const_set(:KeyName, key_name) 1904 | c.const_set(:KeyNumber, key_number) 1905 | self.const_set(:"Key#{key_number}", c) 1906 | ClassHash[key_name] = ClassHash[key_number] = c 1907 | return c 1908 | end 1909 | end 1910 | 1911 | ## 1912 | # "mandatory" SvcParam -- Mandatory keys in service binding RR 1913 | 1914 | class Mandatory < SvcParam 1915 | KeyName = :mandatory 1916 | KeyNumber = 0 1917 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 1918 | 1919 | ## 1920 | # Mandatory keys. 1921 | 1922 | attr_reader :keys 1923 | 1924 | ## 1925 | # Initialize "mandatory" ScvParam. 1926 | 1927 | def initialize(keys) 1928 | @keys = keys.map(&:to_int) 1929 | end 1930 | 1931 | def encode(msg) # :nodoc: 1932 | @keys.sort.each do |key| 1933 | msg.put_pack('n', key) 1934 | end 1935 | end 1936 | 1937 | def self.decode(msg) # :nodoc: 1938 | keys = msg.get_list { msg.get_unpack('n')[0] } 1939 | return self.new(keys) 1940 | end 1941 | end 1942 | 1943 | ## 1944 | # "alpn" SvcParam -- Additional supported protocols 1945 | 1946 | class ALPN < SvcParam 1947 | KeyName = :alpn 1948 | KeyNumber = 1 1949 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 1950 | 1951 | ## 1952 | # Supported protocol IDs. 1953 | 1954 | attr_reader :protocol_ids 1955 | 1956 | ## 1957 | # Initialize "alpn" ScvParam. 1958 | 1959 | def initialize(protocol_ids) 1960 | @protocol_ids = protocol_ids.map(&:to_str) 1961 | end 1962 | 1963 | def encode(msg) # :nodoc: 1964 | msg.put_string_list(@protocol_ids) 1965 | end 1966 | 1967 | def self.decode(msg) # :nodoc: 1968 | return self.new(msg.get_string_list) 1969 | end 1970 | end 1971 | 1972 | ## 1973 | # "no-default-alpn" SvcParam -- No support for default protocol 1974 | 1975 | class NoDefaultALPN < SvcParam 1976 | KeyName = :'no-default-alpn' 1977 | KeyNumber = 2 1978 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 1979 | 1980 | def encode(msg) # :nodoc: 1981 | # no payload 1982 | end 1983 | 1984 | def self.decode(msg) # :nodoc: 1985 | return self.new 1986 | end 1987 | end 1988 | 1989 | ## 1990 | # "port" SvcParam -- Port for alternative endpoint 1991 | 1992 | class Port < SvcParam 1993 | KeyName = :port 1994 | KeyNumber = 3 1995 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 1996 | 1997 | ## 1998 | # Port number. 1999 | 2000 | attr_reader :port 2001 | 2002 | ## 2003 | # Initialize "port" ScvParam. 2004 | 2005 | def initialize(port) 2006 | @port = port.to_int 2007 | end 2008 | 2009 | def encode(msg) # :nodoc: 2010 | msg.put_pack('n', @port) 2011 | end 2012 | 2013 | def self.decode(msg) # :nodoc: 2014 | port, = msg.get_unpack('n') 2015 | return self.new(port) 2016 | end 2017 | end 2018 | 2019 | ## 2020 | # "ipv4hint" SvcParam -- IPv4 address hints 2021 | 2022 | class IPv4Hint < SvcParam 2023 | KeyName = :ipv4hint 2024 | KeyNumber = 4 2025 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 2026 | 2027 | ## 2028 | # Set of IPv4 addresses. 2029 | 2030 | attr_reader :addresses 2031 | 2032 | ## 2033 | # Initialize "ipv4hint" ScvParam. 2034 | 2035 | def initialize(addresses) 2036 | @addresses = addresses.map {|address| IPv4.create(address) } 2037 | end 2038 | 2039 | def encode(msg) # :nodoc: 2040 | @addresses.each do |address| 2041 | msg.put_bytes(address.address) 2042 | end 2043 | end 2044 | 2045 | def self.decode(msg) # :nodoc: 2046 | addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) } 2047 | return self.new(addresses) 2048 | end 2049 | end 2050 | 2051 | ## 2052 | # "ipv6hint" SvcParam -- IPv6 address hints 2053 | 2054 | class IPv6Hint < SvcParam 2055 | KeyName = :ipv6hint 2056 | KeyNumber = 6 2057 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 2058 | 2059 | ## 2060 | # Set of IPv6 addresses. 2061 | 2062 | attr_reader :addresses 2063 | 2064 | ## 2065 | # Initialize "ipv6hint" ScvParam. 2066 | 2067 | def initialize(addresses) 2068 | @addresses = addresses.map {|address| IPv6.create(address) } 2069 | end 2070 | 2071 | def encode(msg) # :nodoc: 2072 | @addresses.each do |address| 2073 | msg.put_bytes(address.address) 2074 | end 2075 | end 2076 | 2077 | def self.decode(msg) # :nodoc: 2078 | addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) } 2079 | return self.new(addresses) 2080 | end 2081 | end 2082 | 2083 | ## 2084 | # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461] 2085 | 2086 | class DoHPath < SvcParam 2087 | KeyName = :dohpath 2088 | KeyNumber = 7 2089 | ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: 2090 | 2091 | ## 2092 | # URI template for DoH queries. 2093 | 2094 | attr_reader :template 2095 | 2096 | ## 2097 | # Initialize "dohpath" ScvParam. 2098 | 2099 | def initialize(template) 2100 | @template = template.encode('utf-8') 2101 | end 2102 | 2103 | def encode(msg) # :nodoc: 2104 | msg.put_bytes(@template) 2105 | end 2106 | 2107 | def self.decode(msg) # :nodoc: 2108 | template = msg.get_bytes.force_encoding('utf-8') 2109 | return self.new(template) 2110 | end 2111 | end 2112 | end 2113 | 2114 | ## 2115 | # A DNS query abstract class. 2116 | 2117 | class Query 2118 | def encode_rdata(msg) # :nodoc: 2119 | raise EncodeError.new("#{self.class} is query.") 2120 | end 2121 | 2122 | def self.decode_rdata(msg) # :nodoc: 2123 | raise DecodeError.new("#{self.class} is query.") 2124 | end 2125 | end 2126 | 2127 | ## 2128 | # A DNS resource abstract class. 2129 | 2130 | class Resource < Query 2131 | 2132 | ## 2133 | # Remaining Time To Live for this Resource. 2134 | 2135 | attr_reader :ttl 2136 | 2137 | ClassHash = Module.new do 2138 | module_function 2139 | 2140 | def []=(type_class_value, klass) 2141 | type_value, class_value = type_class_value 2142 | Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass) 2143 | end 2144 | end 2145 | 2146 | def encode_rdata(msg) # :nodoc: 2147 | raise NotImplementedError.new 2148 | end 2149 | 2150 | def self.decode_rdata(msg) # :nodoc: 2151 | raise NotImplementedError.new 2152 | end 2153 | 2154 | def ==(other) # :nodoc: 2155 | return false unless self.class == other.class 2156 | s_ivars = self.instance_variables 2157 | s_ivars.sort! 2158 | s_ivars.delete :@ttl 2159 | o_ivars = other.instance_variables 2160 | o_ivars.sort! 2161 | o_ivars.delete :@ttl 2162 | return s_ivars == o_ivars && 2163 | s_ivars.collect {|name| self.instance_variable_get name} == 2164 | o_ivars.collect {|name| other.instance_variable_get name} 2165 | end 2166 | 2167 | def eql?(other) # :nodoc: 2168 | return self == other 2169 | end 2170 | 2171 | def hash # :nodoc: 2172 | h = 0 2173 | vars = self.instance_variables 2174 | vars.delete :@ttl 2175 | vars.each {|name| 2176 | h ^= self.instance_variable_get(name).hash 2177 | } 2178 | return h 2179 | end 2180 | 2181 | def self.get_class(type_value, class_value) # :nodoc: 2182 | cache = :"Type#{type_value}_Class#{class_value}" 2183 | 2184 | return (const_defined?(cache) && const_get(cache)) || 2185 | Generic.create(type_value, class_value) 2186 | end 2187 | 2188 | ## 2189 | # A generic resource abstract class. 2190 | 2191 | class Generic < Resource 2192 | 2193 | ## 2194 | # Creates a new generic resource. 2195 | 2196 | def initialize(data) 2197 | @data = data 2198 | end 2199 | 2200 | ## 2201 | # Data for this generic resource. 2202 | 2203 | attr_reader :data 2204 | 2205 | def encode_rdata(msg) # :nodoc: 2206 | msg.put_bytes(data) 2207 | end 2208 | 2209 | def self.decode_rdata(msg) # :nodoc: 2210 | return self.new(msg.get_bytes) 2211 | end 2212 | 2213 | def self.create(type_value, class_value) # :nodoc: 2214 | c = Class.new(Generic) 2215 | c.const_set(:TypeValue, type_value) 2216 | c.const_set(:ClassValue, class_value) 2217 | Generic.const_set("Type#{type_value}_Class#{class_value}", c) 2218 | ClassHash[[type_value, class_value]] = c 2219 | return c 2220 | end 2221 | end 2222 | 2223 | ## 2224 | # Domain Name resource abstract class. 2225 | 2226 | class DomainName < Resource 2227 | 2228 | ## 2229 | # Creates a new DomainName from +name+. 2230 | 2231 | def initialize(name) 2232 | @name = name 2233 | end 2234 | 2235 | ## 2236 | # The name of this DomainName. 2237 | 2238 | attr_reader :name 2239 | 2240 | def encode_rdata(msg) # :nodoc: 2241 | msg.put_name(@name) 2242 | end 2243 | 2244 | def self.decode_rdata(msg) # :nodoc: 2245 | return self.new(msg.get_name) 2246 | end 2247 | end 2248 | 2249 | # Standard (class generic) RRs 2250 | 2251 | ClassValue = nil # :nodoc: 2252 | 2253 | ## 2254 | # An authoritative name server. 2255 | 2256 | class NS < DomainName 2257 | TypeValue = 2 # :nodoc: 2258 | end 2259 | 2260 | ## 2261 | # The canonical name for an alias. 2262 | 2263 | class CNAME < DomainName 2264 | TypeValue = 5 # :nodoc: 2265 | end 2266 | 2267 | ## 2268 | # Start Of Authority resource. 2269 | 2270 | class SOA < Resource 2271 | 2272 | TypeValue = 6 # :nodoc: 2273 | 2274 | ## 2275 | # Creates a new SOA record. See the attr documentation for the 2276 | # details of each argument. 2277 | 2278 | def initialize(mname, rname, serial, refresh, retry_, expire, minimum) 2279 | @mname = mname 2280 | @rname = rname 2281 | @serial = serial 2282 | @refresh = refresh 2283 | @retry = retry_ 2284 | @expire = expire 2285 | @minimum = minimum 2286 | end 2287 | 2288 | ## 2289 | # Name of the host where the master zone file for this zone resides. 2290 | 2291 | attr_reader :mname 2292 | 2293 | ## 2294 | # The person responsible for this domain name. 2295 | 2296 | attr_reader :rname 2297 | 2298 | ## 2299 | # The version number of the zone file. 2300 | 2301 | attr_reader :serial 2302 | 2303 | ## 2304 | # How often, in seconds, a secondary name server is to check for 2305 | # updates from the primary name server. 2306 | 2307 | attr_reader :refresh 2308 | 2309 | ## 2310 | # How often, in seconds, a secondary name server is to retry after a 2311 | # failure to check for a refresh. 2312 | 2313 | attr_reader :retry 2314 | 2315 | ## 2316 | # Time in seconds that a secondary name server is to use the data 2317 | # before refreshing from the primary name server. 2318 | 2319 | attr_reader :expire 2320 | 2321 | ## 2322 | # The minimum number of seconds to be used for TTL values in RRs. 2323 | 2324 | attr_reader :minimum 2325 | 2326 | def encode_rdata(msg) # :nodoc: 2327 | msg.put_name(@mname) 2328 | msg.put_name(@rname) 2329 | msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum) 2330 | end 2331 | 2332 | def self.decode_rdata(msg) # :nodoc: 2333 | mname = msg.get_name 2334 | rname = msg.get_name 2335 | serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN') 2336 | return self.new( 2337 | mname, rname, serial, refresh, retry_, expire, minimum) 2338 | end 2339 | end 2340 | 2341 | ## 2342 | # A Pointer to another DNS name. 2343 | 2344 | class PTR < DomainName 2345 | TypeValue = 12 # :nodoc: 2346 | end 2347 | 2348 | ## 2349 | # Host Information resource. 2350 | 2351 | class HINFO < Resource 2352 | 2353 | TypeValue = 13 # :nodoc: 2354 | 2355 | ## 2356 | # Creates a new HINFO running +os+ on +cpu+. 2357 | 2358 | def initialize(cpu, os) 2359 | @cpu = cpu 2360 | @os = os 2361 | end 2362 | 2363 | ## 2364 | # CPU architecture for this resource. 2365 | 2366 | attr_reader :cpu 2367 | 2368 | ## 2369 | # Operating system for this resource. 2370 | 2371 | attr_reader :os 2372 | 2373 | def encode_rdata(msg) # :nodoc: 2374 | msg.put_string(@cpu) 2375 | msg.put_string(@os) 2376 | end 2377 | 2378 | def self.decode_rdata(msg) # :nodoc: 2379 | cpu = msg.get_string 2380 | os = msg.get_string 2381 | return self.new(cpu, os) 2382 | end 2383 | end 2384 | 2385 | ## 2386 | # Mailing list or mailbox information. 2387 | 2388 | class MINFO < Resource 2389 | 2390 | TypeValue = 14 # :nodoc: 2391 | 2392 | def initialize(rmailbx, emailbx) 2393 | @rmailbx = rmailbx 2394 | @emailbx = emailbx 2395 | end 2396 | 2397 | ## 2398 | # Domain name responsible for this mail list or mailbox. 2399 | 2400 | attr_reader :rmailbx 2401 | 2402 | ## 2403 | # Mailbox to use for error messages related to the mail list or mailbox. 2404 | 2405 | attr_reader :emailbx 2406 | 2407 | def encode_rdata(msg) # :nodoc: 2408 | msg.put_name(@rmailbx) 2409 | msg.put_name(@emailbx) 2410 | end 2411 | 2412 | def self.decode_rdata(msg) # :nodoc: 2413 | rmailbx = msg.get_string 2414 | emailbx = msg.get_string 2415 | return self.new(rmailbx, emailbx) 2416 | end 2417 | end 2418 | 2419 | ## 2420 | # Mail Exchanger resource. 2421 | 2422 | class MX < Resource 2423 | 2424 | TypeValue= 15 # :nodoc: 2425 | 2426 | ## 2427 | # Creates a new MX record with +preference+, accepting mail at 2428 | # +exchange+. 2429 | 2430 | def initialize(preference, exchange) 2431 | @preference = preference 2432 | @exchange = exchange 2433 | end 2434 | 2435 | ## 2436 | # The preference for this MX. 2437 | 2438 | attr_reader :preference 2439 | 2440 | ## 2441 | # The host of this MX. 2442 | 2443 | attr_reader :exchange 2444 | 2445 | def encode_rdata(msg) # :nodoc: 2446 | msg.put_pack('n', @preference) 2447 | msg.put_name(@exchange) 2448 | end 2449 | 2450 | def self.decode_rdata(msg) # :nodoc: 2451 | preference, = msg.get_unpack('n') 2452 | exchange = msg.get_name 2453 | return self.new(preference, exchange) 2454 | end 2455 | end 2456 | 2457 | ## 2458 | # Unstructured text resource. 2459 | 2460 | class TXT < Resource 2461 | 2462 | TypeValue = 16 # :nodoc: 2463 | 2464 | def initialize(first_string, *rest_strings) 2465 | @strings = [first_string, *rest_strings] 2466 | end 2467 | 2468 | ## 2469 | # Returns an Array of Strings for this TXT record. 2470 | 2471 | attr_reader :strings 2472 | 2473 | ## 2474 | # Returns the concatenated string from +strings+. 2475 | 2476 | def data 2477 | @strings.join("") 2478 | end 2479 | 2480 | def encode_rdata(msg) # :nodoc: 2481 | msg.put_string_list(@strings) 2482 | end 2483 | 2484 | def self.decode_rdata(msg) # :nodoc: 2485 | strings = msg.get_string_list 2486 | return self.new(*strings) 2487 | end 2488 | end 2489 | 2490 | ## 2491 | # Location resource 2492 | 2493 | class LOC < Resource 2494 | 2495 | TypeValue = 29 # :nodoc: 2496 | 2497 | def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude) 2498 | @version = version 2499 | @ssize = Resolv::LOC::Size.create(ssize) 2500 | @hprecision = Resolv::LOC::Size.create(hprecision) 2501 | @vprecision = Resolv::LOC::Size.create(vprecision) 2502 | @latitude = Resolv::LOC::Coord.create(latitude) 2503 | @longitude = Resolv::LOC::Coord.create(longitude) 2504 | @altitude = Resolv::LOC::Alt.create(altitude) 2505 | end 2506 | 2507 | ## 2508 | # Returns the version value for this LOC record which should always be 00 2509 | 2510 | attr_reader :version 2511 | 2512 | ## 2513 | # The spherical size of this LOC 2514 | # in meters using scientific notation as 2 integers of XeY 2515 | 2516 | attr_reader :ssize 2517 | 2518 | ## 2519 | # The horizontal precision using ssize type values 2520 | # in meters using scientific notation as 2 integers of XeY 2521 | # for precision use value/2 e.g. 2m = +/-1m 2522 | 2523 | attr_reader :hprecision 2524 | 2525 | ## 2526 | # The vertical precision using ssize type values 2527 | # in meters using scientific notation as 2 integers of XeY 2528 | # for precision use value/2 e.g. 2m = +/-1m 2529 | 2530 | attr_reader :vprecision 2531 | 2532 | ## 2533 | # The latitude for this LOC where 2**31 is the equator 2534 | # in thousandths of an arc second as an unsigned 32bit integer 2535 | 2536 | attr_reader :latitude 2537 | 2538 | ## 2539 | # The longitude for this LOC where 2**31 is the prime meridian 2540 | # in thousandths of an arc second as an unsigned 32bit integer 2541 | 2542 | attr_reader :longitude 2543 | 2544 | ## 2545 | # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid 2546 | # in centimeters as an unsigned 32bit integer 2547 | 2548 | attr_reader :altitude 2549 | 2550 | def encode_rdata(msg) # :nodoc: 2551 | msg.put_bytes(@version) 2552 | msg.put_bytes(@ssize.scalar) 2553 | msg.put_bytes(@hprecision.scalar) 2554 | msg.put_bytes(@vprecision.scalar) 2555 | msg.put_bytes(@latitude.coordinates) 2556 | msg.put_bytes(@longitude.coordinates) 2557 | msg.put_bytes(@altitude.altitude) 2558 | end 2559 | 2560 | def self.decode_rdata(msg) # :nodoc: 2561 | version = msg.get_bytes(1) 2562 | ssize = msg.get_bytes(1) 2563 | hprecision = msg.get_bytes(1) 2564 | vprecision = msg.get_bytes(1) 2565 | latitude = msg.get_bytes(4) 2566 | longitude = msg.get_bytes(4) 2567 | altitude = msg.get_bytes(4) 2568 | return self.new( 2569 | version, 2570 | Resolv::LOC::Size.new(ssize), 2571 | Resolv::LOC::Size.new(hprecision), 2572 | Resolv::LOC::Size.new(vprecision), 2573 | Resolv::LOC::Coord.new(latitude,"lat"), 2574 | Resolv::LOC::Coord.new(longitude,"lon"), 2575 | Resolv::LOC::Alt.new(altitude) 2576 | ) 2577 | end 2578 | end 2579 | 2580 | ## 2581 | # A Query type requesting any RR. 2582 | 2583 | class ANY < Query 2584 | TypeValue = 255 # :nodoc: 2585 | end 2586 | 2587 | ## 2588 | # CAA resource record defined in RFC 8659 2589 | # 2590 | # These records identify certificate authority allowed to issue 2591 | # certificates for the given domain. 2592 | 2593 | class CAA < Resource 2594 | TypeValue = 257 2595 | 2596 | ## 2597 | # Creates a new CAA for +flags+, +tag+ and +value+. 2598 | 2599 | def initialize(flags, tag, value) 2600 | unless (0..255) === flags 2601 | raise ArgumentError.new('flags must be an Integer between 0 and 255') 2602 | end 2603 | unless (1..15) === tag.bytesize 2604 | raise ArgumentError.new('length of tag must be between 1 and 15') 2605 | end 2606 | 2607 | @flags = flags 2608 | @tag = tag 2609 | @value = value 2610 | end 2611 | 2612 | ## 2613 | # Flags for this property: 2614 | # - Bit 0 : 0 = not critical, 1 = critical 2615 | 2616 | attr_reader :flags 2617 | 2618 | ## 2619 | # Property tag ("issue", "issuewild", "iodef"...). 2620 | 2621 | attr_reader :tag 2622 | 2623 | ## 2624 | # Property value. 2625 | 2626 | attr_reader :value 2627 | 2628 | ## 2629 | # Whether the critical flag is set on this property. 2630 | 2631 | def critical? 2632 | flags & 0x80 != 0 2633 | end 2634 | 2635 | def encode_rdata(msg) # :nodoc: 2636 | msg.put_pack('C', @flags) 2637 | msg.put_string(@tag) 2638 | msg.put_bytes(@value) 2639 | end 2640 | 2641 | def self.decode_rdata(msg) # :nodoc: 2642 | flags, = msg.get_unpack('C') 2643 | tag = msg.get_string 2644 | value = msg.get_bytes 2645 | self.new flags, tag, value 2646 | end 2647 | end 2648 | 2649 | ClassInsensitiveTypes = [ # :nodoc: 2650 | NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA 2651 | ] 2652 | 2653 | ## 2654 | # module IN contains ARPA Internet specific RRs. 2655 | 2656 | module IN 2657 | 2658 | ClassValue = 1 # :nodoc: 2659 | 2660 | ClassInsensitiveTypes.each {|s| 2661 | c = Class.new(s) 2662 | c.const_set(:TypeValue, s::TypeValue) 2663 | c.const_set(:ClassValue, ClassValue) 2664 | ClassHash[[s::TypeValue, ClassValue]] = c 2665 | self.const_set(s.name.sub(/.*::/, ''), c) 2666 | } 2667 | 2668 | ## 2669 | # IPv4 Address resource 2670 | 2671 | class A < Resource 2672 | TypeValue = 1 2673 | ClassValue = IN::ClassValue 2674 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2675 | 2676 | ## 2677 | # Creates a new A for +address+. 2678 | 2679 | def initialize(address) 2680 | @address = IPv4.create(address) 2681 | end 2682 | 2683 | ## 2684 | # The Resolv::IPv4 address for this A. 2685 | 2686 | attr_reader :address 2687 | 2688 | def encode_rdata(msg) # :nodoc: 2689 | msg.put_bytes(@address.address) 2690 | end 2691 | 2692 | def self.decode_rdata(msg) # :nodoc: 2693 | return self.new(IPv4.new(msg.get_bytes(4))) 2694 | end 2695 | end 2696 | 2697 | ## 2698 | # Well Known Service resource. 2699 | 2700 | class WKS < Resource 2701 | TypeValue = 11 2702 | ClassValue = IN::ClassValue 2703 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2704 | 2705 | def initialize(address, protocol, bitmap) 2706 | @address = IPv4.create(address) 2707 | @protocol = protocol 2708 | @bitmap = bitmap 2709 | end 2710 | 2711 | ## 2712 | # The host these services run on. 2713 | 2714 | attr_reader :address 2715 | 2716 | ## 2717 | # IP protocol number for these services. 2718 | 2719 | attr_reader :protocol 2720 | 2721 | ## 2722 | # A bit map of enabled services on this host. 2723 | # 2724 | # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP 2725 | # service (port 25). If this bit is set, then an SMTP server should 2726 | # be listening on TCP port 25; if zero, SMTP service is not 2727 | # supported. 2728 | 2729 | attr_reader :bitmap 2730 | 2731 | def encode_rdata(msg) # :nodoc: 2732 | msg.put_bytes(@address.address) 2733 | msg.put_pack("n", @protocol) 2734 | msg.put_bytes(@bitmap) 2735 | end 2736 | 2737 | def self.decode_rdata(msg) # :nodoc: 2738 | address = IPv4.new(msg.get_bytes(4)) 2739 | protocol, = msg.get_unpack("n") 2740 | bitmap = msg.get_bytes 2741 | return self.new(address, protocol, bitmap) 2742 | end 2743 | end 2744 | 2745 | ## 2746 | # An IPv6 address record. 2747 | 2748 | class AAAA < Resource 2749 | TypeValue = 28 2750 | ClassValue = IN::ClassValue 2751 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2752 | 2753 | ## 2754 | # Creates a new AAAA for +address+. 2755 | 2756 | def initialize(address) 2757 | @address = IPv6.create(address) 2758 | end 2759 | 2760 | ## 2761 | # The Resolv::IPv6 address for this AAAA. 2762 | 2763 | attr_reader :address 2764 | 2765 | def encode_rdata(msg) # :nodoc: 2766 | msg.put_bytes(@address.address) 2767 | end 2768 | 2769 | def self.decode_rdata(msg) # :nodoc: 2770 | return self.new(IPv6.new(msg.get_bytes(16))) 2771 | end 2772 | end 2773 | 2774 | ## 2775 | # SRV resource record defined in RFC 2782 2776 | # 2777 | # These records identify the hostname and port that a service is 2778 | # available at. 2779 | 2780 | class SRV < Resource 2781 | TypeValue = 33 2782 | ClassValue = IN::ClassValue 2783 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2784 | 2785 | # Create a SRV resource record. 2786 | # 2787 | # See the documentation for #priority, #weight, #port and #target 2788 | # for +priority+, +weight+, +port and +target+ respectively. 2789 | 2790 | def initialize(priority, weight, port, target) 2791 | @priority = priority.to_int 2792 | @weight = weight.to_int 2793 | @port = port.to_int 2794 | @target = Name.create(target) 2795 | end 2796 | 2797 | # The priority of this target host. 2798 | # 2799 | # A client MUST attempt to contact the target host with the 2800 | # lowest-numbered priority it can reach; target hosts with the same 2801 | # priority SHOULD be tried in an order defined by the weight field. 2802 | # The range is 0-65535. Note that it is not widely implemented and 2803 | # should be set to zero. 2804 | 2805 | attr_reader :priority 2806 | 2807 | # A server selection mechanism. 2808 | # 2809 | # The weight field specifies a relative weight for entries with the 2810 | # same priority. Larger weights SHOULD be given a proportionately 2811 | # higher probability of being selected. The range of this number is 2812 | # 0-65535. Domain administrators SHOULD use Weight 0 when there 2813 | # isn't any server selection to do, to make the RR easier to read 2814 | # for humans (less noisy). Note that it is not widely implemented 2815 | # and should be set to zero. 2816 | 2817 | attr_reader :weight 2818 | 2819 | # The port on this target host of this service. 2820 | # 2821 | # The range is 0-65535. 2822 | 2823 | attr_reader :port 2824 | 2825 | # The domain name of the target host. 2826 | # 2827 | # A target of "." means that the service is decidedly not available 2828 | # at this domain. 2829 | 2830 | attr_reader :target 2831 | 2832 | def encode_rdata(msg) # :nodoc: 2833 | msg.put_pack("n", @priority) 2834 | msg.put_pack("n", @weight) 2835 | msg.put_pack("n", @port) 2836 | msg.put_name(@target, compress: false) 2837 | end 2838 | 2839 | def self.decode_rdata(msg) # :nodoc: 2840 | priority, = msg.get_unpack("n") 2841 | weight, = msg.get_unpack("n") 2842 | port, = msg.get_unpack("n") 2843 | target = msg.get_name 2844 | return self.new(priority, weight, port, target) 2845 | end 2846 | end 2847 | 2848 | ## 2849 | # Common implementation for SVCB-compatible resource records. 2850 | 2851 | class ServiceBinding 2852 | 2853 | ## 2854 | # Create a service binding resource record. 2855 | 2856 | def initialize(priority, target, params = []) 2857 | @priority = priority.to_int 2858 | @target = Name.create(target) 2859 | @params = SvcParams.new(params) 2860 | end 2861 | 2862 | ## 2863 | # The priority of this target host. 2864 | # 2865 | # The range is 0-65535. 2866 | # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode. 2867 | 2868 | attr_reader :priority 2869 | 2870 | ## 2871 | # The domain name of the target host. 2872 | 2873 | attr_reader :target 2874 | 2875 | ## 2876 | # The service parameters for the target host. 2877 | 2878 | attr_reader :params 2879 | 2880 | ## 2881 | # Whether this RR is in AliasMode. 2882 | 2883 | def alias_mode? 2884 | self.priority == 0 2885 | end 2886 | 2887 | ## 2888 | # Whether this RR is in ServiceMode. 2889 | 2890 | def service_mode? 2891 | !alias_mode? 2892 | end 2893 | 2894 | def encode_rdata(msg) # :nodoc: 2895 | msg.put_pack("n", @priority) 2896 | msg.put_name(@target, compress: false) 2897 | @params.encode(msg) 2898 | end 2899 | 2900 | def self.decode_rdata(msg) # :nodoc: 2901 | priority, = msg.get_unpack("n") 2902 | target = msg.get_name 2903 | params = SvcParams.decode(msg) 2904 | return self.new(priority, target, params) 2905 | end 2906 | end 2907 | 2908 | ## 2909 | # SVCB resource record [RFC9460] 2910 | 2911 | class SVCB < ServiceBinding 2912 | TypeValue = 64 2913 | ClassValue = IN::ClassValue 2914 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2915 | end 2916 | 2917 | ## 2918 | # HTTPS resource record [RFC9460] 2919 | 2920 | class HTTPS < ServiceBinding 2921 | TypeValue = 65 2922 | ClassValue = IN::ClassValue 2923 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2924 | end 2925 | end 2926 | end 2927 | end 2928 | 2929 | ## 2930 | # A Resolv::DNS IPv4 address. 2931 | 2932 | class IPv4 2933 | 2934 | Regex256 = /0 2935 | |1(?:[0-9][0-9]?)? 2936 | |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? 2937 | |[3-9][0-9]?/x # :nodoc: 2938 | 2939 | ## 2940 | # Regular expression IPv4 addresses must match. 2941 | Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ 2942 | 2943 | ## 2944 | # Creates a new IPv4 address from +arg+ which may be: 2945 | # 2946 | # IPv4:: returns +arg+. 2947 | # String:: +arg+ must match the IPv4::Regex constant 2948 | 2949 | def self.create(arg) 2950 | case arg 2951 | when IPv4 2952 | return arg 2953 | when Regex 2954 | if (0..255) === (a = $1.to_i) && 2955 | (0..255) === (b = $2.to_i) && 2956 | (0..255) === (c = $3.to_i) && 2957 | (0..255) === (d = $4.to_i) 2958 | return self.new([a, b, c, d].pack("CCCC")) 2959 | else 2960 | raise ArgumentError.new("IPv4 address with invalid value: " + arg) 2961 | end 2962 | else 2963 | raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}") 2964 | end 2965 | end 2966 | 2967 | def initialize(address) # :nodoc: 2968 | unless address.kind_of?(String) 2969 | raise ArgumentError, 'IPv4 address must be a string' 2970 | end 2971 | unless address.length == 4 2972 | raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" 2973 | end 2974 | @address = address 2975 | end 2976 | 2977 | ## 2978 | # A String representation of this IPv4 address. 2979 | 2980 | ## 2981 | # The raw IPv4 address as a String. 2982 | 2983 | attr_reader :address 2984 | 2985 | def to_s # :nodoc: 2986 | return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC")) 2987 | end 2988 | 2989 | def inspect # :nodoc: 2990 | return "#<#{self.class} #{self}>" 2991 | end 2992 | 2993 | ## 2994 | # Turns this IPv4 address into a Resolv::DNS::Name. 2995 | 2996 | def to_name 2997 | return DNS::Name.create( 2998 | '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse) 2999 | end 3000 | 3001 | def ==(other) # :nodoc: 3002 | return @address == other.address 3003 | end 3004 | 3005 | def eql?(other) # :nodoc: 3006 | return self == other 3007 | end 3008 | 3009 | def hash # :nodoc: 3010 | return @address.hash 3011 | end 3012 | end 3013 | 3014 | ## 3015 | # A Resolv::DNS IPv6 address. 3016 | 3017 | class IPv6 3018 | 3019 | ## 3020 | # IPv6 address format a:b:c:d:e:f:g:h 3021 | Regex_8Hex = /\A 3022 | (?:[0-9A-Fa-f]{1,4}:){7} 3023 | [0-9A-Fa-f]{1,4} 3024 | \z/x 3025 | 3026 | ## 3027 | # Compressed IPv6 address format a::b 3028 | 3029 | Regex_CompressedHex = /\A 3030 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 3031 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 3032 | \z/x 3033 | 3034 | ## 3035 | # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z 3036 | 3037 | Regex_6Hex4Dec = /\A 3038 | ((?:[0-9A-Fa-f]{1,4}:){6,6}) 3039 | (\d+)\.(\d+)\.(\d+)\.(\d+) 3040 | \z/x 3041 | 3042 | ## 3043 | # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z 3044 | 3045 | Regex_CompressedHex4Dec = /\A 3046 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 3047 | ((?:[0-9A-Fa-f]{1,4}:)*) 3048 | (\d+)\.(\d+)\.(\d+)\.(\d+) 3049 | \z/x 3050 | 3051 | ## 3052 | # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1 3053 | Regex_8HexLinkLocal = /\A 3054 | [Ff][Ee]80 3055 | (?::[0-9A-Fa-f]{1,4}){7} 3056 | %[-0-9A-Za-z._~]+ 3057 | \z/x 3058 | 3059 | ## 3060 | # Compressed IPv6 link local address format fe80::b%em1 3061 | 3062 | Regex_CompressedHexLinkLocal = /\A 3063 | [Ff][Ee]80: 3064 | (?: 3065 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 3066 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 3067 | | 3068 | :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 3069 | )? 3070 | :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+ 3071 | \z/x 3072 | 3073 | ## 3074 | # A composite IPv6 address Regexp. 3075 | 3076 | Regex = / 3077 | (?:#{Regex_8Hex}) | 3078 | (?:#{Regex_CompressedHex}) | 3079 | (?:#{Regex_6Hex4Dec}) | 3080 | (?:#{Regex_CompressedHex4Dec}) | 3081 | (?:#{Regex_8HexLinkLocal}) | 3082 | (?:#{Regex_CompressedHexLinkLocal}) 3083 | /x 3084 | 3085 | ## 3086 | # Creates a new IPv6 address from +arg+ which may be: 3087 | # 3088 | # IPv6:: returns +arg+. 3089 | # String:: +arg+ must match one of the IPv6::Regex* constants 3090 | 3091 | def self.create(arg) 3092 | case arg 3093 | when IPv6 3094 | return arg 3095 | when String 3096 | address = ''.b 3097 | if Regex_8Hex =~ arg 3098 | arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 3099 | elsif Regex_CompressedHex =~ arg 3100 | prefix = $1 3101 | suffix = $2 3102 | a1 = ''.b 3103 | a2 = ''.b 3104 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 3105 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 3106 | omitlen = 16 - a1.length - a2.length 3107 | address << a1 << "\0" * omitlen << a2 3108 | elsif Regex_6Hex4Dec =~ arg 3109 | prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i 3110 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 3111 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 3112 | address << [a, b, c, d].pack('CCCC') 3113 | else 3114 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 3115 | end 3116 | elsif Regex_CompressedHex4Dec =~ arg 3117 | prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i 3118 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 3119 | a1 = ''.b 3120 | a2 = ''.b 3121 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 3122 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 3123 | omitlen = 12 - a1.length - a2.length 3124 | address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC') 3125 | else 3126 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 3127 | end 3128 | else 3129 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 3130 | end 3131 | return IPv6.new(address) 3132 | else 3133 | raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") 3134 | end 3135 | end 3136 | 3137 | def initialize(address) # :nodoc: 3138 | unless address.kind_of?(String) && address.length == 16 3139 | raise ArgumentError.new('IPv6 address must be 16 bytes') 3140 | end 3141 | @address = address 3142 | end 3143 | 3144 | ## 3145 | # The raw IPv6 address as a String. 3146 | 3147 | attr_reader :address 3148 | 3149 | def to_s # :nodoc: 3150 | sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::') 3151 | end 3152 | 3153 | def inspect # :nodoc: 3154 | return "#<#{self.class} #{self}>" 3155 | end 3156 | 3157 | ## 3158 | # Turns this IPv6 address into a Resolv::DNS::Name. 3159 | #-- 3160 | # ip6.arpa should be searched too. [RFC3152] 3161 | 3162 | def to_name 3163 | return DNS::Name.new( 3164 | @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa']) 3165 | end 3166 | 3167 | def ==(other) # :nodoc: 3168 | return @address == other.address 3169 | end 3170 | 3171 | def eql?(other) # :nodoc: 3172 | return self == other 3173 | end 3174 | 3175 | def hash # :nodoc: 3176 | return @address.hash 3177 | end 3178 | end 3179 | 3180 | ## 3181 | # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly 3182 | # makes queries to the mDNS addresses without understanding anything about 3183 | # multicast ports. 3184 | # 3185 | # Information taken form the following places: 3186 | # 3187 | # * RFC 6762 3188 | 3189 | class MDNS < DNS 3190 | 3191 | ## 3192 | # Default mDNS Port 3193 | 3194 | Port = 5353 3195 | 3196 | ## 3197 | # Default IPv4 mDNS address 3198 | 3199 | AddressV4 = '224.0.0.251' 3200 | 3201 | ## 3202 | # Default IPv6 mDNS address 3203 | 3204 | AddressV6 = 'ff02::fb' 3205 | 3206 | ## 3207 | # Default mDNS addresses 3208 | 3209 | Addresses = [ 3210 | [AddressV4, Port], 3211 | [AddressV6, Port], 3212 | ] 3213 | 3214 | ## 3215 | # Creates a new one-shot Multicast DNS (mDNS) resolver. 3216 | # 3217 | # +config_info+ can be: 3218 | # 3219 | # nil:: 3220 | # Uses the default mDNS addresses 3221 | # 3222 | # Hash:: 3223 | # Must contain :nameserver or :nameserver_port like 3224 | # Resolv::DNS#initialize. 3225 | 3226 | def initialize(config_info=nil) 3227 | if config_info then 3228 | super({ nameserver_port: Addresses }.merge(config_info)) 3229 | else 3230 | super(nameserver_port: Addresses) 3231 | end 3232 | end 3233 | 3234 | ## 3235 | # Iterates over all IP addresses for +name+ retrieved from the mDNS 3236 | # resolver, provided name ends with "local". If the name does not end in 3237 | # "local" no records will be returned. 3238 | # 3239 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 3240 | # be a Resolv::IPv4 or Resolv::IPv6 3241 | 3242 | def each_address(name) 3243 | name = Resolv::DNS::Name.create(name) 3244 | 3245 | return unless name[-1].to_s == 'local' 3246 | 3247 | super(name) 3248 | end 3249 | 3250 | def make_udp_requester # :nodoc: 3251 | nameserver_port = @config.nameserver_port 3252 | Requester::MDNSOneShot.new(*nameserver_port) 3253 | end 3254 | 3255 | end 3256 | 3257 | module LOC # :nodoc: 3258 | 3259 | ## 3260 | # A Resolv::LOC::Size 3261 | 3262 | class Size 3263 | 3264 | # Regular expression LOC size must match. 3265 | 3266 | Regex = /^(\d+\.*\d*)[m]$/ 3267 | 3268 | ## 3269 | # Creates a new LOC::Size from +arg+ which may be: 3270 | # 3271 | # LOC::Size:: returns +arg+. 3272 | # String:: +arg+ must match the LOC::Size::Regex constant 3273 | 3274 | def self.create(arg) 3275 | case arg 3276 | when Size 3277 | return arg 3278 | when String 3279 | scalar = '' 3280 | if Regex =~ arg 3281 | 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") 3282 | else 3283 | raise ArgumentError.new("not a properly formed Size string: " + arg) 3284 | end 3285 | return Size.new(scalar) 3286 | else 3287 | raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") 3288 | end 3289 | end 3290 | 3291 | # Internal use; use self.create. 3292 | def initialize(scalar) 3293 | @scalar = scalar 3294 | end 3295 | 3296 | ## 3297 | # The raw size 3298 | 3299 | attr_reader :scalar 3300 | 3301 | def to_s # :nodoc: 3302 | s = @scalar.unpack("H2").join.to_s 3303 | return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m" 3304 | end 3305 | 3306 | def inspect # :nodoc: 3307 | return "#<#{self.class} #{self}>" 3308 | end 3309 | 3310 | def ==(other) # :nodoc: 3311 | return @scalar == other.scalar 3312 | end 3313 | 3314 | def eql?(other) # :nodoc: 3315 | return self == other 3316 | end 3317 | 3318 | def hash # :nodoc: 3319 | return @scalar.hash 3320 | end 3321 | 3322 | end 3323 | 3324 | ## 3325 | # A Resolv::LOC::Coord 3326 | 3327 | class Coord 3328 | 3329 | # Regular expression LOC Coord must match. 3330 | 3331 | Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ 3332 | 3333 | ## 3334 | # Creates a new LOC::Coord from +arg+ which may be: 3335 | # 3336 | # LOC::Coord:: returns +arg+. 3337 | # String:: +arg+ must match the LOC::Coord::Regex constant 3338 | 3339 | def self.create(arg) 3340 | case arg 3341 | when Coord 3342 | return arg 3343 | when String 3344 | coordinates = '' 3345 | if Regex =~ arg && $1.to_f < 180 3346 | m = $~ 3347 | hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1 3348 | coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) + 3349 | (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N") 3350 | orientation = m[4][/[NS]/] ? 'lat' : 'lon' 3351 | else 3352 | raise ArgumentError.new("not a properly formed Coord string: " + arg) 3353 | end 3354 | return Coord.new(coordinates,orientation) 3355 | else 3356 | raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") 3357 | end 3358 | end 3359 | 3360 | # Internal use; use self.create. 3361 | def initialize(coordinates,orientation) 3362 | unless coordinates.kind_of?(String) 3363 | raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") 3364 | end 3365 | unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/] 3366 | raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') 3367 | end 3368 | @coordinates = coordinates 3369 | @orientation = orientation 3370 | end 3371 | 3372 | ## 3373 | # The raw coordinates 3374 | 3375 | attr_reader :coordinates 3376 | 3377 | ## The orientation of the hemisphere as 'lat' or 'lon' 3378 | 3379 | attr_reader :orientation 3380 | 3381 | def to_s # :nodoc: 3382 | c = @coordinates.unpack("N").join.to_i 3383 | val = (c - (2**31)).abs 3384 | fracsecs = (val % 1e3).to_i.to_s 3385 | val = val / 1e3 3386 | secs = (val % 60).to_i.to_s 3387 | val = val / 60 3388 | mins = (val % 60).to_i.to_s 3389 | degs = (val / 60).to_i.to_s 3390 | posi = (c >= 2**31) 3391 | case posi 3392 | when true 3393 | hemi = @orientation[/^lat$/] ? "N" : "E" 3394 | else 3395 | hemi = @orientation[/^lon$/] ? "W" : "S" 3396 | end 3397 | return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi 3398 | end 3399 | 3400 | def inspect # :nodoc: 3401 | return "#<#{self.class} #{self}>" 3402 | end 3403 | 3404 | def ==(other) # :nodoc: 3405 | return @coordinates == other.coordinates 3406 | end 3407 | 3408 | def eql?(other) # :nodoc: 3409 | return self == other 3410 | end 3411 | 3412 | def hash # :nodoc: 3413 | return @coordinates.hash 3414 | end 3415 | 3416 | end 3417 | 3418 | ## 3419 | # A Resolv::LOC::Alt 3420 | 3421 | class Alt 3422 | 3423 | # Regular expression LOC Alt must match. 3424 | 3425 | Regex = /^([+-]*\d+\.*\d*)[m]$/ 3426 | 3427 | ## 3428 | # Creates a new LOC::Alt from +arg+ which may be: 3429 | # 3430 | # LOC::Alt:: returns +arg+. 3431 | # String:: +arg+ must match the LOC::Alt::Regex constant 3432 | 3433 | def self.create(arg) 3434 | case arg 3435 | when Alt 3436 | return arg 3437 | when String 3438 | altitude = '' 3439 | if Regex =~ arg 3440 | altitude = [($1.to_f*(1e2))+(1e7)].pack("N") 3441 | else 3442 | raise ArgumentError.new("not a properly formed Alt string: " + arg) 3443 | end 3444 | return Alt.new(altitude) 3445 | else 3446 | raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") 3447 | end 3448 | end 3449 | 3450 | # Internal use; use self.create. 3451 | def initialize(altitude) 3452 | @altitude = altitude 3453 | end 3454 | 3455 | ## 3456 | # The raw altitude 3457 | 3458 | attr_reader :altitude 3459 | 3460 | def to_s # :nodoc: 3461 | a = @altitude.unpack("N").join.to_i 3462 | return ((a.to_f/1e2)-1e5).to_s + "m" 3463 | end 3464 | 3465 | def inspect # :nodoc: 3466 | return "#<#{self.class} #{self}>" 3467 | end 3468 | 3469 | def ==(other) # :nodoc: 3470 | return @altitude == other.altitude 3471 | end 3472 | 3473 | def eql?(other) # :nodoc: 3474 | return self == other 3475 | end 3476 | 3477 | def hash # :nodoc: 3478 | return @altitude.hash 3479 | end 3480 | 3481 | end 3482 | 3483 | end 3484 | 3485 | ## 3486 | # Default resolver to use for Resolv class methods. 3487 | 3488 | DefaultResolver = self.new 3489 | 3490 | ## 3491 | # Replaces the resolvers in the default resolver with +new_resolvers+. This 3492 | # allows resolvers to be changed for resolv-replace. 3493 | 3494 | def DefaultResolver.replace_resolvers new_resolvers 3495 | @resolvers = new_resolvers 3496 | end 3497 | 3498 | ## 3499 | # Address Regexp to use for matching IP addresses. 3500 | 3501 | AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ 3502 | 3503 | end 3504 | --------------------------------------------------------------------------------