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