monovm-whois-ruby 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +75 -0
- data/LICENSE +21 -0
- data/README.md +348 -0
- data/data/rdap_bootstrap.json +5337 -0
- data/data/whois_servers.json +1460 -0
- data/exe/monovm-whois +6 -0
- data/lib/monovm/whois/availability/analyzer.rb +91 -0
- data/lib/monovm/whois/availability/context.rb +137 -0
- data/lib/monovm/whois/availability/patterns.rb +415 -0
- data/lib/monovm/whois/availability/rule.rb +57 -0
- data/lib/monovm/whois/availability/rule_set.rb +137 -0
- data/lib/monovm/whois/availability/rules/availability_keywords.rb +32 -0
- data/lib/monovm/whois/availability/rules/explicit_unavailability.rb +43 -0
- data/lib/monovm/whois/availability/rules/no_match.rb +31 -0
- data/lib/monovm/whois/availability/rules/premium_name.rb +35 -0
- data/lib/monovm/whois/availability/rules/rdap_object.rb +94 -0
- data/lib/monovm/whois/availability/rules/recordless.rb +45 -0
- data/lib/monovm/whois/availability/rules/registration_fields.rb +37 -0
- data/lib/monovm/whois/availability/rules/registry_marker.rb +38 -0
- data/lib/monovm/whois/availability/rules/server_refusal.rb +46 -0
- data/lib/monovm/whois/availability/rules/status_field.rb +42 -0
- data/lib/monovm/whois/availability/rules/tld_specific.rb +38 -0
- data/lib/monovm/whois/availability/rules/wrong_registry.rb +48 -0
- data/lib/monovm/whois/availability/verdict.rb +100 -0
- data/lib/monovm/whois/checker.rb +165 -0
- data/lib/monovm/whois/cli.rb +250 -0
- data/lib/monovm/whois/client.rb +227 -0
- data/lib/monovm/whois/configuration.rb +160 -0
- data/lib/monovm/whois/domain_name.rb +168 -0
- data/lib/monovm/whois/endpoint.rb +131 -0
- data/lib/monovm/whois/errors.rb +63 -0
- data/lib/monovm/whois/parser/base.rb +126 -0
- data/lib/monovm/whois/parser/icann_rdd.rb +79 -0
- data/lib/monovm/whois/parser/key_value.rb +169 -0
- data/lib/monovm/whois/parser/rdap_json.rb +170 -0
- data/lib/monovm/whois/parser/record.rb +165 -0
- data/lib/monovm/whois/parser/selector.rb +74 -0
- data/lib/monovm/whois/paths.rb +31 -0
- data/lib/monovm/whois/punycode.rb +206 -0
- data/lib/monovm/whois/referral/follower.rb +90 -0
- data/lib/monovm/whois/registry/definition.rb +119 -0
- data/lib/monovm/whois/registry/resolution.rb +57 -0
- data/lib/monovm/whois/registry/server_registry.rb +164 -0
- data/lib/monovm/whois/registry/sources/base.rb +58 -0
- data/lib/monovm/whois/registry/sources/iana_bootstrap.rb +142 -0
- data/lib/monovm/whois/registry/sources/json_file.rb +137 -0
- data/lib/monovm/whois/response.rb +89 -0
- data/lib/monovm/whois/result.rb +114 -0
- data/lib/monovm/whois/transport/base.rb +51 -0
- data/lib/monovm/whois/transport/factory.rb +51 -0
- data/lib/monovm/whois/transport/middleware/base.rb +55 -0
- data/lib/monovm/whois/transport/middleware/cache.rb +92 -0
- data/lib/monovm/whois/transport/middleware/instrumentation.rb +63 -0
- data/lib/monovm/whois/transport/middleware/retry.rb +56 -0
- data/lib/monovm/whois/transport/middleware/throttle.rb +62 -0
- data/lib/monovm/whois/transport/rdap_http.rb +146 -0
- data/lib/monovm/whois/transport/whois_socket.rb +130 -0
- data/lib/monovm/whois/version.rb +7 -0
- data/lib/monovm/whois/whois_handler.rb +157 -0
- data/lib/monovm/whois.rb +142 -0
- data/lib/monovm-whois-ruby.rb +5 -0
- data/lib/monovm-whois.rb +5 -0
- metadata +114 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rule"
|
|
4
|
+
|
|
5
|
+
module MonoVM
|
|
6
|
+
module Whois
|
|
7
|
+
module Availability
|
|
8
|
+
module Rules
|
|
9
|
+
# The server we reached does not serve this TLD.
|
|
10
|
+
#
|
|
11
|
+
# Two shapes of this exist. A WHOIS server may say so outright ("TLD not
|
|
12
|
+
# supported"), or — more dangerously — a TLD may be mapped to an address
|
|
13
|
+
# registry by mistake. RIPE, APNIC, ARIN and friends answer any domain query
|
|
14
|
+
# with +%ERROR:101: no entries found+, which reads as "no entries found" to a
|
|
15
|
+
# keyword scan and therefore as availability for every name under that TLD.
|
|
16
|
+
#
|
|
17
|
+
# Either way the response describes the server's capabilities, not the
|
|
18
|
+
# domain, so the only honest verdict is +:unknown+.
|
|
19
|
+
class WrongRegistry < Rule
|
|
20
|
+
def call(context)
|
|
21
|
+
return nil if context.empty?
|
|
22
|
+
|
|
23
|
+
# As with {ServerRefusal}: an RDAP document reports this structurally, and
|
|
24
|
+
# its Terms of Service prose is not evidence about the server's coverage.
|
|
25
|
+
return nil unless context.json.nil?
|
|
26
|
+
|
|
27
|
+
matched = context.find(Patterns::UNSUPPORTED)
|
|
28
|
+
if matched
|
|
29
|
+
return unknown(
|
|
30
|
+
reason: "the server does not serve #{context.tld || "this TLD"}",
|
|
31
|
+
evidence: matched
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
banner = context.find(Patterns::WRONG_REGISTRY_BANNERS)
|
|
36
|
+
return nil if banner.nil?
|
|
37
|
+
|
|
38
|
+
unknown(
|
|
39
|
+
reason: "reached an address registry, not the domain registry for " \
|
|
40
|
+
"#{context.tld || "this TLD"}",
|
|
41
|
+
evidence: banner
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MonoVM
|
|
4
|
+
module Whois
|
|
5
|
+
module Availability
|
|
6
|
+
# The outcome of analysing one response.
|
|
7
|
+
#
|
|
8
|
+
# This is the central design decision. With a boolean-only
|
|
9
|
+
# +isAvailable()+, "this domain is registered" and "the
|
|
10
|
+
# server would not tell me" collapse into the same +false+ — and, worse, a
|
|
11
|
+
# response that carries no verdict at all falls through the detector's
|
|
12
|
+
# heuristics and comes back +true+. A rate-limited registry then reports
|
|
13
|
+
# every registered domain as free to register.
|
|
14
|
+
#
|
|
15
|
+
# Here {#status} has four values and +:unknown+ is a real answer that is
|
|
16
|
+
# never promoted to +:available+. {#rule} and {#trace} record *why*, which
|
|
17
|
+
# is what makes a surprising classification debuggable.
|
|
18
|
+
class Verdict
|
|
19
|
+
STATUSES = %i[available registered premium unknown].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :status, :rule, :reason, :evidence, :trace
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
STATUSES.each do |status|
|
|
25
|
+
define_method(status) do |rule: nil, reason: nil, evidence: nil, trace: nil|
|
|
26
|
+
new(status: status, rule: rule, reason: reason, evidence: evidence, trace: trace)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param status [Symbol] one of {STATUSES}
|
|
32
|
+
# @param rule [String, nil] name of the rule that decided it
|
|
33
|
+
# @param reason [String, nil] human-readable justification
|
|
34
|
+
# @param evidence [String, nil] the matched text, trimmed for display
|
|
35
|
+
# @param trace [Array<Hash>, nil] every rule consulted, in order
|
|
36
|
+
def initialize(status:, rule: nil, reason: nil, evidence: nil, trace: nil)
|
|
37
|
+
raise ArgumentError, "unknown verdict status #{status.inspect}" unless STATUSES.include?(status)
|
|
38
|
+
|
|
39
|
+
@status = status
|
|
40
|
+
@rule = rule
|
|
41
|
+
@reason = reason
|
|
42
|
+
@evidence = evidence
|
|
43
|
+
@trace = (trace || []).freeze
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def available?
|
|
48
|
+
status == :available
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def registered?
|
|
52
|
+
status == :registered
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def premium?
|
|
56
|
+
status == :premium
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# True when no rule could reach a conclusion, or when the server refused
|
|
60
|
+
# to answer. Callers must treat this as "ask again later", never as free.
|
|
61
|
+
def unknown?
|
|
62
|
+
status == :unknown
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# True when the verdict says something actionable about the domain.
|
|
66
|
+
def conclusive?
|
|
67
|
+
!unknown?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Return a copy carrying +trace+. Rules build verdicts without knowing the
|
|
71
|
+
# trace; the analyzer attaches it once the chain finishes.
|
|
72
|
+
def with_trace(trace)
|
|
73
|
+
self.class.new(status: status, rule: rule, reason: reason, evidence: evidence, trace: trace)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{
|
|
78
|
+
status: status,
|
|
79
|
+
rule: rule,
|
|
80
|
+
reason: reason,
|
|
81
|
+
evidence: evidence
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ==(other)
|
|
86
|
+
other.is_a?(Verdict) && other.status == status && other.rule == rule
|
|
87
|
+
end
|
|
88
|
+
alias eql? ==
|
|
89
|
+
|
|
90
|
+
def hash
|
|
91
|
+
[self.class, status, rule].hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def inspect
|
|
95
|
+
"#<#{self.class.name} #{status}#{" by #{rule}" if rule}>"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client"
|
|
4
|
+
require_relative "configuration"
|
|
5
|
+
require_relative "domain_name"
|
|
6
|
+
|
|
7
|
+
module MonoVM
|
|
8
|
+
module Whois
|
|
9
|
+
# Checks many domains, and expands a bare name across popular TLDs.
|
|
10
|
+
#
|
|
11
|
+
# Two jobs the single-domain {Client} deliberately does not do. Given +"monovm"+
|
|
12
|
+
# with no TLD it tries +.com+, +.net+, +.org+ and +.info+; given a list it works
|
|
13
|
+
# through it concurrently.
|
|
14
|
+
#
|
|
15
|
+
# Concurrency here is safe only because {Transport::Middleware::Throttle} sits
|
|
16
|
+
# underneath it: eight threads against a list of +.com+ names would otherwise send
|
|
17
|
+
# eight simultaneous queries to one Verisign host and get the caller's IP
|
|
18
|
+
# rate-limited, after which every remaining answer is a refusal. The throttle is
|
|
19
|
+
# per host, so a batch spanning many registries still runs genuinely in parallel.
|
|
20
|
+
#
|
|
21
|
+
# MonoVM::Whois::Checker.whois("monovm.com")
|
|
22
|
+
# # => {"monovm.com" => :registered}
|
|
23
|
+
#
|
|
24
|
+
# MonoVM::Whois::Checker.whois(["monovm", "google.com"])
|
|
25
|
+
# # => {"monovm.com" => :registered, "monovm.net" => :registered, ...}
|
|
26
|
+
class Checker
|
|
27
|
+
class << self
|
|
28
|
+
# @param domains [String, Enumerable<String>]
|
|
29
|
+
# @param options [Hash] +:popular_tlds+ (or +:popularTLDs+), +:concurrency+,
|
|
30
|
+
# plus anything {Configuration} accepts
|
|
31
|
+
# @return [Hash{String => Symbol}] name => status
|
|
32
|
+
def whois(domains, options = {})
|
|
33
|
+
new(**build_options(options)).check(domains)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# As {.whois} but each value is the full {Result}.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{String => Result}]
|
|
39
|
+
def lookup(domains, options = {})
|
|
40
|
+
new(**build_options(options)).check_detailed(domains)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Accept the camelCase alias key alongside the Ruby one, and route
|
|
46
|
+
# everything else at the configuration.
|
|
47
|
+
def build_options(options)
|
|
48
|
+
options = options.to_h.dup
|
|
49
|
+
popular = options.delete(:popular_tlds) || options.delete(:popularTLDs)
|
|
50
|
+
concurrency = options.delete(:concurrency)
|
|
51
|
+
|
|
52
|
+
config = Configuration.new
|
|
53
|
+
options.each do |key, value|
|
|
54
|
+
setter = "#{key}="
|
|
55
|
+
raise ArgumentError, "unknown option #{key.inspect}" unless config.respond_to?(setter)
|
|
56
|
+
|
|
57
|
+
config.public_send(setter, value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ config: config, popular_tlds: popular, concurrency: concurrency }.compact
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :client, :popular_tlds, :concurrency
|
|
65
|
+
|
|
66
|
+
def initialize(client: nil, config: nil, popular_tlds: nil, concurrency: nil)
|
|
67
|
+
@config = config || Configuration.new
|
|
68
|
+
@client = client || Client.new(config: @config)
|
|
69
|
+
@popular_tlds = normalise_tlds(popular_tlds || @config.popular_tlds)
|
|
70
|
+
@concurrency = (concurrency || @config.concurrency).to_i.clamp(1, 64)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Hash{String => Symbol}]
|
|
74
|
+
def check(domains)
|
|
75
|
+
check_detailed(domains).transform_values(&:status)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Hash{String => Result}]
|
|
79
|
+
def check_detailed(domains)
|
|
80
|
+
targets = expand(domains)
|
|
81
|
+
return {} if targets.empty?
|
|
82
|
+
|
|
83
|
+
results = run(targets)
|
|
84
|
+
|
|
85
|
+
# Return in the order asked, which is what a caller rendering a table needs.
|
|
86
|
+
targets.to_h { |name| [name.to_s, results[name.to_s]] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def normalise_tlds(tlds)
|
|
92
|
+
list = tlds.is_a?(String) ? tlds.split(",") : Array(tlds)
|
|
93
|
+
normalised = list.filter_map do |tld|
|
|
94
|
+
cleaned = tld.to_s.strip.downcase.delete_prefix(".")
|
|
95
|
+
cleaned.empty? ? nil : ".#{cleaned}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
raise ArgumentError, "popular_tlds must not be empty" if normalised.empty?
|
|
99
|
+
|
|
100
|
+
normalised
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# One input may become several names, and duplicates are looked up once.
|
|
104
|
+
def expand(domains)
|
|
105
|
+
inputs = domains.is_a?(String) ? [domains] : Array(domains)
|
|
106
|
+
|
|
107
|
+
inputs.flat_map { |input| candidates_for(input) }.uniq(&:to_s)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def candidates_for(input)
|
|
111
|
+
raise ArgumentError, "every domain must be a String, got #{input.class}" unless input.is_a?(String)
|
|
112
|
+
|
|
113
|
+
name = DomainName.parse(input)
|
|
114
|
+
|
|
115
|
+
# An unusable name is passed through untouched so the caller sees it reported
|
|
116
|
+
# as :invalid under the string they supplied. Expanding it across the popular
|
|
117
|
+
# TLDs first would turn one bad entry into four confusing ones.
|
|
118
|
+
return [name] unless name.valid?
|
|
119
|
+
return [name] unless name.bare?
|
|
120
|
+
|
|
121
|
+
popular_tlds.map { |tld| name.join(tld) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def run(targets)
|
|
125
|
+
return sequential(targets) if concurrency == 1 || targets.length == 1
|
|
126
|
+
|
|
127
|
+
queue = Queue.new
|
|
128
|
+
targets.each { |name| queue << name }
|
|
129
|
+
|
|
130
|
+
results = {}
|
|
131
|
+
mutex = Mutex.new
|
|
132
|
+
|
|
133
|
+
workers = Array.new([concurrency, targets.length].min) do
|
|
134
|
+
Thread.new do
|
|
135
|
+
while (name = pop(queue))
|
|
136
|
+
result = safely(name) { client.lookup(name) }
|
|
137
|
+
mutex.synchronize { results[name.to_s] = result }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
workers.each(&:join)
|
|
143
|
+
results
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sequential(targets)
|
|
147
|
+
targets.to_h { |name| [name.to_s, safely(name) { client.lookup(name) }] }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def pop(queue)
|
|
151
|
+
queue.pop(true)
|
|
152
|
+
rescue ThreadError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# One unexpected failure must not lose the other 499 answers in a batch, and it
|
|
157
|
+
# must not be reported as availability either.
|
|
158
|
+
def safely(name)
|
|
159
|
+
yield
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
Result.unknown(name, reason: "lookup raised #{e.class}: #{e.message}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../whois"
|
|
6
|
+
|
|
7
|
+
module MonoVM
|
|
8
|
+
module Whois
|
|
9
|
+
# The +monovm-whois+ command.
|
|
10
|
+
#
|
|
11
|
+
# Kept as a class taking explicit +stdout+/+stderr+ and returning an exit code
|
|
12
|
+
# rather than calling +exit+ itself, so the specs can run it in-process and assert
|
|
13
|
+
# on its output.
|
|
14
|
+
#
|
|
15
|
+
# Exit codes: 0 when every name got a real answer, 1 when any came back unknown or
|
|
16
|
+
# invalid, 2 for a usage error.
|
|
17
|
+
class CLI
|
|
18
|
+
EXIT_OK = 0
|
|
19
|
+
EXIT_INCONCLUSIVE = 1
|
|
20
|
+
EXIT_USAGE = 2
|
|
21
|
+
|
|
22
|
+
# Terminal colours, skipped when output is redirected.
|
|
23
|
+
COLOURS = {
|
|
24
|
+
available: "\e[32m", # green
|
|
25
|
+
registered: "\e[31m", # red
|
|
26
|
+
premium: "\e[33m", # yellow
|
|
27
|
+
unknown: "\e[35m", # magenta
|
|
28
|
+
invalid: "\e[90m" # grey
|
|
29
|
+
}.freeze
|
|
30
|
+
RESET = "\e[0m"
|
|
31
|
+
|
|
32
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
33
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(stdout: $stdout, stderr: $stderr)
|
|
37
|
+
@stdout = stdout
|
|
38
|
+
@stderr = stderr
|
|
39
|
+
@options = default_options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(argv)
|
|
43
|
+
domains = parser.parse(argv)
|
|
44
|
+
|
|
45
|
+
return print_help if @options[:help]
|
|
46
|
+
return print_version if @options[:version]
|
|
47
|
+
return print_tld_count if @options[:tld_count]
|
|
48
|
+
return usage_error("give at least one domain name") if domains.empty?
|
|
49
|
+
|
|
50
|
+
results = check(domains)
|
|
51
|
+
render(results)
|
|
52
|
+
exit_code(results)
|
|
53
|
+
rescue OptionParser::ParseError => e
|
|
54
|
+
usage_error(e.message)
|
|
55
|
+
rescue Error => e
|
|
56
|
+
@stderr.puts "monovm-whois: #{e.message}"
|
|
57
|
+
EXIT_INCONCLUSIVE
|
|
58
|
+
rescue Interrupt
|
|
59
|
+
@stderr.puts "monovm-whois: interrupted"
|
|
60
|
+
EXIT_INCONCLUSIVE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def default_options
|
|
66
|
+
{
|
|
67
|
+
format: :text,
|
|
68
|
+
details: false,
|
|
69
|
+
raw: false,
|
|
70
|
+
prefer: :rdap,
|
|
71
|
+
cache: true,
|
|
72
|
+
concurrency: nil,
|
|
73
|
+
timeout: nil,
|
|
74
|
+
popular_tlds: nil,
|
|
75
|
+
colour: nil
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Memoised: the option handlers close over @options, and building a second
|
|
80
|
+
# parser to print usage would be wasteful and easy to let drift.
|
|
81
|
+
def parser
|
|
82
|
+
@parser ||= OptionParser.new do |opts|
|
|
83
|
+
opts.banner = "Usage: monovm-whois DOMAIN [DOMAIN...] [options]"
|
|
84
|
+
opts.separator ""
|
|
85
|
+
opts.separator "Checks domain availability over RDAP and WHOIS."
|
|
86
|
+
opts.separator ""
|
|
87
|
+
|
|
88
|
+
opts.on("-j", "--json", "Emit JSON instead of a table") { @options[:format] = :json }
|
|
89
|
+
opts.on("-d", "--details", "Show which rule decided, and why") { @options[:details] = true }
|
|
90
|
+
opts.on("-r", "--raw", "Print the raw registry response") { @options[:raw] = true }
|
|
91
|
+
|
|
92
|
+
opts.on("--prefer PROTOCOL", %w[rdap whois],
|
|
93
|
+
"Try rdap or whois first (default: rdap)") do |value|
|
|
94
|
+
@options[:prefer] = value.to_sym
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
opts.on("-t", "--timeout SECONDS", Float, "Per-request timeout") do |value|
|
|
98
|
+
@options[:timeout] = value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
opts.on("-c", "--concurrency N", Integer, "Parallel lookups (default: 8)") do |value|
|
|
102
|
+
@options[:concurrency] = value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
opts.on("--tlds LIST", "TLDs to try for a name with no TLD",
|
|
106
|
+
"(default: .com,.net,.org,.info)") do |value|
|
|
107
|
+
@options[:popular_tlds] = value.split(",")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
opts.on("--[no-]cache", "Cache responses in-process (default: on)") do |value|
|
|
111
|
+
@options[:cache] = value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
opts.on("--[no-]color", "--[no-]colour", "Colourise the status column") do |value|
|
|
115
|
+
@options[:colour] = value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
opts.on("--tld-count", "Print how many TLDs are supported, then exit") do
|
|
119
|
+
@options[:tld_count] = true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts.on("-v", "--version", "Print the version, then exit") do
|
|
123
|
+
@options[:version] = true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
opts.on("-h", "--help", "Print this message") { @options[:help] = true }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def print_help
|
|
131
|
+
@stdout.puts parser
|
|
132
|
+
EXIT_OK
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def print_version
|
|
136
|
+
@stdout.puts "monovm-whois #{MonoVM::Whois::VERSION}"
|
|
137
|
+
EXIT_OK
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def print_tld_count
|
|
141
|
+
registry = Configuration.new.server_registry
|
|
142
|
+
@stdout.puts "#{registry.size} TLDs supported"
|
|
143
|
+
EXIT_OK
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def check(domains)
|
|
147
|
+
Checker.new(
|
|
148
|
+
config: build_config,
|
|
149
|
+
popular_tlds: @options[:popular_tlds],
|
|
150
|
+
concurrency: @options[:concurrency]
|
|
151
|
+
).check_detailed(domains)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_config
|
|
155
|
+
config = Configuration.new
|
|
156
|
+
config.prefer = @options[:prefer]
|
|
157
|
+
config.cache = @options[:cache]
|
|
158
|
+
|
|
159
|
+
if @options[:timeout]
|
|
160
|
+
timeout = @options[:timeout]
|
|
161
|
+
config.socket_connect_timeout = timeout
|
|
162
|
+
config.socket_read_timeout = timeout
|
|
163
|
+
config.http_open_timeout = timeout
|
|
164
|
+
config.http_read_timeout = timeout
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
config
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def render(results)
|
|
171
|
+
case @options[:format]
|
|
172
|
+
when :json then render_json(results)
|
|
173
|
+
else render_text(results)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def render_json(results)
|
|
178
|
+
payload = results.transform_values do |result|
|
|
179
|
+
entry = result.to_h
|
|
180
|
+
entry[:trace] = result.verdict&.trace if @options[:details]
|
|
181
|
+
entry[:raw] = result.whois_message if @options[:raw]
|
|
182
|
+
entry
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
@stdout.puts JSON.pretty_generate(payload)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_text(results)
|
|
189
|
+
width = results.keys.map(&:length).max.to_i
|
|
190
|
+
|
|
191
|
+
results.each do |name, result|
|
|
192
|
+
@stdout.puts "#{name.ljust(width)} #{colourise(result.status)}"
|
|
193
|
+
next unless @options[:details]
|
|
194
|
+
|
|
195
|
+
@stdout.puts "#{" " * width} decided by: #{result.verdict&.rule || "-"}"
|
|
196
|
+
@stdout.puts "#{" " * width} reason: #{result.reason}"
|
|
197
|
+
@stdout.puts "#{" " * width} endpoint: #{result.response&.endpoint || "-"}"
|
|
198
|
+
render_record(result, width)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
render_raw(results) if @options[:raw]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def render_record(result, width)
|
|
205
|
+
record = result.record
|
|
206
|
+
return if record.nil? || record.empty?
|
|
207
|
+
|
|
208
|
+
pad = " " * width
|
|
209
|
+
@stdout.puts "#{pad} registrar: #{record.registrar}" if record.registrar
|
|
210
|
+
@stdout.puts "#{pad} created: #{record.created_on&.iso8601}" if record.created_on
|
|
211
|
+
@stdout.puts "#{pad} expires: #{record.expires_on&.iso8601}" if record.expires_on
|
|
212
|
+
return if record.nameservers.empty?
|
|
213
|
+
|
|
214
|
+
@stdout.puts "#{pad} nameservers: #{record.nameservers.join(", ")}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def render_raw(results)
|
|
218
|
+
results.each do |name, result|
|
|
219
|
+
@stdout.puts
|
|
220
|
+
@stdout.puts "=== #{name} ==="
|
|
221
|
+
@stdout.puts result.whois_message
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def colourise(status)
|
|
226
|
+
return status.to_s unless colour?
|
|
227
|
+
|
|
228
|
+
"#{COLOURS.fetch(status, "")}#{status}#{RESET}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def colour?
|
|
232
|
+
return @options[:colour] unless @options[:colour].nil?
|
|
233
|
+
|
|
234
|
+
@stdout.respond_to?(:tty?) && @stdout.tty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Anything not conclusively answered is worth a non-zero exit, so a shell script
|
|
238
|
+
# can tell "definitely free" from "could not find out".
|
|
239
|
+
def exit_code(results)
|
|
240
|
+
results.each_value.all?(&:conclusive?) ? EXIT_OK : EXIT_INCONCLUSIVE
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def usage_error(message)
|
|
244
|
+
@stderr.puts "monovm-whois: #{message}"
|
|
245
|
+
@stderr.puts parser
|
|
246
|
+
EXIT_USAGE
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|