whois-parser-next 0.4.5 → 0.4.6

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: 66e7767b738ae0cf7df9736dc9a6a7685f9ddf7d344506a3a4233accf905f0d2
4
+ data.tar.gz: c15b010e832b1e52cebdb02fd473e8c75176a0f232c777ba385e39b17676b327
5
5
  SHA512:
6
- metadata.gz: 0e32b486fb6c61548fe11307395704a4548ac68ac2187d7dd95f42dc5a9ecee9147466f83639ab9ea2637732f771f38ef2c1331a8cc3514cc386e8228ac80afc
7
- data.tar.gz: 4939cb1e255090dc84e95a824df8db82cdd11eceda1625a1caf3a465f12c8dc835afdcb023dea059cc874bbf4ea938154d823128951601dedd1b7f3694a3b972
6
+ metadata.gz: 587e231a29331459c3d14a9cd655ef2fa0230b819cbbcde397be5465500fd4a8eef4b5742407f402ed203d7104a9dc235553795427ec1b2daa1d08a34a24e82e
7
+ data.tar.gz: d4e01866484c46fcf88ef68889d677459ae34db5c0920dcabfc4bb2eaf390f16af8d5524436056a543154320742d51cc445dfb3553de4eb4386adc5c0ef511f1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  This project uses [Semantic Versioning 2.0.0](http://semver.org/).
4
4
 
5
+ #### Release 0.4.6 (2026-10-19)
6
+
7
+ - ADDED: Added `.ga` parsing for ANINF's current French WHOIS response, requiring
8
+ one valid domain, `Statut: actif`, and a parseable creation date; absence
9
+ requires the exact `%% NOT FOUND` marker. Added `.jo` parsing for a single
10
+ valid domain with a creation date and `Domain Status: Active`; absence
11
+ requires the exact quoted-name `No match for domain` response and matching
12
+ echoed domain.
13
+ - UPDATED: `.sx` recognizes the current CIRA EPP statuses
14
+ `clientDeleteProhibited` and `clientTransferProhibited`, plus the exact
15
+ `Not found: <name>` absence response, while retaining its legacy format.
16
+ Duplicate, contradictory, malformed, or incomplete evidence remains
17
+ unknown; explicit denials and throttling remain blocked.
18
+ - SAFETY: `.il` no longer treats a missing status as absence. Production
19
+ registered and absent queries returned the same 790-byte disclaimer-only
20
+ reply without an absence marker. Absence now requires the exact
21
+ `% No data was found to match the request criteria.` marker; registration
22
+ requires one valid domain and one `Transfer Locked` or `Transfer Allowed`
23
+ status. Empty, denied, throttled, duplicate, mixed, or incomplete replies
24
+ remain unknown. This corrects false absence; `.il` coverage remains
25
+ unverified.
26
+ - EVIDENCE: Bounded production-host and managed-proxy checks on 2026-10-19 UTC
27
+ observed registered and absent replies for all three hosts. IANA lists
28
+ `whois.nic.net.jo` for [`.jo`](https://www.iana.org/domains/root/db/jo.html)
29
+ and `whois.sx` for [`.sx`](https://www.iana.org/domains/root/db/sx.html).
30
+ IANA has no WHOIS listing for `.ga`; ANINF's current [official
31
+ webWHOIS](https://whois.nic.ga/) and [registration
32
+ policy](https://www.mon.ga/policy/registry_policies.pdf) establish its provenance.
33
+ The `.ga` and `.sx` source terms restrict circulation or use of WHOIS data,
34
+ so the repository fixtures are synthetic technical-shape controls, not
35
+ copies of the captured replies. See the fixture README for details.
36
+
5
37
  #### Release 0.4.5 (2026-10-18)
6
38
 
7
39
  - 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.6".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,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.6
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
@@ -778,6 +780,7 @@ files:
778
780
  - lib/whois/scanners/whois.nc.rb
779
781
  - lib/whois/scanners/whois.nic.cz.rb
780
782
  - lib/whois/scanners/whois.nic.it.rb
783
+ - lib/whois/scanners/whois.nic.net.jo.rb
781
784
  - lib/whois/scanners/whois.pir.org.rb
782
785
  - lib/whois/scanners/whois.rnids.rs.rb
783
786
  - lib/whois/scanners/whois.smallregistry.net.rb