mirror of
https://github.com/Evolix/chexpire.git
synced 2024-05-05 02:05:09 +02:00
Whois parsing of .fr tld
This commit is contained in:
parent
c6b1ac7162
commit
00c85e7796
33
app/services/whois.rb
Normal file
33
app/services/whois.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
require "null_logger"
|
||||
require "domain_helper"
|
||||
require "whois/command"
|
||||
require "whois/parser"
|
||||
require "whois/response"
|
||||
|
||||
module Whois
|
||||
class << self
|
||||
def ask(domain, logger: NullLogger.new)
|
||||
Service.new(domain, logger).call
|
||||
end
|
||||
end
|
||||
|
||||
class Service
|
||||
attr_reader :domain
|
||||
attr_reader :logger
|
||||
|
||||
def initialize(domain, logger)
|
||||
@domain = domain
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def call
|
||||
command = Command.new(domain, logger: logger)
|
||||
raw_response = command.run
|
||||
|
||||
parser = Parser.for(domain, logger: logger)
|
||||
response = parser.parse(raw_response)
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
end
|
18
app/services/whois/command.rb
Normal file
18
app/services/whois/command.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
require "null_logger"
|
||||
require "system_command"
|
||||
|
||||
module Whois
|
||||
class Command
|
||||
attr_reader :logger
|
||||
attr_reader :domain
|
||||
|
||||
def initialize(domain, logger: NullLogger.new)
|
||||
@domain = domain
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def run
|
||||
SystemCommand.new("whois", domain, logger: logger).execute
|
||||
end
|
||||
end
|
||||
end
|
8
app/services/whois/errors.rb
Normal file
8
app/services/whois/errors.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
module Whois
|
||||
class UnsupportedDomainError < StandardError; end
|
||||
class ParserError < StandardError; end
|
||||
class CommentNotFoundError < ParserError; end
|
||||
class FieldNotFoundError < ParserError; end
|
||||
class MissingDateFormatError < ParserError; end
|
||||
class InvalidDateError < ParserError; end
|
||||
end
|
19
app/services/whois/parser.rb
Normal file
19
app/services/whois/parser.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require "null_logger"
|
||||
require "whois/errors"
|
||||
require "whois/parser/fr"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
PARSERS = [Fr].freeze
|
||||
|
||||
class << self
|
||||
def for(domain, logger: NullLogger.new)
|
||||
parser_class = PARSERS.find { |k| k.supports?(domain) }
|
||||
|
||||
fail UnsupportedDomainError, "Unsupported domain '#{domain}'" if parser_class.nil?
|
||||
|
||||
parser_class.new(domain, logger: logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
74
app/services/whois/parser/base.rb
Normal file
74
app/services/whois/parser/base.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
require "null_logger"
|
||||
require_relative "../response"
|
||||
require_relative "../errors"
|
||||
require_relative "entry_builder"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
class Base
|
||||
extend DomainHelper
|
||||
|
||||
attr_reader :domain
|
||||
attr_reader :logger
|
||||
attr_reader :response
|
||||
attr_reader :entries
|
||||
attr_reader :date_format
|
||||
|
||||
def initialize(domain, logger: NullLogger.new)
|
||||
@domain = domain
|
||||
@logger = logger
|
||||
@response = Response.new(domain)
|
||||
@date_format = nil
|
||||
end
|
||||
|
||||
def parse(raw)
|
||||
@entries = build_entries(raw)
|
||||
|
||||
do_parse
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_field!(name, after: -1, value: nil)
|
||||
fields.detect { |field|
|
||||
field.index > after &&
|
||||
field.name == name &&
|
||||
(value.nil? || field.value == value)
|
||||
} || fail(FieldNotFoundError, "Field `#{name}` not found, after index #{after}")
|
||||
end
|
||||
|
||||
def get_value!(name, after: -1)
|
||||
get_field!(name, after: after).value
|
||||
end
|
||||
|
||||
def parse_date(str)
|
||||
fail MissingDateFormatError, "Date format not set" if date_format.nil?
|
||||
|
||||
begin
|
||||
Date.strptime(str, date_format)
|
||||
rescue ArgumentError
|
||||
raise InvalidDateError, "Date `#{str}` does not match format #{date_format}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_entries(raw)
|
||||
builder = EntryBuilder.new(
|
||||
field_regex: self.class::FIELD_REGEX,
|
||||
comment_regex: self.class::COMMENT_REGEX,
|
||||
)
|
||||
|
||||
raw.split("\n").map.each_with_index { |line, index|
|
||||
builder.build_from_line(line, index)
|
||||
}.sort_by(&:index)
|
||||
end
|
||||
|
||||
def fields
|
||||
@fields ||= entries.select(&:field?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
30
app/services/whois/parser/entry/base.rb
Normal file
30
app/services/whois/parser/entry/base.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
module Whois
|
||||
module Parser
|
||||
module Entry
|
||||
class Base
|
||||
attr_reader :index
|
||||
|
||||
def initialize(index)
|
||||
@index = index
|
||||
@comment = false
|
||||
end
|
||||
|
||||
def comment!
|
||||
@comment = true
|
||||
end
|
||||
|
||||
def comment?
|
||||
@comment == true
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
def field?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/services/whois/parser/entry/blank.rb
Normal file
13
app/services/whois/parser/entry/blank.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
require_relative "base"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
module Entry
|
||||
class Blank < Base
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
app/services/whois/parser/entry/field.rb
Normal file
23
app/services/whois/parser/entry/field.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
require_relative "base"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
module Entry
|
||||
class Field < Base
|
||||
attr_reader :index
|
||||
attr_reader :name
|
||||
attr_reader :value
|
||||
|
||||
def initialize(index, name, value)
|
||||
super index
|
||||
@name = name.strip
|
||||
@value = value.strip
|
||||
end
|
||||
|
||||
def field?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
app/services/whois/parser/entry/text.rb
Normal file
16
app/services/whois/parser/entry/text.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
require_relative "base"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
module Entry
|
||||
class Text < Base
|
||||
attr_reader :text
|
||||
|
||||
def initialize(index, text)
|
||||
super index
|
||||
@text = text.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
55
app/services/whois/parser/entry_builder.rb
Normal file
55
app/services/whois/parser/entry_builder.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
require_relative "entry/blank"
|
||||
require_relative "entry/field"
|
||||
require_relative "entry/text"
|
||||
|
||||
module Whois
|
||||
module Parser
|
||||
class EntryBuilder
|
||||
attr_reader :field_regex
|
||||
attr_reader :comment_regex
|
||||
|
||||
def initialize(field_regex:, comment_regex:)
|
||||
@field_regex = field_regex
|
||||
@comment_regex = comment_regex
|
||||
end
|
||||
|
||||
def build_from_line(line, index)
|
||||
text = normalize_text(line)
|
||||
|
||||
return Entry::Blank.new(index) if line.empty?
|
||||
|
||||
build(index, text).tap do |entry|
|
||||
entry.comment! if comment?(line)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build(index, text)
|
||||
parts = field_regex.match(text)
|
||||
|
||||
if parts.nil?
|
||||
Entry::Text.new(index, text)
|
||||
else
|
||||
Entry::Field.new(index, parts[:name], parts[:value])
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_text(line)
|
||||
line.strip!
|
||||
|
||||
comment_data = comment_regex.match(line)
|
||||
|
||||
if comment_data.nil?
|
||||
line
|
||||
else
|
||||
comment_data[:text]
|
||||
end
|
||||
end
|
||||
|
||||
def comment?(line)
|
||||
comment_regex.match?(line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
39
app/services/whois/parser/fr.rb
Normal file
39
app/services/whois/parser/fr.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
require "domain_helper"
|
||||
require_relative "base"
|
||||
|
||||
module Whois::Parser
|
||||
class Fr < Base
|
||||
SUPPORTED_TLD = %w[.fr].freeze
|
||||
COMMENT_REGEX = /^%+ +(?<text>.+)$/
|
||||
FIELD_REGEX = /^(?<name>[^:]+)\s*:\s+(?<value>.+)$/
|
||||
|
||||
def self.supports?(domain)
|
||||
SUPPORTED_TLD.include?(tld(domain))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def do_parse
|
||||
set_date_format
|
||||
|
||||
domain_index = get_field!("domain", value: domain).index
|
||||
|
||||
created_date = get_value!("created", after: domain_index)
|
||||
response.created_on = parse_date(created_date)
|
||||
|
||||
expire_date = get_value!("Expiry Date", after: domain_index)
|
||||
response.expire_on = parse_date(expire_date)
|
||||
|
||||
updated_date = get_value!("last-update", after: domain_index)
|
||||
response.updated_on = parse_date(updated_date)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_date_format
|
||||
afnic_format = get_field!("complete date format").value
|
||||
|
||||
@date_format = "%d/%m/%Y" if afnic_format == "DD/MM/YYYY"
|
||||
end
|
||||
end
|
||||
end
|
11
app/services/whois/response.rb
Normal file
11
app/services/whois/response.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Whois
|
||||
class Response
|
||||
attr_accessor :created_on
|
||||
attr_accessor :updated_on
|
||||
attr_accessor :expire_on
|
||||
|
||||
def initialize(domain)
|
||||
@domain = domain
|
||||
end
|
||||
end
|
||||
end
|
150
test/fixtures/files/whois/domain.fr.txt
vendored
Normal file
150
test/fixtures/files/whois/domain.fr.txt
vendored
Normal file
|
@ -0,0 +1,150 @@
|
|||
% IANA WHOIS server
|
||||
% for more information on IANA, visit http://www.iana.org
|
||||
% This query returned 1 object
|
||||
|
||||
refer: whois.nic.fr
|
||||
|
||||
domain: FR
|
||||
|
||||
organisation: Association Française pour le Nommage Internet en Coopération (A.F.N.I.C.)
|
||||
address: Immeuble Le Stephenson
|
||||
address: 1 rue Stephenson
|
||||
address: 78180 Montigny-le-Bretonneux
|
||||
address: France
|
||||
|
||||
contact: administrative
|
||||
name: TLD Admin Contact
|
||||
organisation: Association Française pour le Nommage Internet en Coopération (A.F.N.I.C.)
|
||||
address: Immeuble Le Stephenson
|
||||
address: 1 rue Stephenson
|
||||
address: 78180 Montigny-le-Bretonneux
|
||||
address: France
|
||||
phone: +33 1 39 30 83 05
|
||||
fax-no: +33 1 39 30 83 01
|
||||
e-mail: tld-admin@nic.fr
|
||||
|
||||
contact: technical
|
||||
name: TLD Tech Contact
|
||||
organisation: Association Française pour le Nommage Internet en Coopération (A.F.N.I.C.)
|
||||
address: Immeuble Le Stephenson
|
||||
address: 1 rue Stephenson
|
||||
address: 78180 Montigny-le-Bretonneux
|
||||
address: France
|
||||
phone: +33 1 39 30 83 81
|
||||
fax-no: +33 1 39 30 83 01
|
||||
e-mail: tld-tech@nic.fr
|
||||
|
||||
nserver: D.EXT.NIC.FR 192.5.4.2 2001:500:2e:0:0:0:0:2
|
||||
nserver: D.NIC.FR 194.0.9.1 2001:678:c:0:0:0:0:1
|
||||
nserver: E.EXT.NIC.FR 193.176.144.22 2a00:d78:0:102:193:176:144:22
|
||||
nserver: F.EXT.NIC.FR 194.146.106.46 2001:67c:1010:11:0:0:0:53
|
||||
nserver: G.EXT.NIC.FR 194.0.36.1 2001:678:4c:0:0:0:0:1
|
||||
ds-rdata: 35095 8 2 23c6caadc9927ee98061f2b52c9b8da6b53f3f648f814a4a86a0faf9854bfa8e
|
||||
ds-rdata: 42104 8 2 8D913A49C3FA2A39BA0065B4E18BA793E3AD128F7C6C8AA008AEFE0A17985DF5
|
||||
|
||||
whois: whois.nic.fr
|
||||
|
||||
status: ACTIVE
|
||||
remarks: Registration information: http://www.nic.fr/
|
||||
|
||||
created: 1986-09-02
|
||||
changed: 2018-01-22
|
||||
source: IANA
|
||||
|
||||
%%
|
||||
%% This is the AFNIC Whois server.
|
||||
%%
|
||||
%% complete date format : DD/MM/YYYY
|
||||
%% short date format : DD/MM
|
||||
%% version : FRNIC-2.5
|
||||
%%
|
||||
%% Rights restricted by copyright.
|
||||
%% See https://www.afnic.fr/en/products-and-services/services/whois/whois-special-notice/
|
||||
%%
|
||||
%% Use '-h' option to obtain more information about this service.
|
||||
%%
|
||||
%% [5ca0:1e73:fedf:ed4:2af1:f4a6:56e6:8308 REQUEST] >> domain.fr
|
||||
%%
|
||||
%% RL Net [##########] - RL IP [#########.]
|
||||
%%
|
||||
|
||||
domain: domain.fr
|
||||
status: ACTIVE
|
||||
hold: NO
|
||||
holder-c: E1768-FRNIC
|
||||
admin-c: GC647-FRNIC
|
||||
tech-c: OVH5-FRNIC
|
||||
zone-c: NFC1-FRNIC
|
||||
nsl-id: NSL60350-FRNIC
|
||||
registrar: OVH
|
||||
Expiry Date: 17/02/2019
|
||||
created: 18/02/2004
|
||||
last-update: 28/01/2017
|
||||
source: FRNIC
|
||||
|
||||
ns-list: NSL60350-FRNIC
|
||||
nserver: ns4.dnsserver.fr
|
||||
nserver: ns0.dnsserver.com
|
||||
source: FRNIC
|
||||
|
||||
registrar: OVH
|
||||
type: Isp Option 1
|
||||
address: 2 Rue Kellermann
|
||||
address: 59100 ROUBAIX
|
||||
country: FR
|
||||
phone: +33 8 99 88 77 66
|
||||
fax-no: +33 3 20 20 20 20
|
||||
e-mail: support@registrar.fr
|
||||
website: http://www.registrar.fr
|
||||
anonymous: NO
|
||||
registered: 21/10/1999
|
||||
source: FRNIC
|
||||
|
||||
nic-hdl: SB999-FRNIC
|
||||
type: PERSON
|
||||
contact: Rex Lorne
|
||||
address: Chexpire
|
||||
address: Impasse Pastourelle
|
||||
address: 13001 Marseille
|
||||
country: FR
|
||||
phone: +33 1 23 45 67 89
|
||||
registrar: OVH
|
||||
changed: 18/02/2004 frnic-dbm-updates@nic.fr
|
||||
anonymous: NO
|
||||
obsoleted: NO
|
||||
source: FRNIC
|
||||
|
||||
nic-hdl: OVH5-FRNIC
|
||||
type: ROLE
|
||||
contact: OVH NET
|
||||
address: OVH
|
||||
address: 140, quai du Sartel
|
||||
address: 59100 Roubaix
|
||||
country: FR
|
||||
phone: +33 8 99 88 77 66
|
||||
e-mail: tech@registrar.fr
|
||||
trouble: Information: http://www.registrar.fr
|
||||
trouble: Questions: mailto:tech@registrar.fr
|
||||
trouble: Spam: mailto:abuse@registrar.fr
|
||||
admin-c: OK217-FRNIC
|
||||
tech-c: OK217-FRNIC
|
||||
notify: tech@registrar.fr
|
||||
registrar: OVH
|
||||
changed: 11/10/2006 tech@registrar.fr
|
||||
anonymous: NO
|
||||
obsoleted: NO
|
||||
source: FRNIC
|
||||
|
||||
nic-hdl: E9999-FRNIC
|
||||
type: ORGANIZATION
|
||||
contact: Chexpire
|
||||
address: Impasse Pastourelle
|
||||
address: 13001 Marseille
|
||||
country: FR
|
||||
phone: +33 1 23 45 67 89
|
||||
e-mail: info@domain.fr
|
||||
registrar: OVH
|
||||
changed: 11/03/2012 nic@nic.fr
|
||||
anonymous: NO
|
||||
obsoleted: NO
|
||||
source: FRNIC
|
27
test/services/whois/command_test.rb
Normal file
27
test/services/whois/command_test.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
require "test_helper"
|
||||
require "whois/command"
|
||||
|
||||
module Whois
|
||||
class CommandTest < ActiveSupport::TestCase
|
||||
test "should return the result and log the command" do
|
||||
result = "mocked whois result"
|
||||
|
||||
mock = Minitest::Mock.new
|
||||
mock.expect(:execute, result)
|
||||
|
||||
stub = lambda do |program, args, _logger|
|
||||
assert_equal "whois", program
|
||||
assert_equal "example.org", args
|
||||
|
||||
mock
|
||||
end
|
||||
|
||||
SystemCommand.stub(:new, stub) do
|
||||
command = Command.new("example.org")
|
||||
assert_equal result, command.run
|
||||
end
|
||||
|
||||
mock.verify
|
||||
end
|
||||
end
|
||||
end
|
21
test/services/whois/parser/fr_test.rb
Normal file
21
test/services/whois/parser/fr_test.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
require "test_helper"
|
||||
require "whois/parser/fr"
|
||||
require "whois/response"
|
||||
|
||||
module Whois
|
||||
class FrTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@parser = Parser::Fr.new("domain.fr")
|
||||
@domain_fr = file_fixture("whois/domain.fr.txt").read
|
||||
end
|
||||
|
||||
test "should parse a whois response" do
|
||||
response = @parser.parse(@domain_fr)
|
||||
assert_kind_of Response, response
|
||||
|
||||
assert_equal Date.new(2004, 2, 18), response.created_on
|
||||
assert_equal Date.new(2017, 1, 28), response.updated_on
|
||||
assert_equal Date.new(2019, 2, 17), response.expire_on
|
||||
end
|
||||
end
|
||||
end
|
15
test/services/whois/parser_test.rb
Normal file
15
test/services/whois/parser_test.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require "test_helper"
|
||||
require "whois/parser"
|
||||
require "whois/errors"
|
||||
|
||||
module Whois
|
||||
class ParserTest < ActiveSupport::TestCase
|
||||
test "should instanciate a parser class matching the tld" do
|
||||
assert_kind_of Parser::Fr, Parser.for("example.fr")
|
||||
|
||||
assert_raises UnsupportedDomainError do
|
||||
Parser.for("example.xyz")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
test/services/whois_test.rb
Normal file
9
test/services/whois_test.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
require "test_helper"
|
||||
require "whois"
|
||||
|
||||
class WhoisTest < ActiveSupport::TestCase
|
||||
test "should instanciate a parser class matching the tld" do
|
||||
# TODO: stub system command
|
||||
# assert_kind_of Whois::Response, Whois.ask("example.fr")
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue