21
1
Fork 0
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:
Colin Darie 2018-05-30 12:04:07 +02:00
parent c6b1ac7162
commit 00c85e7796
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
17 changed files with 561 additions and 0 deletions

33
app/services/whois.rb Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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