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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5f8f480691e87bc25b8579f2392661362c8ad7a84d0931ca7c9140d032b4ca53
|
|
4
|
+
data.tar.gz: 7cf7b4eacb5a979835d74369c4b916f1222461c4fdfb0ac8b13bb0995b6f2d8c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91346d50c1234e0acb4a4c46dd1dec4486cf953560c43d3dd74e3ab7e097e5cd9f81df3673cc73dbd0e5aeab93877e72d5680edd23168c1d4db16c3538563754
|
|
7
|
+
data.tar.gz: b8543cd285fc1a074b6d56af85b9d01732f9b31f5614b69766e9965a0ee7443c8d4ab77f39a5b861cded6f70ea076b9561521af718642420e9eb92d1552bb340
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-08-05
|
|
9
|
+
|
|
10
|
+
First release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Four-state availability verdict.** `:available`, `:registered`, `:premium` and
|
|
15
|
+
`:unknown`, plus `:invalid` on `Result` for names that never reached a server.
|
|
16
|
+
`:unknown` is a real answer and is never promoted to `:available`.
|
|
17
|
+
- **RDAP support, preferred over WHOIS.** TLD → RDAP endpoint mapping from a bundled
|
|
18
|
+
snapshot of the IANA bootstrap registry (RFC 7484, ~1,200 TLDs). A registered domain
|
|
19
|
+
is read from `objectClassName`, an unregistered one from `errorCode` 404, so no
|
|
20
|
+
pattern matching is involved. Port 43 remains the fallback.
|
|
21
|
+
- **Availability detection as an extensible rule chain.** Twelve ordered rules, each an
|
|
22
|
+
object returning a verdict or deferring. `RuleSet` supports `prepend`, `append`,
|
|
23
|
+
`insert_before`, `insert_after`, `replace` and `remove`, so a registry's new wording
|
|
24
|
+
needs one small class rather than a fork.
|
|
25
|
+
- **Structured record parsing.** `Parser::Record` exposes registrar, registrant,
|
|
26
|
+
creation/update/expiry dates, nameservers, EPP statuses, DNSSEC and contacts, from
|
|
27
|
+
three parsers (RDAP JSON, ICANN RDD, generic key/value) selected per response. Raw
|
|
28
|
+
fields stay reachable. GDPR redaction placeholders are reported as `nil`.
|
|
29
|
+
- **Registrar referral following.** Thin registries are chased one hop to the
|
|
30
|
+
registrar's server for the fuller record. The referral enriches the record only; the
|
|
31
|
+
registry keeps authority over the verdict.
|
|
32
|
+
- **Concurrent bulk checks** with per-host throttling, so a large single-TLD batch does
|
|
33
|
+
not trigger a registry rate limit.
|
|
34
|
+
- **Transport middleware**: in-process response cache, per-host throttle, retry for
|
|
35
|
+
transient failures only, and an instrumentation hook.
|
|
36
|
+
- **Punycode / IDN support** via an RFC 3492 implementation with no dependencies,
|
|
37
|
+
verified against the RFC test vectors and the published IDN ccTLD ACE forms.
|
|
38
|
+
- **`explain`** on the client, the module and the handler, returning the deciding rule,
|
|
39
|
+
its evidence and the full trace of every rule consulted.
|
|
40
|
+
- **CLI** `monovm-whois`, with `--json`, `--details`, `--raw`, `--prefer`, `--timeout`,
|
|
41
|
+
`--concurrency`, `--tlds`, `--[no-]cache`, `--[no-]colour` and `--tld-count`. Exits
|
|
42
|
+
non-zero when any name came back inconclusive.
|
|
43
|
+
- **`WhoisHandler`**, a single-domain handler object, with camelCase method aliases
|
|
44
|
+
(`isAvailable`, `getWhoisMessage`, …) for code migrating from camelCase WHOIS APIs.
|
|
45
|
+
- **Definition overrides** through `MONOVM_WHOIS_DEFINITIONS`, so a stale registry
|
|
46
|
+
entry can be corrected without a gem release.
|
|
47
|
+
- Error taxonomy under `MonoVM::Whois::Error`: `InvalidDomainError`,
|
|
48
|
+
`UnsupportedTldError`, `DefinitionsError`, `ConnectionError`, `TimeoutError`,
|
|
49
|
+
`ServerRefusedError`, `EmptyResponseError`.
|
|
50
|
+
|
|
51
|
+
### Fail-safe design decisions
|
|
52
|
+
|
|
53
|
+
Every choice points the same way — declining to guess where a permissive heuristic
|
|
54
|
+
would answer "available". See the table in the README for the full list. The main ones:
|
|
55
|
+
|
|
56
|
+
- Rate limits, blocked clients, retired port 43 endpoints, HTTP 4xx/5xx and empty
|
|
57
|
+
responses are `:unknown`, not `:available`.
|
|
58
|
+
- Reaching an address registry (RIPE, ARIN, APNIC) by a misconfigured TLD mapping is
|
|
59
|
+
`:unknown`, not `:available` for every name in that TLD.
|
|
60
|
+
- "Fewer than two registration fields" is no longer treated as availability. That
|
|
61
|
+
inference converts any non-answer into a free domain. It survives only as the
|
|
62
|
+
`recordless` rule, opt-in per TLD via `available_when_empty`.
|
|
63
|
+
- DENIC's `Status: invalid` is `:registered`, not `:available`.
|
|
64
|
+
- Premium and reserved names are `:premium`, not `:available`.
|
|
65
|
+
- Field matching tolerates dot-padded keys (`status.........: Registered`), which
|
|
66
|
+
several registries emit and a plain `status:` match misses.
|
|
67
|
+
- TLS certificates are verified by default.
|
|
68
|
+
|
|
69
|
+
### Notes
|
|
70
|
+
|
|
71
|
+
- Requires Ruby 3.1 or newer. No runtime dependencies.
|
|
72
|
+
- The bundled RDAP bootstrap snapshot was taken 2026-07-23 and can be refreshed with
|
|
73
|
+
`rake data:refresh_rdap`.
|
|
74
|
+
|
|
75
|
+
[1.0.0]: https://github.com/monovm/whois-ruby/releases/tag/v1.0.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MonoVM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# monovm-whois-ruby
|
|
2
|
+
|
|
3
|
+
Domain WHOIS and RDAP lookups for Ruby, with availability detection that says
|
|
4
|
+
"I don't know" instead of guessing.
|
|
5
|
+
|
|
6
|
+
No runtime dependencies.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
require "monovm-whois-ruby"
|
|
10
|
+
|
|
11
|
+
MonoVM::Whois.available?("monovm.com") # => false
|
|
12
|
+
MonoVM::Whois.lookup("monovm.com").record.expires_on
|
|
13
|
+
MonoVM::Whois.whois(%w[monovm google.com])
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "monovm-whois-ruby"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
or
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install monovm-whois-ruby
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Ruby 3.1 or newer.
|
|
29
|
+
|
|
30
|
+
## Why the availability status has four values
|
|
31
|
+
|
|
32
|
+
Most WHOIS libraries answer "is this domain available?" with a boolean. That collapses two very different situations —
|
|
33
|
+
*this domain is registered* and *I could not find out* — into the same `false`.
|
|
34
|
+
|
|
35
|
+
The expensive failure is the other direction. A WHOIS server that is rate-limiting
|
|
36
|
+
you, or that has retired port 43, or that answered with an HTML error page, sends back
|
|
37
|
+
text containing none of the words that mean "registered". A detector built from
|
|
38
|
+
heuristics falls through all of them and lands on its most permissive rule, which
|
|
39
|
+
reports the domain as free. For a registrar that means putting a registered domain in
|
|
40
|
+
a customer's shopping cart.
|
|
41
|
+
|
|
42
|
+
So a lookup here returns one of five statuses, and `available` means it was positively
|
|
43
|
+
established:
|
|
44
|
+
|
|
45
|
+
| Status | Meaning |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `:available` | The registry said this name is not registered. |
|
|
48
|
+
| `:registered` | The name exists. |
|
|
49
|
+
| `:premium` | Unregistered, but reserved or premium-priced — not obtainable normally. |
|
|
50
|
+
| `:unknown` | No verdict. Rate limited, unreachable, unreadable. **Ask again later.** |
|
|
51
|
+
| `:invalid` | The input was not a usable domain, or its TLD has no known server. |
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
result = MonoVM::Whois.lookup("example.com")
|
|
55
|
+
|
|
56
|
+
result.status # => :registered
|
|
57
|
+
result.available? # => false
|
|
58
|
+
result.registered? # => true
|
|
59
|
+
result.unknown? # => false
|
|
60
|
+
result.conclusive? # => true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`available?` is safe to branch on. Treating `!available?` as "registered" is not —
|
|
64
|
+
that is what `registered?` is for.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### One domain
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
result = MonoVM::Whois.lookup("monovm.com")
|
|
72
|
+
|
|
73
|
+
result.name # => "monovm.com"
|
|
74
|
+
result.tld # => ".com"
|
|
75
|
+
result.sld # => "monovm"
|
|
76
|
+
result.status # => :registered
|
|
77
|
+
result.whois_message # the raw registry response, verbatim
|
|
78
|
+
result.record.registrar # => "Example Registrar, LLC"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Many domains, and names without a TLD
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
MonoVM::Whois.whois("monovm.com")
|
|
85
|
+
# => {"monovm.com" => :registered}
|
|
86
|
+
|
|
87
|
+
MonoVM::Whois.whois(%w[monovm google.com])
|
|
88
|
+
# => {"monovm.com" => :registered,
|
|
89
|
+
# "monovm.net" => :registered,
|
|
90
|
+
# "monovm.org" => :available,
|
|
91
|
+
# "monovm.info" => :available,
|
|
92
|
+
# "google.com" => :registered}
|
|
93
|
+
|
|
94
|
+
MonoVM::Whois.whois("monovm", popular_tlds: %w[.io .dev])
|
|
95
|
+
# => {"monovm.io" => :registered, "monovm.dev" => :available}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Bulk checks run concurrently (8 threads by default) and are throttled per host, so a
|
|
99
|
+
list of 500 `.com` names does not get your IP rate-limited by Verisign.
|
|
100
|
+
|
|
101
|
+
Duplicates, mixed case, URLs and trailing dots all collapse to one lookup:
|
|
102
|
+
`"EXAMPLE.COM"`, `"https://example.com/path"` and `"example.com."` are the same name.
|
|
103
|
+
|
|
104
|
+
### The parsed record
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
record = MonoVM::Whois.lookup("example.com").record
|
|
108
|
+
|
|
109
|
+
record.registrar # => "Example Registrar, LLC"
|
|
110
|
+
record.created_on # => 1995-08-14 04:00:00 UTC
|
|
111
|
+
record.expires_on # => 2027-08-13 04:00:00 UTC
|
|
112
|
+
record.days_until_expiry # => 372
|
|
113
|
+
record.nameservers # => ["ns1.example.com", "ns2.example.com"]
|
|
114
|
+
record.statuses # => ["clientTransferProhibited"]
|
|
115
|
+
record.registrant # => nil when the registry redacts it
|
|
116
|
+
record.dnssec? # => false
|
|
117
|
+
record.transfer_prohibited? # => true
|
|
118
|
+
record.expiring? # => false
|
|
119
|
+
record.contacts[:admin] # => {name: ..., email: ...}
|
|
120
|
+
record["Registry Domain ID"] # any raw field, by its original key
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Post-GDPR placeholders (`REDACTED FOR PRIVACY`, `Data Protected`, …) are reported as
|
|
124
|
+
`nil` rather than as a registrant literally named "REDACTED FOR PRIVACY".
|
|
125
|
+
|
|
126
|
+
### Why a verdict came out that way
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
MonoVM::Whois.explain("example.com")
|
|
130
|
+
# => {domain: "example.com",
|
|
131
|
+
# status: :registered,
|
|
132
|
+
# reason: "RDAP returned a domain object",
|
|
133
|
+
# trace: [{rule: "server_refusal", matched: false},
|
|
134
|
+
# {rule: "wrong_registry", matched: false},
|
|
135
|
+
# {rule: "rdap_object", matched: true, status: :registered,
|
|
136
|
+
# evidence: "example.com"}],
|
|
137
|
+
# endpoint: "https://rdap.verisign.com/com/v1/domain/"}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every rule that was consulted is in the trace, in order, with the one that decided and
|
|
141
|
+
the text it matched. This is the first thing to reach for when a classification looks
|
|
142
|
+
wrong.
|
|
143
|
+
|
|
144
|
+
### Single-domain handler
|
|
145
|
+
|
|
146
|
+
`WhoisHandler` wraps one lookup in an object, with camelCase aliases for code
|
|
147
|
+
being migrated from camelCase WHOIS APIs:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
handler = MonoVM::Whois::WhoisHandler.whois("monovm.com")
|
|
151
|
+
|
|
152
|
+
handler.available? # also handler.isAvailable
|
|
153
|
+
handler.valid? # also handler.isValid
|
|
154
|
+
handler.whois_message # also handler.getWhoisMessage
|
|
155
|
+
handler.tld # also handler.getTld
|
|
156
|
+
handler.availability_details # also handler.getAvailabilityDetails
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The four-state verdict described above applies here too: `handler.unknown?` is a
|
|
160
|
+
real answer, distinct from both `available?` and `registered?`.
|
|
161
|
+
|
|
162
|
+
### Command line
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
$ monovm-whois monovm.com google.com
|
|
166
|
+
monovm.com registered
|
|
167
|
+
google.com registered
|
|
168
|
+
|
|
169
|
+
$ monovm-whois monovm --tlds .io,.dev --details
|
|
170
|
+
$ monovm-whois example.com --json
|
|
171
|
+
$ monovm-whois example.com --prefer whois --timeout 5
|
|
172
|
+
$ monovm-whois --tld-count
|
|
173
|
+
2043 TLDs supported
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Exit code is 0 when every name got a real answer and 1 when any came back `unknown` or
|
|
177
|
+
`invalid` — so a script can tell "definitely free" from "could not find out".
|
|
178
|
+
|
|
179
|
+
### Configuration
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
MonoVM::Whois.configure do |config|
|
|
183
|
+
config.prefer = :whois # port 43 before RDAP (default: :rdap)
|
|
184
|
+
config.follow_referrals = true # chase thin registries to the registrar
|
|
185
|
+
config.concurrency = 8 # threads for bulk checks
|
|
186
|
+
config.throttle_interval = 0.5 # minimum seconds between queries to one host
|
|
187
|
+
config.cache_ttl = 300 # in-process response cache
|
|
188
|
+
config.retry_attempts = 2 # retries for timeouts, never for refusals
|
|
189
|
+
config.verify_ssl = true
|
|
190
|
+
config.socket_read_timeout = 15
|
|
191
|
+
config.popular_tlds = %w[.com .net .org .info]
|
|
192
|
+
config.instrumentation = ->(event) { Rails.logger.info(event) }
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Architecture
|
|
197
|
+
|
|
198
|
+
Five collaborators, each replaceable, wired together by `Client`:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
Client
|
|
202
|
+
├── Registry::ServerRegistry which server serves this TLD, and where the name splits
|
|
203
|
+
├── Transport::Factory how to talk to it (Strategy + Decorator)
|
|
204
|
+
├── Availability::Analyzer what the answer means (Chain of Responsibility)
|
|
205
|
+
├── Parser::Selector what the record says (Adapter)
|
|
206
|
+
└── Referral::Follower thin-registry second hop
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`Client` owns the sequence and none of the policy. Every collaborator arrives by
|
|
210
|
+
constructor injection, which is why the whole test suite runs offline against a
|
|
211
|
+
one-method fake transport.
|
|
212
|
+
|
|
213
|
+
### Detection is a chain of rules
|
|
214
|
+
|
|
215
|
+
Each rule answers one question and either returns a `Verdict` or `nil` for "not mine,
|
|
216
|
+
ask the next one". Two invariants hold the order together:
|
|
217
|
+
|
|
218
|
+
1. Rules that recognise a **non-answer** run first, so nothing reaching the permissive
|
|
219
|
+
rules could have been a refusal or a wrong-server reply.
|
|
220
|
+
2. Every rule concluding `:registered` runs before every rule concluding `:available`,
|
|
221
|
+
so when signals conflict the safe one wins.
|
|
222
|
+
|
|
223
|
+
| # | Rule | Verdict |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| 1 | `server_refusal` — rate limit, blocked client, retired port 43, HTTP error | `:unknown` |
|
|
226
|
+
| 2 | `wrong_registry` — unsupported TLD, or an address registry's banner | `:unknown` |
|
|
227
|
+
| 3 | `rdap_object` — structured JSON: `objectClassName` vs `errorCode` 404 | `:registered` / `:available` |
|
|
228
|
+
| 4 | `premium_name` | `:premium` |
|
|
229
|
+
| 5 | `explicit_unavailability` — general and per-TLD "registered" wording | `:registered` |
|
|
230
|
+
| 6 | `registration_fields` — three or more record fields present | `:registered` |
|
|
231
|
+
| 7 | `registry_marker` — the TLD's configured not-found string | `:available` |
|
|
232
|
+
| 8 | `availability_keywords` — multilingual not-found phrases | `:available` |
|
|
233
|
+
| 9 | `no_match` — the whitespace-tolerant regexp forms | `:available` |
|
|
234
|
+
| 10 | `tld_specific` — per-TLD availability wording | `:available` |
|
|
235
|
+
| 11 | `status_field` — an explicit `status: available` | `:available` |
|
|
236
|
+
| 12 | `recordless` — no record and no refusal; opt-in per TLD | `:available` |
|
|
237
|
+
| — | nothing matched | `:unknown` |
|
|
238
|
+
|
|
239
|
+
Adding support for a registry that invents new wording means one small object:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
class AcmeRule < MonoVM::Whois::Availability::Rule
|
|
243
|
+
def call(context)
|
|
244
|
+
return nil unless context.lower.include?("acme says this name is free")
|
|
245
|
+
|
|
246
|
+
available(reason: "ACME wording")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
MonoVM::Whois.configure do |config|
|
|
251
|
+
config.rules.insert_before("registry_marker", AcmeRule.new)
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`RuleSet` also supports `prepend`, `append`, `insert_after`, `replace` and `remove`.
|
|
256
|
+
|
|
257
|
+
### RDAP first
|
|
258
|
+
|
|
259
|
+
For any TLD with an RDAP endpoint the client queries RDAP before port 43. RDAP answers
|
|
260
|
+
in structured JSON — a registered domain is an object with an `objectClassName`, an
|
|
261
|
+
unregistered one is an error document with `errorCode` 404 — so availability is *read*
|
|
262
|
+
rather than inferred from prose. Port 43 is the fallback, used whenever a TLD has no
|
|
263
|
+
RDAP endpoint or its RDAP endpoint fails.
|
|
264
|
+
|
|
265
|
+
TLD coverage comes from two bundled data files:
|
|
266
|
+
|
|
267
|
+
- `data/whois_servers.json` — a curated port 43 server list, 872 TLDs, with the
|
|
268
|
+
registry's not-found marker per TLD.
|
|
269
|
+
- `data/rdap_bootstrap.json` — a snapshot of the IANA RDAP bootstrap registry
|
|
270
|
+
(RFC 7484), ~1,200 TLDs. Refresh with `rake data:refresh_rdap`.
|
|
271
|
+
|
|
272
|
+
Definitions merge, so a TLD can get its port 43 host from one file and its RDAP URL
|
|
273
|
+
from the other. A deployment can correct a stale entry without waiting for a release:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
export MONOVM_WHOIS_DEFINITIONS=/etc/monovm/whois-overrides.json
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
[
|
|
281
|
+
{
|
|
282
|
+
"extensions": ".example,.test",
|
|
283
|
+
"uri": "socket://whois.example.test",
|
|
284
|
+
"available": "Domain not found",
|
|
285
|
+
"rdap": "https://rdap.example.test/domain/"
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Referral following
|
|
291
|
+
|
|
292
|
+
Thin registries hold almost nothing. Query `.com` and Verisign returns a name, a
|
|
293
|
+
status, nameservers and a pointer: `Registrar WHOIS Server:`. The registrant and often
|
|
294
|
+
the accurate expiry date only exist on that second server, so the client follows the
|
|
295
|
+
pointer one hop.
|
|
296
|
+
|
|
297
|
+
A referral enriches the **record** and never the **verdict**. The registry is
|
|
298
|
+
authoritative about whether a name exists, and a registrar's server that is down must
|
|
299
|
+
not be able to turn a registered domain into an available one.
|
|
300
|
+
|
|
301
|
+
### Punycode
|
|
302
|
+
|
|
303
|
+
Internationalised names are converted with an RFC 3492 implementation in
|
|
304
|
+
`MonoVM::Whois::Punycode`, verified against the RFC's own test vectors and the
|
|
305
|
+
published ACE forms of the IDN ccTLDs. It lives here rather than in a dependency to
|
|
306
|
+
keep the gem dependency-free.
|
|
307
|
+
|
|
308
|
+
Registries are queried in Punycode by default, because Verisign answers "No match" to
|
|
309
|
+
a UTF-8 query — which would read as availability. DENIC is the documented exception and
|
|
310
|
+
is sent the Unicode form over port 43 (`config.unicode_query_tlds`).
|
|
311
|
+
|
|
312
|
+
## Fail-safe classifications
|
|
313
|
+
|
|
314
|
+
Situations that a permissive heuristic reads as "available", and what this gem
|
|
315
|
+
reports instead — every choice points the same way, refusing to guess:
|
|
316
|
+
|
|
317
|
+
| Situation | Permissive heuristic | This gem |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| Rate-limit notice | `available` | `:unknown` |
|
|
320
|
+
| Client blocked / port 43 retired | `available` | `:unknown` |
|
|
321
|
+
| HTTP 403/429/5xx from RDAP | `available` | `:unknown` |
|
|
322
|
+
| Empty response | `available` | `:unknown` |
|
|
323
|
+
| Address registry reached by mistake | `available` | `:unknown` |
|
|
324
|
+
| Fewer than 2 registration fields | `available` | `:unknown` unless the TLD opts in |
|
|
325
|
+
| DENIC `Status: invalid` | `available` | `:registered` |
|
|
326
|
+
| Premium/reserved name | `available` | `:premium` |
|
|
327
|
+
| Dot-padded keys (`status....: Registered`) | `available` | `:registered` |
|
|
328
|
+
| Registry restriction notice | `available` | `:registered` |
|
|
329
|
+
|
|
330
|
+
## Development
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
bundle install
|
|
334
|
+
bundle exec rspec # the offline suite
|
|
335
|
+
bundle exec rubocop
|
|
336
|
+
rake # both
|
|
337
|
+
|
|
338
|
+
rake spec_network # live registry lookups, opt-in
|
|
339
|
+
rake data:refresh_rdap # re-snapshot the IANA bootstrap
|
|
340
|
+
COVERAGE=1 bundle exec rspec
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The offline suite never touches the network: HTTP is blocked by WebMock, and the
|
|
344
|
+
socket transport is exercised against a loopback server the spec starts itself.
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT. See [LICENSE](LICENSE).
|