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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +75 -0
  3. data/LICENSE +21 -0
  4. data/README.md +348 -0
  5. data/data/rdap_bootstrap.json +5337 -0
  6. data/data/whois_servers.json +1460 -0
  7. data/exe/monovm-whois +6 -0
  8. data/lib/monovm/whois/availability/analyzer.rb +91 -0
  9. data/lib/monovm/whois/availability/context.rb +137 -0
  10. data/lib/monovm/whois/availability/patterns.rb +415 -0
  11. data/lib/monovm/whois/availability/rule.rb +57 -0
  12. data/lib/monovm/whois/availability/rule_set.rb +137 -0
  13. data/lib/monovm/whois/availability/rules/availability_keywords.rb +32 -0
  14. data/lib/monovm/whois/availability/rules/explicit_unavailability.rb +43 -0
  15. data/lib/monovm/whois/availability/rules/no_match.rb +31 -0
  16. data/lib/monovm/whois/availability/rules/premium_name.rb +35 -0
  17. data/lib/monovm/whois/availability/rules/rdap_object.rb +94 -0
  18. data/lib/monovm/whois/availability/rules/recordless.rb +45 -0
  19. data/lib/monovm/whois/availability/rules/registration_fields.rb +37 -0
  20. data/lib/monovm/whois/availability/rules/registry_marker.rb +38 -0
  21. data/lib/monovm/whois/availability/rules/server_refusal.rb +46 -0
  22. data/lib/monovm/whois/availability/rules/status_field.rb +42 -0
  23. data/lib/monovm/whois/availability/rules/tld_specific.rb +38 -0
  24. data/lib/monovm/whois/availability/rules/wrong_registry.rb +48 -0
  25. data/lib/monovm/whois/availability/verdict.rb +100 -0
  26. data/lib/monovm/whois/checker.rb +165 -0
  27. data/lib/monovm/whois/cli.rb +250 -0
  28. data/lib/monovm/whois/client.rb +227 -0
  29. data/lib/monovm/whois/configuration.rb +160 -0
  30. data/lib/monovm/whois/domain_name.rb +168 -0
  31. data/lib/monovm/whois/endpoint.rb +131 -0
  32. data/lib/monovm/whois/errors.rb +63 -0
  33. data/lib/monovm/whois/parser/base.rb +126 -0
  34. data/lib/monovm/whois/parser/icann_rdd.rb +79 -0
  35. data/lib/monovm/whois/parser/key_value.rb +169 -0
  36. data/lib/monovm/whois/parser/rdap_json.rb +170 -0
  37. data/lib/monovm/whois/parser/record.rb +165 -0
  38. data/lib/monovm/whois/parser/selector.rb +74 -0
  39. data/lib/monovm/whois/paths.rb +31 -0
  40. data/lib/monovm/whois/punycode.rb +206 -0
  41. data/lib/monovm/whois/referral/follower.rb +90 -0
  42. data/lib/monovm/whois/registry/definition.rb +119 -0
  43. data/lib/monovm/whois/registry/resolution.rb +57 -0
  44. data/lib/monovm/whois/registry/server_registry.rb +164 -0
  45. data/lib/monovm/whois/registry/sources/base.rb +58 -0
  46. data/lib/monovm/whois/registry/sources/iana_bootstrap.rb +142 -0
  47. data/lib/monovm/whois/registry/sources/json_file.rb +137 -0
  48. data/lib/monovm/whois/response.rb +89 -0
  49. data/lib/monovm/whois/result.rb +114 -0
  50. data/lib/monovm/whois/transport/base.rb +51 -0
  51. data/lib/monovm/whois/transport/factory.rb +51 -0
  52. data/lib/monovm/whois/transport/middleware/base.rb +55 -0
  53. data/lib/monovm/whois/transport/middleware/cache.rb +92 -0
  54. data/lib/monovm/whois/transport/middleware/instrumentation.rb +63 -0
  55. data/lib/monovm/whois/transport/middleware/retry.rb +56 -0
  56. data/lib/monovm/whois/transport/middleware/throttle.rb +62 -0
  57. data/lib/monovm/whois/transport/rdap_http.rb +146 -0
  58. data/lib/monovm/whois/transport/whois_socket.rb +130 -0
  59. data/lib/monovm/whois/version.rb +7 -0
  60. data/lib/monovm/whois/whois_handler.rb +157 -0
  61. data/lib/monovm/whois.rb +142 -0
  62. data/lib/monovm-whois-ruby.rb +5 -0
  63. data/lib/monovm-whois.rb +5 -0
  64. 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