module DomainProbe
DomainProbe — local whois and dig subprocess probes for a registrable domain. Public: probe, parse_whois, parse_dig, build_whois_argv, build_dig_argv, shell_safe_domain? Tests: test/lib/domain_probe_test.rb
Constants
- DIG_BIN
- SHELL_SAFE_DOMAIN_RE
- WHOIS_AVAILABLE_RES
- WHOIS_BIN
- WHOIS_CREATED_RES
- WHOIS_EXPIRY_RES
- WHOIS_NAMESERVER_RE
- WHOIS_REGISTRAR_RE
- WHOIS_STATUS_RE
Public Instance Methods
Source
# File lib/domain_probe.rb, line 58 def build_dig_argv(domain) host = domain.to_s [DIG_BIN, "+short", "+time=2", host, "A", host, "AAAA", host, "NS", host, "MX"] end
Source
# File lib/domain_probe.rb, line 54 def build_whois_argv(domain) [WHOIS_BIN, domain.to_s] end
Source
# File lib/domain_probe.rb, line 211 def clean_whois_value(value) cleaned = value.to_s.strip return nil if cleaned.empty? cleaned.split(/\s+http/i).first.to_s.strip end
Source
# File lib/domain_probe.rb, line 179 def command_error_message(tool, text, status) message = text.to_s.strip message = "#{tool} failed" if message.empty? && !status.success? message = "#{tool}: command not found" if message.match?(/command not found/i) message end
Source
# File lib/domain_probe.rb, line 146 def command_failed?(text, status) return true if text.match?(/command not found/i) return true if text.match?(/not found:\s*#{Regexp.escape(WHOIS_BIN)}/i) return true if text.match?(/not found:\s*#{Regexp.escape(DIG_BIN)}/i) !status.success? && text.strip.empty? end
Source
# File lib/domain_probe.rb, line 228 def default_dig_run lambda do |argv| Open3.capture3(*argv) end end
Source
# File lib/domain_probe.rb, line 222 def default_whois_run lambda do |argv| Open3.capture3(*argv) end end
Source
# File lib/domain_probe.rb, line 167 def dig_command_error(text, status) { ok: false, a: [], aaaa: [], ns: [], mx: [], nxdomain: false, error: command_error_message(:dig, text, status) } end
Source
# File lib/domain_probe.rb, line 204 def extract_all_matches(text, pattern) text.to_s.each_line.filter_map do |line| match = line.strip.match(pattern) match[:value].strip if match end end
Source
# File lib/domain_probe.rb, line 186 def extract_first_match(text, pattern) text.to_s.each_line do |line| match = line.strip.match(pattern) return match[:value].strip if match end nil end
Source
# File lib/domain_probe.rb, line 194 def extract_first_res(text, patterns) patterns.each do |pattern| text.to_s.each_line do |line| match = line.strip.match(pattern) return match[:value].strip if match end end nil end
Source
# File lib/domain_probe.rb, line 240 def failure_status status = Object.new status.define_singleton_method(:success?) { false } status end
Source
# File lib/domain_probe.rb, line 218 def nxdomain_hint?(text) text.match?(/NXDOMAIN/i) end
Source
# File lib/domain_probe.rb, line 111 def parse_dig(output, status: success_status, nxdomain: nil) text = output.to_s return dig_command_error(text, status) if command_failed?(text, status) lines = text.lines.map { |line| line.strip }.reject(&:empty?) a = [] aaaa = [] ns = [] mx = [] lines.each do |line| case line when /\A\d+\.\d+\.\d+\.\d+\z/ a << line when /\A[0-9a-f:]+\z/i aaaa << line if line.include?(":") when /\.\z/ ns << line if line.match?(/[a-z]/i) when /\A\d+\s/ mx << line end end nx = nxdomain.nil? ? (a.empty? && aaaa.empty? && ns.empty? && mx.empty? && nxdomain_hint?(text)) : nxdomain { ok: true, a: a.uniq.first(4), aaaa: aaaa.uniq.first(2), ns: ns.uniq.first(4), mx: mx.uniq.first(2), nxdomain: nx, error: nil } end
Source
# File lib/domain_probe.rb, line 87 def parse_whois(output, status: success_status) text = output.to_s return whois_command_error(text, status) if command_failed?(text, status) available = WHOIS_AVAILABLE_RES.any? { |pattern| text.match?(pattern) } registrar = extract_first_match(text, WHOIS_REGISTRAR_RE) expires = extract_first_res(text, WHOIS_EXPIRY_RES) created = extract_first_res(text, WHOIS_CREATED_RES) nameservers = extract_all_matches(text, WHOIS_NAMESERVER_RE).map { |row| row.upcase }.uniq.first(4) status_line = extract_first_match(text, WHOIS_STATUS_RE) registered = !available && (!registrar.to_s.strip.empty? || nameservers.any? || !created.to_s.strip.empty?) { ok: true, registered: registered, registrar: clean_whois_value(registrar), created: clean_whois_value(created), expires: clean_whois_value(expires), nameservers: nameservers, status: available ? "available" : status_line, error: nil } end
Source
# File lib/domain_probe.rb, line 63 def probe(domain, whois_run: default_whois_run, dig_run: default_dig_run) host = domain.to_s.downcase raise ArgumentError, "unsafe domain" unless shell_safe_domain?(host) whois_raw, whois_status = run_command(build_whois_argv(host), run: whois_run) dig_raw, dig_status = run_command(build_dig_argv(host), run: dig_run) { domain: host, whois: parse_whois(whois_raw, status: whois_status), dig: parse_dig(dig_raw, status: dig_status) } end
Source
# File lib/domain_probe.rb, line 77 def run_command(argv, run:) stdout, stderr, status = run.call(argv) output = [stdout, stderr].map(&:to_s).join("\n").strip [output, status] rescue Errno::ENOENT ["", "command not found: #{argv.first}", failure_status] rescue StandardError => e ["", e.message.to_s, failure_status] end
Source
# File lib/domain_probe.rb, line 50 def shell_safe_domain?(domain) domain.to_s.match?(SHELL_SAFE_DOMAIN_RE) end
Source
# File lib/domain_probe.rb, line 234 def success_status status = Object.new status.define_singleton_method(:success?) { true } status end
Source
# File lib/domain_probe.rb, line 154 def whois_command_error(text, status) { ok: false, registered: nil, registrar: nil, created: nil, expires: nil, nameservers: [], status: nil, error: command_error_message(:whois, text, status) } end