whois-parser-next 0.4.5 → 0.4.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1006a5373a7bed9d074cee1bc20d02bd78d9bd7a682d740c78c5fa6cf44aaf1f
4
- data.tar.gz: 99899f0e0a3db2b665119c876d4c561b11deaca2de4172a89bbdecebbb9f4b0d
3
+ metadata.gz: bf39e6c3afbb1b6a0cb49c48cb3fb11cf6db7d0162aaba77c9027ca4d2fadc3f
4
+ data.tar.gz: 1134d08ab3b1bcef0551f8008c366234805614e86cbe02ffe90501f5203dae84
5
5
  SHA512:
6
- metadata.gz: 0e32b486fb6c61548fe11307395704a4548ac68ac2187d7dd95f42dc5a9ecee9147466f83639ab9ea2637732f771f38ef2c1331a8cc3514cc386e8228ac80afc
7
- data.tar.gz: 4939cb1e255090dc84e95a824df8db82cdd11eceda1625a1caf3a465f12c8dc835afdcb023dea059cc874bbf4ea938154d823128951601dedd1b7f3694a3b972
6
+ metadata.gz: addd71819acd60b21142b60e419625ac8ebbed5b160b33e6cfe5562bcd064727e04b98c820f53bb98c1e1649158829b10b10d3c9ae7b900b47b25f57c1023043
7
+ data.tar.gz: 56a6dde832f4f0a9990e7753ea7f709a2bcba56d461d4eac0aa299bdd69b5896fd3b584647b6766267e621c4f6d52e42961a1fca1521c619b086f96f2dc80fcf
data/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  This project uses [Semantic Versioning 2.0.0](http://semver.org/).
4
4
 
5
+ #### Release 0.4.7 (2026-10-19)
6
+
7
+ - ADDED: Added strict parsing for the official `.gt` and `.com.gt` web WHOIS
8
+ response. Registration requires one result heading with a single valid domain
9
+ and `ACTIVO`; absence requires one `alert alert-danger` result with the same
10
+ domain and the registry's exact `no está registrado` marker. Duplicate,
11
+ contradictory, malformed, mismatched, incomplete, denied, throttled, or
12
+ challenge responses remain non-positive. The parser ignores scripts, styles,
13
+ comments, and unrelated page content; contacts and dates are not parsed.
14
+ - EVIDENCE: The official UVG registry lookup is
15
+ [`whois.php?dn=...`](https://www.gt/sitio/whois.php?dn={domain}). Bounded
16
+ 2026-10-19 observations returned registered results for `google.gt` and
17
+ `google.com.gt`, and exact absence results for generated eight-letter names
18
+ under both suffixes. The registry's
19
+ [policy](https://www.gt/sitio/registration_policy.php?lang=en) limits lookup
20
+ to individual names rather than lists of registered names. Regression
21
+ examples use only minimal synthetic HTML. This is a web WHOIS source; no
22
+ TCP/43 or RDAP service is claimed. See the
23
+ [dated evidence report](docs/audits/guatemala-web-whois-2026-10-19.md).
24
+
25
+ #### Release 0.4.6 (2026-10-19)
26
+
27
+ - ADDED: Added `.ga` parsing for ANINF's current French WHOIS response, requiring
28
+ one valid domain, `Statut: actif`, and a parseable creation date; absence
29
+ requires the exact `%% NOT FOUND` marker. Added `.jo` parsing for a single
30
+ valid domain with a creation date and `Domain Status: Active`; absence
31
+ requires the exact quoted-name `No match for domain` response and matching
32
+ echoed domain.
33
+ - UPDATED: `.sx` recognizes the current CIRA EPP statuses
34
+ `clientDeleteProhibited` and `clientTransferProhibited`, plus the exact
35
+ `Not found: <name>` absence response, while retaining its legacy format.
36
+ Duplicate, contradictory, malformed, or incomplete evidence remains
37
+ unknown; explicit denials and throttling remain blocked.
38
+ - SAFETY: `.il` no longer treats a missing status as absence. Production
39
+ registered and absent queries returned the same 790-byte disclaimer-only
40
+ reply without an absence marker. Absence now requires the exact
41
+ `% No data was found to match the request criteria.` marker; registration
42
+ requires one valid domain and one `Transfer Locked` or `Transfer Allowed`
43
+ status. Empty, denied, throttled, duplicate, mixed, or incomplete replies
44
+ remain unknown. This corrects false absence; `.il` coverage remains
45
+ unverified.
46
+ - EVIDENCE: Bounded production-host and managed-proxy checks on 2026-10-19 UTC
47
+ observed registered and absent replies for all three hosts. IANA lists
48
+ `whois.nic.net.jo` for [`.jo`](https://www.iana.org/domains/root/db/jo.html)
49
+ and `whois.sx` for [`.sx`](https://www.iana.org/domains/root/db/sx.html).
50
+ IANA has no WHOIS listing for `.ga`; ANINF's current [official
51
+ webWHOIS](https://whois.nic.ga/) and [registration
52
+ policy](https://www.mon.ga/policy/registry_policies.pdf) establish its provenance.
53
+ The `.ga` and `.sx` source terms restrict circulation or use of WHOIS data,
54
+ so the repository fixtures are synthetic technical-shape controls, not
55
+ copies of the captured replies. See the fixture README for details.
56
+
5
57
  #### Release 0.4.5 (2026-10-18)
6
58
 
7
59
  - ADDED: Added current CoCCA response parsing for `.bf`, `.td`, and `.mr`,
@@ -1,6 +1,6 @@
1
1
  module Whois
2
2
  class Parser
3
3
  # The current library version.
4
- VERSION = "0.4.5".freeze
4
+ VERSION = "0.4.7".freeze
5
5
  end
6
6
  end
@@ -8,6 +8,7 @@
8
8
 
9
9
 
10
10
  require_relative 'base'
11
+ require_relative 'registry_response_safety'
11
12
 
12
13
 
13
14
  module Whois
@@ -25,19 +26,20 @@ module Whois
25
26
  # and examples.
26
27
  #
27
28
  class WhoisIsocOrgIl < Base
29
+ include RegistryResponseSafety
28
30
 
29
31
  property_supported :status do
30
- if content_for_scanner =~ /status:\s+(.*?)\n/
31
- case ::Regexp.last_match(1).downcase
32
- when "transfer locked"
33
- :registered
34
- when "transfer allowed"
35
- :registered
36
- else
37
- Whois::Parser.bug!(ParserError, "Unknown status `#{::Regexp.last_match(1)}'.")
38
- end
39
- else
32
+ names = content_for_scanner.scan(/^domain:[ \t]*(.+)$/i).flatten.map(&:strip)
33
+ states = content_for_scanner.scan(/^status:[ \t]*(.+)$/i).flatten.map { |value| value.strip.downcase }
34
+ missing = content_for_scanner.scan(/^% No data was found to match the request criteria\.[ \t]*$/).size
35
+ if missing == 1 && names.empty? && states.empty?
40
36
  :available
37
+ elsif missing.zero? && names.size == 1 && states.size == 1 &&
38
+ names.first.match?(/\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+il\z/i) &&
39
+ ['transfer locked', 'transfer allowed'].include?(states.first)
40
+ :registered
41
+ else
42
+ :unknown
41
43
  end
42
44
  end
43
45
 
@@ -46,7 +48,7 @@ module Whois
46
48
  end
47
49
 
48
50
  property_supported :registered? do
49
- !available?
51
+ status == :registered
50
52
  end
51
53
 
52
54
 
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'registry_response_safety'
5
+
6
+ module Whois
7
+ class Parsers
8
+ # ANINF's current French-language registry response, observed 2026-10-19.
9
+ class WhoisNicGa < Base
10
+ include RegistryResponseSafety
11
+
12
+ property_supported(:domain) { header_field('Nom de domaine')&.downcase }
13
+ property_supported(:domain_id) { header_field('Domain ID') }
14
+ property_supported(:status) do
15
+ names = content_for_scanner.scan(/^Nom de domaine:[ \t]*(.+)$/).flatten
16
+ missing = content_for_scanner.scan(/^%% NOT FOUND[ \t]*$/).size
17
+ if missing == 1 && names.empty? && !header_field('Domain ID')
18
+ :available
19
+ elsif missing.zero? && names.size == 1 &&
20
+ names.first.strip.match?(/\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.ga\z/i) &&
21
+ header_field('Domain ID') && header_field('Statut') == 'actif' &&
22
+ valid_creation_date?
23
+ :registered
24
+ else
25
+ :unknown
26
+ end
27
+ end
28
+ property_supported(:available?) { status == :available }
29
+ property_supported(:registered?) { status == :registered }
30
+ property_supported(:created_on) { header_date('Date de création') }
31
+ property_supported(:updated_on) { header_date('Dernière modification') }
32
+ property_supported(:expires_on) { header_date("Date d'expiration") }
33
+ property_supported(:registrar) do
34
+ name = header_field('Registrar')
35
+ Parser::Registrar.new(name: name) if name
36
+ end
37
+ property_supported(:nameservers) do
38
+ content_for_scanner.scan(/^Serveur de noms:[ \t]*(\S+)[ \t]*$/).flatten.map do |name|
39
+ Parser::Nameserver.new(name: name.downcase)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def header_field(label)
46
+ header = content_for_scanner.split(/^\[/, 2).first.to_s
47
+ values = header.scan(/^#{Regexp.escape(label)}:[ \t]*(.+)$/).flatten.map(&:strip)
48
+ values.first if values.size == 1 && !values.first.empty?
49
+ end
50
+
51
+ def header_date(label)
52
+ value = header_field(label)
53
+ parse_time(value) if value
54
+ end
55
+
56
+ def valid_creation_date?
57
+ value = header_field('Date de création')
58
+ value && value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\z/) && Date.iso8601(value[0, 10]) && Time.iso8601(value)
59
+ rescue ArgumentError
60
+ false
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_cocca2'
4
+ require_relative 'registry_response_safety'
5
+ require 'whois/scanners/whois.nic.net.jo'
6
+
7
+ module Whois
8
+ class Parsers
9
+ # Jordan's official registry endpoint, observed 2026-10-19.
10
+ class WhoisNicNetJo < BaseCocca2
11
+ include RegistryResponseSafety
12
+ self.scanner = Scanners::WhoisNicNetJo
13
+
14
+ property_supported(:status) do
15
+ names = content_for_scanner.scan(/^Domain Name:[ \t]*(.+)$/i).flatten.map(&:strip)
16
+ absent = content_for_scanner.scan(/^No match for domain "([^"\n]+)"[ \t]*$/i).flatten
17
+ statuses = content_for_scanner.scan(/^Domain Status:[ \t]*(.+)$/i).flatten.map(&:strip)
18
+ valid_name = names.size == 1 && names.first.match?(/\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+jo\z/i)
19
+ if valid_name && absent.size == 1 && absent.first.casecmp?(names.first) && statuses.empty? &&
20
+ !content_for_scanner.match?(/^Creation Date:/i)
21
+ :available
22
+ elsif valid_name && absent.empty? && statuses == ['Active'] && valid_creation_date?
23
+ :registered
24
+ else
25
+ :unknown
26
+ end
27
+ end
28
+ property_supported(:available?) { status == :available }
29
+ property_supported(:registered?) { status == :registered }
30
+
31
+ private
32
+
33
+ def valid_creation_date?
34
+ values = content_for_scanner.scan(/^Creation Date:[ \t]*(.+)$/i).flatten.map(&:strip)
35
+ values.size == 1 && values.first.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/) && Date.iso8601(values.first[0, 10]) && Time.iso8601(values.first)
36
+ rescue ArgumentError
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,6 +8,7 @@
8
8
 
9
9
 
10
10
  require_relative 'base'
11
+ require_relative 'registry_response_safety'
11
12
  require 'whois/scanners/whois.sx.rb'
12
13
 
13
14
 
@@ -20,6 +21,7 @@ module Whois
20
21
  # The Example parser for the list of all available methods.
21
22
  #
22
23
  class WhoisSx < Base
24
+ include RegistryResponseSafety
23
25
  include Scanners::Scannable
24
26
 
25
27
  self.scanner = Scanners::WhoisSx
@@ -31,7 +33,7 @@ module Whois
31
33
 
32
34
 
33
35
  property_supported :domain do
34
- node("Domain Name", &:downcase)
36
+ (node("Domain Name") || node("Not found"))&.downcase
35
37
  end
36
38
 
37
39
  property_supported :domain_id do
@@ -40,6 +42,9 @@ module Whois
40
42
 
41
43
 
42
44
  property_supported :status do
45
+ current = current_registry_status
46
+ return current if current
47
+
43
48
  case (s = node("Domain Status", &:downcase))
44
49
  when "available"
45
50
  :available
@@ -103,6 +108,34 @@ module Whois
103
108
  end
104
109
 
105
110
 
111
+ # Keep the historical scanner for old fixtures, while recognizing the
112
+ # current EPP record and exact missing reply before scanning legacy text.
113
+ def current_registry_status
114
+ names = content_for_scanner.scan(/^Domain Name:[ \t]*(.+)$/i).flatten.map(&:strip)
115
+ absent = content_for_scanner.scan(/^Not found:[ \t]*(\S+)[ \t]*$/i).flatten
116
+ return nil if absent.empty? && !content_for_scanner.match?(/^Registry Domain ID:/i)
117
+
118
+ valid = ->(name) { name.match?(/\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.sx\z/i) }
119
+ if absent.size == 1 && names.empty? && valid.call(absent.first) &&
120
+ !content_for_scanner.match?(/^(?:Creation Date|Registry Domain ID|Domain Status):/i)
121
+ return :available
122
+ end
123
+ return :unknown unless absent.empty? && names.size == 1 && valid.call(names.first)
124
+
125
+ statuses = content_for_scanner.scan(/^Domain Status:[ \t]*(\S+)(?:[^\n]*)$/i).flatten
126
+ dates = content_for_scanner.scan(/^Creation Date:[ \t]*(.+)$/i).flatten.map(&:strip)
127
+ return :unknown unless dates.size == 1 && dates.first.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/)
128
+ Date.iso8601(dates.first[0, 10])
129
+ Time.iso8601(dates.first)
130
+ if statuses.any? && statuses.all? { |value| value.match?(/\A(?:ok|active|(?:client|server)(?:Delete|Transfer|Update|Renew)Prohibited)\z/) }
131
+ :registered
132
+ else
133
+ :unknown
134
+ end
135
+ rescue ArgumentError
136
+ :unknown
137
+ end
138
+
106
139
  private
107
140
 
108
141
  def build_contact(element, type)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'strscan'
5
+ require_relative 'base'
6
+ require_relative 'registry_response_safety'
7
+
8
+ module Whois
9
+ class Parsers
10
+ # The registry's per-domain public web WHOIS, observed 2026-10-19.
11
+ # Inspect only its result heading/alert, never navigation or script text.
12
+ class WwwGt < Base
13
+ include RegistryResponseSafety
14
+
15
+ property_supported(:status) { web_result[:status] }
16
+ property_supported(:domain) { web_result[:domain] }
17
+ property_supported(:registered?) { status == :registered }
18
+ property_supported(:available?) { status == :available }
19
+
20
+ def response_unavailable?
21
+ super || content_for_scanner.match?(/<cap-widget\b|\bclass=["']g-recaptcha["']|cf-chl-/i)
22
+ end
23
+
24
+ private
25
+
26
+ def web_result
27
+ @web_result ||= classify_web_result
28
+ end
29
+
30
+ def classify_web_result
31
+ blocks = result_blocks
32
+ return { status: :unknown } unless blocks
33
+
34
+ candidates = blocks.filter_map do |kind, text|
35
+ text = CGI.unescapeHTML(text.gsub('&aacute;', 'á').gsub('&Aacute;', 'Á')).gsub(/[ \t\r\n]+/, ' ').strip
36
+ if kind == 'h3' && (match = /\A(\S+)\. ACTIVO\z/i.match(text))
37
+ [match[1].downcase, :registered]
38
+ elsif kind == 'div' && (match = /\A(\S+)\. no está registrado \.\z/i.match(text))
39
+ [match[1].downcase, :available]
40
+ end
41
+ end
42
+ return { status: :unknown } unless candidates.size == 1
43
+
44
+ name, state = candidates.first
45
+ return { status: :unknown } unless name.bytesize <= 253 &&
46
+ name.match?(/\A(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+gt\z/)
47
+
48
+ { domain: name, status: state }
49
+ end
50
+
51
+ # One forward scan, including malformed tags. Limit each tag before
52
+ # inspecting attributes and skip scripts, styles and comments entirely.
53
+ def result_blocks
54
+ input = StringScanner.new(content_for_scanner)
55
+ blocks = []
56
+ active = nil
57
+ depth = 0
58
+ text = +''
59
+ until input.eos?
60
+ chunk = input.scan_until(/</)
61
+ unless chunk
62
+ text << input.rest if active
63
+ input.terminate
64
+ break
65
+ end
66
+ text << chunk[0...-1] if active
67
+ if input.peek(3) == '!--'
68
+ return nil unless input.scan_until(/-->/)
69
+ next
70
+ end
71
+ tag = input.scan_until(/>/)
72
+ return nil unless tag && tag.bytesize <= 16_384
73
+ match = /\A[ \t]*(\/?)[ \t]*([a-z0-9]+)/i.match(tag)
74
+ next unless match
75
+ closing = match[1] == '/'
76
+ name = match[2].downcase
77
+ if !closing && %w[script style].include?(name)
78
+ return nil if active
79
+ return nil unless input.scan_until(%r{</#{name}[ \t\r\n]*>}i)
80
+ next
81
+ end
82
+ if active
83
+ depth += 1 if !closing && name == active
84
+ depth -= 1 if closing && name == active
85
+ if depth.zero?
86
+ blocks << [active, text]
87
+ active = nil
88
+ text = +''
89
+ else
90
+ text << ' '
91
+ end
92
+ elsif !closing && (name == 'h3' || (name == 'div' && alert_classes?(tag)))
93
+ active = name
94
+ depth = 1
95
+ end
96
+ end
97
+ active ? nil : blocks
98
+ end
99
+
100
+ def alert_classes?(tag)
101
+ value = tag[/\bclass[ \t]*=[ \t]*["']([^"']*)["']/i, 1]
102
+ classes = value.to_s.split
103
+ classes.include?('alert') && classes.include?('alert-danger')
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_cocca2'
4
+
5
+ module Whois
6
+ module Scanners
7
+ class WhoisNicNetJo < BaseCocca2
8
+ self.tokenizers += [:skip_jordan_absence]
9
+
10
+ tokenizer :skip_jordan_absence do
11
+ @input.skip(/^No match for domain "[^"\n]+"[ \t]*\n/i)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -11,6 +11,7 @@ module Whois
11
11
  :scan_keyvalue,
12
12
  :skip_lastupdate,
13
13
  :scan_disclaimer,
14
+ :skip_comment,
14
15
  ]
15
16
 
16
17
 
@@ -25,6 +26,10 @@ module Whois
25
26
  @input.skip(/>>>(.+?)<<<\n/)
26
27
  end
27
28
 
29
+ tokenizer :skip_comment do
30
+ @input.skip(/^%[^\n]*\n/)
31
+ end
32
+
28
33
  tokenizer :scan_disclaimer do
29
34
  if @input.match?(/^% WHOIS LEGAL STATEMENT/)
30
35
  @ast["field:disclaimer"] = _scan_lines_to_array(/%(.*)\n/).map(&:strip).join("\n")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whois-parser-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simone Carletti
@@ -439,6 +439,7 @@ files:
439
439
  - lib/whois/parsers/whois.nic.furniture.rb
440
440
  - lib/whois/parsers/whois.nic.futbol.rb
441
441
  - lib/whois/parsers/whois.nic.fyi.rb
442
+ - lib/whois/parsers/whois.nic.ga.rb
442
443
  - lib/whois/parsers/whois.nic.gal.rb
443
444
  - lib/whois/parsers/whois.nic.games.rb
444
445
  - lib/whois/parsers/whois.nic.gd.rb
@@ -537,6 +538,7 @@ files:
537
538
  - lib/whois/parsers/whois.nic.mx.rb
538
539
  - lib/whois/parsers/whois.nic.name.rb
539
540
  - lib/whois/parsers/whois.nic.net.bw.rb
541
+ - lib/whois/parsers/whois.nic.net.jo.rb
540
542
  - lib/whois/parsers/whois.nic.net.ng.rb
541
543
  - lib/whois/parsers/whois.nic.net.sa.rb
542
544
  - lib/whois/parsers/whois.nic.net.sb.rb
@@ -751,6 +753,7 @@ files:
751
753
  - lib/whois/parsers/whois.za.org.rb
752
754
  - lib/whois/parsers/whois.zicta.zm.rb
753
755
  - lib/whois/parsers/whois1.nic.bi.rb
756
+ - lib/whois/parsers/www.gt.rb
754
757
  - lib/whois/parsers/za_central_registry.rb
755
758
  - lib/whois/safe_record.rb
756
759
  - lib/whois/scanners/base.rb
@@ -778,6 +781,7 @@ files:
778
781
  - lib/whois/scanners/whois.nc.rb
779
782
  - lib/whois/scanners/whois.nic.cz.rb
780
783
  - lib/whois/scanners/whois.nic.it.rb
784
+ - lib/whois/scanners/whois.nic.net.jo.rb
781
785
  - lib/whois/scanners/whois.pir.org.rb
782
786
  - lib/whois/scanners/whois.rnids.rs.rb
783
787
  - lib/whois/scanners/whois.smallregistry.net.rb