From 155aeed7a5430c619c4634e15139e5dee726dcd9 Mon Sep 17 00:00:00 2001 From: andrei Date: Sun, 16 Oct 2022 16:59:48 +0200 Subject: [PATCH 01/20] fix error thrown by Inspect protocol --- lib/whois/record.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/whois/record.ex b/lib/whois/record.ex index 559e7f2..d91e204 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -158,6 +158,6 @@ defimpl Inspect, for: Whois.Record do record |> Map.put(:raw, "…") |> Map.delete(:__struct__) - |> Inspect.Map.inspect("Whois.Record", opts) + |> Inspect.Map.inspect(Code.Identifier.inspect_as_atom(Whois.Record), opts) end end From cdcce413f0e2527f40ab098cbe2cac97685ecdc6 Mon Sep 17 00:00:00 2001 From: andrei Date: Sun, 16 Oct 2022 17:32:53 +0200 Subject: [PATCH 02/20] update Inspect protocol impl --- lib/whois/record.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/whois/record.ex b/lib/whois/record.ex index d91e204..3088caf 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -157,7 +157,6 @@ defimpl Inspect, for: Whois.Record do def inspect(%Whois.Record{} = record, opts) do record |> Map.put(:raw, "…") - |> Map.delete(:__struct__) - |> Inspect.Map.inspect(Code.Identifier.inspect_as_atom(Whois.Record), opts) + |> Inspect.Any.inspect(opts) end end From 32f34c80a49e8f46aa6595fee7e508c76e507c40 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Sat, 16 Dec 2023 06:41:43 -0600 Subject: [PATCH 03/20] Support .se and .no Whois formats --- lib/whois/record.ex | 53 ++++++++++++++++++++++++-------------- test/fixtures/raw/amoi.no | 33 ++++++++++++++++++++++++ test/fixtures/raw/amoi.se | 28 ++++++++++++++++++++ test/whois/record_test.exs | 27 +++++++++++++++++++ 4 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/raw/amoi.no create mode 100644 test/fixtures/raw/amoi.se diff --git a/lib/whois/record.ex b/lib/whois/record.ex index 3088caf..ba2899b 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -49,40 +49,31 @@ defmodule Whois.Record do raw |> String.split("\n") |> Enum.reduce(record, fn line, record -> - line - |> String.trim() - |> String.split(":", parts: 2) - |> case do - [name, value] -> - name = name |> String.trim() |> String.downcase() - value = value |> String.trim() - - case name do - "domain name" -> + case split_key_and_value(line) do + {key, value} -> + case String.downcase(key) do + n when n in ["domain name", "domain"] -> %{record | domain: value} - "name server" -> + ns when ns in ["name server", "nserver"] -> %{record | nameservers: record.nameservers ++ [value]} - "domain status" -> + s when s in ["domain status", "status"] -> %{record | status: record.status ++ [value]} - "registrar" -> + r when r in ["registrar", "registrar handle"] -> %{record | registrar: value} "sponsoring registrar" -> %{record | registrar: value} - "creation date" -> + c when c in ["creation date", "created"] -> %{record | created_at: parse_dt(value) || record.created_at} - "updated date" -> + u when u in ["updated date", "modified", "last updated"] -> %{record | updated_at: parse_dt(value) || record.updated_at} - "expiration date" -> - %{record | expires_at: parse_dt(value) || record.expires_at} - - "registry expiry date" -> + e when e in ["expiration date", "expires", "registry expiry date"] -> %{record | expires_at: parse_dt(value) || record.expires_at} "registrant " <> name -> @@ -122,10 +113,32 @@ defmodule Whois.Record do %{record | nameservers: nameservers, status: status} end + def split_key_and_value(line) do + line + |> String.trim() + |> String.split(":", parts: 2, trim: true) + |> Enum.map(&String.trim/1) + |> case do + # Some records are formatted as: + # Key...........: Value + [key, value] -> {String.trim_trailing(key, "."), value} + _ -> nil + end + end + defp parse_dt(string) do case NaiveDateTime.from_iso8601(string) do {:ok, datetime} -> datetime - {:error, _} -> nil + {:error, :invalid_format} -> parse_date_as_dt(string) + end + end + + defp parse_date_as_dt(string) do + with {:ok, %Date{} = date} <- Date.from_iso8601(string), + {:ok, datetime} <- NaiveDateTime.new(date, Time.new!(0, 0, 0)) do + datetime + else + _ -> nil end end diff --git a/test/fixtures/raw/amoi.no b/test/fixtures/raw/amoi.no new file mode 100644 index 0000000..5cc2f63 --- /dev/null +++ b/test/fixtures/raw/amoi.no @@ -0,0 +1,33 @@ +% By looking up information in the domain registration directory +% service, you confirm that you accept the terms and conditions of the +% service: +% https://www.norid.no/en/domeneoppslag/vilkar/ +% +% Norid AS holds the copyright to the lookup service, content, +% layout and the underlying collections of information used in the +% service (cf. the Act on Intellectual Property of May 2, 1961, No. +% 2). Any commercial use of information from the service, including +% targeted marketing, is prohibited. Using information from the domain +% registration directory service in violation of the terms and +% conditions may result in legal prosecution. +% +% The whois service at port 43 is intended to contribute to resolving +% technical problems where individual domains threaten the +% functionality, security and stability of other domains or the +% internet as an infrastructure. It does not give any information +% about who the holder of a domain is. To find information about a +% domain holder, please visit our website: +% https://www.norid.no/en/domeneoppslag/ + +Domain Information + +NORID Handle...............: AMO496D-NORID +Domain Name................: amoi.no +Registrar Handle...........: REG802-NORID +Tech-c Handle..............: DA9735R-NORID +Name Server Handle.........: CHIN23H-NORID +Name Server Handle.........: ED38H-NORID + +Additional information: +Created: 2023-11-06 +Last updated: 2023-11-06 diff --git a/test/fixtures/raw/amoi.se b/test/fixtures/raw/amoi.se new file mode 100644 index 0000000..e4a4e2a --- /dev/null +++ b/test/fixtures/raw/amoi.se @@ -0,0 +1,28 @@ +# Copyright (c) 1997- The Swedish Internet Foundation. +# All rights reserved. +# The information obtained through searches, or otherwise, is protected +# by the Swedish Copyright Act (1960:729) and international conventions. +# It is also subject to database protection according to the Swedish +# Copyright Act. +# Any use of this material to target advertising or +# similar activities is forbidden and will be prosecuted. +# If any of the information below is transferred to a third +# party, it must be done in its entirety. This server must +# not be used as a backend for a search engine. +# Result of search for registered domain names under +# the .se top level domain. +# This whois printout is printed with UTF-8 encoding. +# +state: active +domain: amoi.se +holder: EBWZWQ7514-62528 +created: 2020-03-11 +modified: 2023-05-24 +expires: 2024-03-11 +transferred: 2023-05-09 +nserver: ed.ns.cloudflare.com 199.27.135.11 +nserver: chin.ns.cloudflare.com 173.245.58.84 +dnssec: unsigned delegation +registry-lock: unlocked +status: ok +registrar: Dotkeeper AB diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index 97f7218..2529300 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -110,6 +110,33 @@ defmodule Whois.RecordTest do end end + test "parse amoi.se" do + record = parse("amoi.se") + assert record.domain == "amoi.se" + + assert record.nameservers == [ + "ed.ns.cloudflare.com 199.27.135.11", + "chin.ns.cloudflare.com 173.245.58.84" + ] + + assert record.status == ["ok"] + + assert record.registrar == "Dotkeeper AB" + assert_dt(record.created_at, ~D[2020-03-11]) + assert_dt(record.updated_at, ~D[2023-05-24]) + assert_dt(record.expires_at, ~D[2024-03-11]) + end + + test "parse amoi.no" do + record = parse("amoi.no") + assert record.domain == "amoi.no" + assert Enum.empty?(record.nameservers) + assert record.registrar == "REG802-NORID" + assert_dt(record.created_at, ~D[2023-11-06]) + assert_dt(record.updated_at, ~D[2023-11-06]) + refute record.expires_at + end + defp parse(domain) do "../fixtures/raw/#{domain}" |> Path.expand(__DIR__) From faceb77618c5e15495d5f9498ac041b298a0caca Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 08:55:11 -0600 Subject: [PATCH 04/20] Take the latest TLD list from the Ruby whois gem --- mix.lock | 6 +- priv/tld.csv | 179 +++++++++++++--------- priv/tld.json | 413 ++++++++++++++++---------------------------------- 3 files changed, 246 insertions(+), 352 deletions(-) diff --git a/mix.lock b/mix.lock index da8bc52..c5cc860 100644 --- a/mix.lock +++ b/mix.lock @@ -1,2 +1,4 @@ -%{"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} +%{ + "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm", "3b1dcad3067985dd8618c38399a8ee9c4e652d52a17a4aae7a6d6fc4fcc24856"}, + "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "f050061c87ad39478c942995b5a20c40f2c0bc06525404613b8b0474cb8bd796"}, +} diff --git a/priv/tld.csv b/priv/tld.csv index b4548ec..aff079e 100644 --- a/priv/tld.csv +++ b/priv/tld.csv @@ -10,7 +10,6 @@ academy,whois.nic.academy accountant,whois.nic.accountant accountants,whois.nic.accountants aco,whois.afilias-srs.net -active,whois.afilias-srs.net actor,whois.nic.actor ads,whois.nic.google adult,whois.afilias-srs.net @@ -33,7 +32,7 @@ akdn,whois.afilias-srs.net alfaromeo,whois.afilias-srs.net alibaba,whois.nic.alibaba alipay,whois.nic.alipay -allfinanz,whois.ksregistry.net +allfinanz,whois.nic.allfinanz allstate,whois.afilias-srs.net ally,whois.nic.ally alsace,whois-alsace.nic.fr @@ -64,16 +63,20 @@ associates,whois.nic.associates at,whois.nic.at priv.at,whois.nic.priv.at attorney,whois.nic.attorney -au,whois.audns.net.au +au,whois.auda.org.au auction,whois.nic.auction audi,whois.afilias-srs.net +audible,whois.nic.audible audio,whois.uniregistry.net auspost,whois.nic.auspost +author,whois.nic.author auto,whois.uniregistry.net autos,whois.afilias.net avianca,whois.afilias-srs.net aw,whois.nic.aw +aws,whois.nic.aws ax,whois.ax +baby,whois.nic.baby baidu,whois.gtld.knet.cn band,whois.nic.band bank,whois.nic.bank @@ -111,14 +114,12 @@ biz,whois.biz bj,whois.nic.bj black,whois.afilias.net blackfriday,whois.uniregistry.net -blanco,whois.nic.blanco blockbuster,whois.nic.blockbuster blog,whois.nic.blog blue,whois.afilias.net bms,whois.nic.bms -bmw,whois.ksregistry.net +bmw,whois.nic.bmw bn,whois.bnnic.bn -bnl,whois.nic.bnl bnpparibas,whois.afilias-srs.net bo,whois.nic.bo boats,whois.afilias-srs.net @@ -127,9 +128,11 @@ bofa,whois.nic.bofa bom,whois.gtlds.nic.br bond,whois.nic.bond boo,whois.nic.google +book,whois.nic.book bosch,whois.nic.bosch bostik,whois-bostik.nic.fr boston,whois.nic.boston +bot,whois.nic.bot boutique,whois.nic.boutique box,whois.aridnrs.net.au br,whois.registro.br @@ -144,7 +147,7 @@ bugatti,whois.afilias-srs.net build,whois.nic.build builders,whois.nic.builders business,whois.nic.business -buy,whois.afilias-srs.net +buy,whois.nic.buy buzz,whois.nic.buzz bw,whois.nic.net.bw by,whois.cctld.by @@ -156,7 +159,8 @@ co.ca,whois.co.ca cab,whois.nic.cab cafe,whois.nic.cafe cal,whois.nic.google -cam,whois.ksregistry.net +call,whois.nic.call +cam,whois.nic.cam camera,whois.nic.camera camp,whois.nic.camp cancerresearch,whois.nic.cancerresearch @@ -192,15 +196,16 @@ cfd,whois.nic.cfd ch,whois.nic.ch chanel,whois.nic.chanel channel,whois.nic.google +charity,whois.nic.charity chat,whois.nic.chat cheap,whois.nic.cheap chintai,whois.nic.chintai christmas,whois.uniregistry.net chrome,whois.nic.google -chrysler,whois.afilias-srs.net church,whois.nic.church ci,whois.nic.ci cipriani,whois.afilias-srs.net +circle,whois.nic.cirle city,whois.nic.city cityeats,whois.nic.cityeats cl,whois.nic.cl @@ -220,7 +225,7 @@ coach,whois.nic.coach codes,whois.nic.codes coffee,whois.nic.coffee college,whois.nic.college -cologne,whois-fe1.pdt.cologne.tango.knipp.de +cologne,whois.ryce-rsp.com com,whois.verisign-grs.com africa.com,whois.centralnic.com ar.com,whois.centralnic.com @@ -261,7 +266,8 @@ cookingchannel,whois.nic.cookingchannel cool,whois.nic.cool coop,whois.nic.coop corsica,whois-corsica.nic.fr -country,whois-dub.mm-registry.com +country,whois.uniregistry.net +coupon,whois.nic.coupon coupons,whois.nic.coupons courses,whois.aridnrs.net.au cr,whois.nic.cr @@ -289,6 +295,7 @@ dclk,whois.nic.google dds,whois.nic.dds de,whois.denic.de com.de,whois.centralnic.com +deal,whois.nic.deal deals,whois.nic.deals degree,whois.nic.degree delivery,whois.nic.delivery @@ -297,7 +304,7 @@ delta,whois.nic.delta democrat,whois.nic.democrat dental,whois.nic.dental dentist,whois.nic.dentist -desi,whois.ksregistry.net +desi,whois.nic.desi design,whois.nic.design dev,whois.nic.google diamonds,whois.nic.diamonds @@ -312,9 +319,7 @@ dk,whois.dk-hostmaster.dk dm,whois.nic.dm docs,whois.nic.google doctor,whois.nic.doctor -dodge,whois.afilias-srs.net dog,whois.nic.dog -doha,whois.nic.doha domains,whois.nic.domains doosan,whois.nic.xn--cg4bki dot,whois.nic.dot @@ -325,12 +330,12 @@ dubai,whois.nic.dubai duck,whois.nic.duck dunlop,whois.nic.dunlop durban,durban-whois.registry.net.za -dvag,whois.ksregistry.net +dvag,whois.nic.dvag dvr,whois.afilias-srs.net dz,whois.nic.dz eat,whois.nic.google ec,whois.nic.ec -eco,whois.afilias-srs.net +eco,whois.nic.eco edeka,whois.afilias-srs.net edu,whois.educause.edu education,whois.nic.education @@ -352,9 +357,8 @@ esurance,whois.afilias-srs.net etisalat,whois.centralnic.com eu,whois.eu eurovision,whois.nic.eurovision -eus,whois.eus.coreregistry.net +eus,whois.nic.eus events,whois.nic.events -everbank,whois.nic.everbank exchange,whois.nic.exchange expert,whois.nic.expert exposed,whois.nic.exposed @@ -369,6 +373,7 @@ fan,whois.nic.fan fans,whois.nic.fans farm,whois.nic.farm fashion,whois.nic.fashion +fast,whois.nic.fast fedex,whois.nic.fedex feedback,whois.nic.feedback ferrari,whois.nic.ferrari @@ -380,6 +385,7 @@ film,whois.nic.film final,whois.gtlds.nic.br finance,whois.nic.finance financial,whois.nic.financial +fire,whois.nic.fire firestone,whois.nic.firestone firmdale,whois.nic.firmdale fish,whois.nic.fish @@ -400,6 +406,7 @@ forex,whois.nic.forex forsale,whois.nic.forsale forum,whois.nic.forum foundation,whois.nic.foundation +fox,whois.nic.fox fr,whois.nic.fr aeroport.fr,whois.smallregistry.net avocat.fr,whois.smallregistry.net @@ -412,9 +419,10 @@ notaires.fr,whois.smallregistry.net pharmacien.fr,whois.smallregistry.net port.fr,whois.smallregistry.net veterinaire.fr,whois.smallregistry.net -fresenius,whois.ksregistry.net +free,whois.nic.free +fresenius,whois.nic.fresenius frl,whois.nic.frl -frogans,whois-frogans.nic.fr +frogans,whois.nic.frogans frontdoor,whois.nic.frontdoor fujitsu,whois.nic.gmo fujixerox,whois.nic.fujixerox @@ -424,7 +432,7 @@ furniture,whois.nic.furniture futbol,whois.nic.futbol fyi,whois.nic.fyi ga,whois.dot.ga -gal,whois.gal.coreregistry.net +gal,whois.nic.gal gallery,whois.nic.gallery gallo,whois.nic.gallo gallup,whois.nic.gallup @@ -461,11 +469,11 @@ gold,whois.nic.gold goldpoint,whois.nic.goldpoint golf,whois.nic.golf goo,whois.nic.gmo -goodhands,whois.afilias-srs.net goodyear,whois.nic.goodyear goog,whois.nic.google google,whois.nic.google gop,whois.nic.gop +got,whois.nic.got gov,whois.dotgov.gov gq,whois.dominio.gq graphics,whois.nic.graphics @@ -510,6 +518,7 @@ horse,whois.nic.horse hospital,whois.nic.hospital host,whois.nic.host hosting,whois.uniregistry.net +hot,whois.nic.hot house,whois.nic.house how,whois.nic.google hr,whois.dns.hr @@ -522,15 +531,17 @@ icbc,whois.nic.icbc ice,whois.nic.ice icu,whois.nic.icu id,whois.id -ie,whois.domainregistry.ie +ie,whois.iedr.ie ifm,whois.nic.ifm ikano,whois.ikano.tld-box.at il,whois.isoc.org.il im,whois.nic.im imamat,whois.afilias-srs.net +imdb,whois.nic.imdb immo,whois.nic.immo immobilien,whois.nic.immobilien -in,whois.inregistry.net +in,whois.registry.in +inc,whois.nic.inc industries,whois.nic.industries infiniti,whois.nic.gmo info,whois.afilias.net @@ -547,7 +558,6 @@ iq,whois.cmc.iq ir,whois.nic.ir irish,whois.nic.irish is,whois.isnic.is -iselect,whois.nic.iselect ismaili,whois.afilias-srs.net ist,whois.afilias-srs.net istanbul,whois.afilias-srs.net @@ -566,6 +576,8 @@ jio,whois.nic.jio jll,whois.afilias-srs.net jobs,whois.nic.jobs joburg,joburg-whois.registry.net.za +jot,whois.nic.jot +joy,whois.nic.joy jp,whois.jprs.jp juegos,whois.uniregistry.net juniper,whois.nic.juniper @@ -580,10 +592,11 @@ kg,whois.kg ki,whois.nic.ki kia,whois.nic.kia kim,whois.afilias.net +kindle,whois.nic.kindle kitchen,whois.nic.kitchen kiwi,whois.nic.kiwi kn,whois.nic.kn -koeln,whois-fe1.pdt.koeln.tango.knipp.de +koeln,whois.ryce-rsp.com komatsu,whois.nic.komatsu kosher,whois.nic.kosher kr,whois.kr @@ -594,12 +607,10 @@ kyoto,whois.nic.kyoto kz,whois.nic.kz la,whois.nic.la lacaixa,whois.nic.lacaixa -ladbrokes,whois.nic.ladbrokes lamborghini,whois.afilias-srs.net lamer,whois.nic.lamer lancaster,whois-lancaster.nic.fr lancia,whois.afilias-srs.net -lancome,whois.nic.lancome land,whois.nic.land landrover,whois.nic.landrover lasalle,whois.afilias-srs.net @@ -618,11 +629,11 @@ lego,whois.nic.lego lexus,whois.nic.lexus lgbt,whois.afilias.net li,whois.nic.li -liaison,whois.nic.liaison lidl,whois.nic.lidl life,whois.nic.life lifestyle,whois.nic.lifestyle lighting,whois.nic.lighting +like,whois.nic.like limited,whois.nic.limited limo,whois.nic.limo linde,whois.nic.linde @@ -659,7 +670,7 @@ maison,whois.nic.maison makeup,whois.nic.makeup man,whois.nic.man management,whois.nic.management -mango,whois.mango.coreregistry.net +mango,whois.nic.mango map,whois.nic.google market,whois.nic.market marketing,whois.nic.marketing @@ -681,7 +692,7 @@ menu,whois.nic.menu metlife,whois.nic.metlife mg,whois.nic.mg miami,whois.nic.miami -mini,whois.ksregistry.net +mini,whois.nic.mini mit,whois.afilias-srs.net mitsubishi,whois.nic.gmo mk,whois.marnet.mk @@ -694,18 +705,17 @@ mobi,whois.afilias.net mobile,whois.nic.mobile moda,whois.nic.moda moe,whois.nic.moe +moi,whois.nic.moi mom,whois.uniregistry.net monash,whois.nic.monash money,whois.nic.money monster,whois.nic.monster -mopar,whois.afilias-srs.net mormon,whois.nic.mormon mortgage,whois.nic.mortgage moscow,whois.nic.moscow motorcycles,whois.afilias-srs.net mov,whois.nic.google movie,whois.nic.movie -movistar,whois-fe.movistar.tango.knipp.de mq,whois.mediaserv.net mr,whois.nic.mr ms,whois.nic.ms @@ -713,12 +723,14 @@ mtn,whois.nic.mtn mtr,whois.nic.mtr mu,whois.nic.mu museum,whois.nic.museum -mx,whois.nic.mx +mw,whois.nic.mw +mx,whois.mx my,whois.mynic.my mz,whois.nic.mz na,whois.na-nic.com.na nab,whois.nic.nab nadex,whois.nic.nadex +nagoya,whois.nic.nagoya name,whois.nic.name nationwide,whois.nic.nationwide natura,whois.afilias-srs.net @@ -753,7 +765,8 @@ nl,whois.domain-registry.nl no,whois.norid.no nokia,whois.afilias-srs.net norton,whois.nic.norton -nowruz,whois.agitsys.net +now,whois.nic.now +nowruz,whois.nic.nowruz nowtv,whois.nic.nowtv nra,whois.afilias-srs.net nrw,whois.nic.nrw @@ -763,6 +776,7 @@ nz,whois.srs.net.nz obi,whois.nic.obi observer,whois.nic.observer off,whois.nic.off +okinawa,whois.nic.okinawa olayan,whois.nic.olayan olayangroup,whois.nic.olayangroup ollo,whois.nic.ollo @@ -782,20 +796,21 @@ eu.org,whois.eu.org hk.org,whois.registry.hk.com us.org,whois.centralnic.com za.org,whois.za.org -organic,whois.afilias-srs.net +organic,whois.afilias.net orientexpress,whois.afilias-srs.net origin,whois.afilias-srs.net origins,whois.nic.origins osaka,whois.nic.osaka ott,whois.nic.ott -ovh,whois-ovh.nic.fr +ovh,whois.ovh.com page,whois.nic.google panasonic,whois.nic.gmo paris,whois-paris.nic.fr -pars,whois.agitsys.net +pars,whois.nic.pars partners,whois.nic.partners parts,whois.nic.parts party,whois.nic.party +pay,whois.nic.pay pccw,whois.nic.pccw pe,kero.yachay.pe pet,whois.afilias.net @@ -810,6 +825,7 @@ physio,whois.nic.physio pics,whois.uniregistry.net pictures,whois.nic.pictures pid,whois.nic.pid +pin,whois.nic.pin pink,whois.afilias.net pioneer,whois.nic.gmo pizza,whois.nic.pizza @@ -822,13 +838,14 @@ plumbing,whois.nic.plumbing plus,whois.nic.plus pm,whois.nic.pm pnc,whois.nic.pnc -pohl,whois.ksregistry.net +pohl,whois.nic.pohl poker,whois.afilias.net politie,whois.nicpolitie porn,whois.afilias-srs.net post,whois.dotpostregistry.net pr,whois.afilias-srs.net press,whois.nic.press +prime,whois.nic.prime pro,whois.afilias.net prod,whois.nic.google productions,whois.nic.productions @@ -850,6 +867,7 @@ racing,whois.nic.racing radio,whois.nic.radio raid,whois.nic.raid re,whois.nic.re +read,whois.nic.read realestate,whois.nic.realestate realty,whois.nic.realty recipes,whois.nic.recipes @@ -883,6 +901,7 @@ ro,whois.rotld.ro rocks,whois.nic.rocks rodeo,whois.nic.rodeo rogers,whois.afilias-srs.net +room,whois.nic.room rs,whois.rnids.rs rsvp,whois.nic.google ru,whois.tcinet.ru @@ -892,8 +911,10 @@ ruhr,whois.nic.ruhr run,whois.nic.run rw,whois.ricta.org.rw rwe,whois.nic.rwe +ryukyu,whois.nic.ryukyu sa,whois.nic.net.sa -saarland,whois.ksregistry.net +saarland,whois.nic.saarland +safe,whois.nic.safe sale,whois.nic.sale salon,whois.nic.salon samsclub,whois.nic.samsclub @@ -903,6 +924,7 @@ sandvikcoromant,whois.nic.sandvikcoromant sanofi,whois.nic.sanofi sap,whois.nic.sap sarl,whois.nic.sarl +save,whois.nic.save saxo,whois.aridnrs.net.au sb,whois.nic.net.sb sbi,whois.nic.sbi @@ -919,11 +941,12 @@ schwarz,whois.nic.schwarz science,whois.nic.science scjohnson,whois.nic.scjohnson scor,whois.nic.scor -scot,whois.scot.coreregistry.net +scot,whois.nic.scot se,whois.iis.se com.se,whois.centralnic.com search,whois.nic.google seat,whois.nic.seat +secure,whois.nic.secure security,whois.nic.security seek,whois.nic.seek select,whois.nic.select @@ -940,15 +963,17 @@ shangrila,whois.nic.shangrila sharp,whois.nic.gmo shaw,whois.afilias-srs.net shell,whois.nic.shell -shia,whois.agitsys.net +shia,whois.nic.shia shiksha,whois.afilias.net shoes,whois.nic.shoes +shop,whois.nic.shop shopping,whois.nic.shopping shouji,whois.teleinfo.cn show,whois.nic.show showtime,whois.afilias-srs.net shriram,whois.afilias-srs.net si,whois.register.si +silk,whois.nic.silk sina,whois.nic.sina singles,whois.nic.singles site,whois.nic.site @@ -960,6 +985,7 @@ sl,whois.nic.sl sling,whois.nic.sling sm,whois.nic.sm smart,whois.nic.smart +smile,whois.nic.smile sn,whois.nic.sn sncf,whois-sncf.nic.fr so,whois.nic.so @@ -969,25 +995,25 @@ softbank,whois.nic.softbank software,whois.nic.software solar,whois.nic.solar solutions,whois.nic.solutions +song,whois.nic.song sony,whois.nic.sony soy,whois.nic.google space,whois.nic.space -spiegel,whois.ksregistry.net sport,whois.nic.sport +spot,whois.nic.spot spreadbetting,whois.nic.spreadbetting srl,whois.afilias-srs.net -srt,whois.afilias-srs.net +ss,whois.nic.ss st,whois.nic.st stada,whois.afilias-srs.net star,whois.nic.star -starhub,whois.nic.starhub statebank,whois.nic.statebank -statoil,whois.nic.statoil stc,whois.nic.stc stcgroup,whois.nic.stcgroup stockholm,whois.afilias-srs.net storage,whois.nic.storage store,whois.nic.store +stream,whois.nic.stream studio,whois.nic.studio study,whois.nic.study style,whois.nic.style @@ -1007,19 +1033,18 @@ symantec,whois.nic.symantec systems,whois.nic.systems tab,whois.nic.tab taipei,whois.nic.taipei +talk,whois.nic.talk tatamotors,whois.nic.tatamotors tatar,whois.nic.tatar tattoo,whois.uniregistry.net tax,whois.nic.tax taxi,whois.nic.taxi tc,whois.nic.tc -tci,whois.agitsys.net +tci,whois.nic.tci team,whois.nic.team tech,whois.nic.tech technology,whois.nic.technology tel,whois.nic.tel -telecity,whois.nic.telecity -telefonica,whois-fe.telefonica.tango.knipp.de temasek,whois.afilias-srs.net tennis,whois.nic.tennis teva,whois.nic.teva @@ -1043,6 +1068,7 @@ tm,whois.nic.tm tn,whois.ati.tn to,whois.tonic.to today,whois.nic.today +tokyo,whois.nic.tokyo tools,whois.nic.tools top,whois.nic.top toray,whois.nic.toray @@ -1062,8 +1088,10 @@ travelers,whois.afilias-srs.net travelersinsurance,whois.afilias-srs.net trust,whois.nic.trust trv,whois.afilias-srs.net -tui,whois.ksregistry.net -tv,tvwhois.verisign-grs.com +tui,whois.nic.tui +tunes,whois.nic.tunes +tushu,whois.nic.tushu +tv,whois.nic.tv tvs,whois.nic.tvs tw,whois.twnic.net.tw tz,whois.tznic.or.tz @@ -1071,12 +1099,12 @@ ua,whois.ua in.ua,whois.in.ua ubank,whois.nic.ubank ubs,whois.nic.ubs -uconnect,whois.afilias-srs.net ug,whois.co.ug uk,whois.nic.uk ac.uk,whois.ja.net gov.uk,whois.ja.net university,whois.nic.university +uno,whois.nic.uno uol,whois.gtlds.nic.br ups,whois.nic.ups us,whois.nic.us @@ -1103,8 +1131,6 @@ vip,whois.nic.vip virgin,whois.nic.virgin visa,whois.nic.visa vision,whois.nic.vision -vista,whois.nic.vista -vistaprint,whois.nic.vistaprint viva,whois.nic.viva vlaanderen,whois.nic.vlaanderen vodka,whois.nic.vodka @@ -1119,7 +1145,7 @@ wales,whois.nic.wales walmart,whois.nic.walmart walter,whois.nic.walter wang,whois.gtld.knet.cn -warman,whois.nic.warman +wanggou,whois.nic.wanggou watch,whois.nic.watch webcam,whois.nic.webcam weber,whois.nic.weber @@ -1139,6 +1165,7 @@ woodside,whois.nic.woodside work,whois.nic.work works,whois.nic.works world,whois.nic.world +wow,whois.nic.wow ws,whois.website.ws wtc,whois.nic.wtc wtf,whois.nic.wtf @@ -1148,14 +1175,17 @@ xihuan,whois.teleinfo.cn xin,whois.nic.xin xn--11b4c3d,whois.nic.xn--11b4c3d xn--1qqw23a,whois.ngtld.cn +xn--2scrj9c,whois.registry.in xn--30rr7y,whois.gtld.knet.cn xn--3bst00m,whois.gtld.knet.cn xn--3ds443g,whois.teleinfo.cn xn--3e0b707e,whois.kr +xn--3hcrj9c,whois.registry.in xn--3oq18vl8pn36a,whois.nic.xn--3oq18vl8pn36a xn--3pxu8k,whois.nic.xn--3pxu8k xn--42c2d9a,whois.nic.xn--42c2d9a -xn--45brj9c,whois.inregistry.net +xn--45br5cyl,whois.registry.in +xn--45brj9c,whois.registry.in xn--4gbrim,whois.afilias-srs.net xn--55qw42g,whois.conac.cn xn--55qx5d,whois.ngtld.cn @@ -1185,7 +1215,6 @@ xn--d1acj3b,whois.nic.xn--d1acj3b xn--d1alf,whois.marnet.mk xn--e1a4c,whois.eu xn--efvy88h,whois.nic.xn--efvy88h -xn--estv75g,whois.nic.xn--estv75g xn--fhbei,whois.nic.xn--fhbei xn--fiq228c5hs,whois.teleinfo.cn xn--fiq64b,whois.gtld.knet.cn @@ -1193,12 +1222,14 @@ xn--fiqs8s,cwhois.cnnic.cn xn--fiqz9s,cwhois.cnnic.cn xn--fjq720a,whois.nic.xn--fjq720a xn--flw351e,whois.nic.google -xn--fpcrj9c3d,whois.inregistry.net +xn--fpcrj9c3d,whois.registry.in xn--fzc2c9e2c,whois.nic.lk xn--fzys8d69uvgm,whois.nic.xn--fzys8d69uvgm xn--g2xx48c,whois.afilias-srs.net -xn--gecrj9c,whois.inregistry.net -xn--h2brj9c,whois.inregistry.net +xn--gecrj9c,whois.registry.in +xn--h2breg3eve,whois.registry.in +xn--h2brj9c,whois.registry.in +xn--h2brj9c8c,whois.registry.in xn--hxt814e,whois.nic.xn--hxt814e xn--i1b6b1a6a2e,whois.publicinterestregistry.net xn--io0a7i,whois.ngtld.cn @@ -1216,12 +1247,15 @@ xn--mgba3a4f16a,whois.nic.ir xn--mgba7c0bbn0a,whois.nic.xn--mgba7c0bbn0a xn--mgbaakc7dvf,whois.centralnic.com xn--mgbaam7a8h,whois.aeda.net.ae -xn--mgbab2bd,whois.bazaar.coreregistry.net -xn--mgbbh1a71e,whois.inregistry.net +xn--mgbab2bd,whois.nic.xn--mgbab2bd +xn--mgbah1a3hjkrd,whois.nic.mr +xn--mgbbh1a,whois.registry.in +xn--mgbbh1a71e,whois.registry.in xn--mgbca7dzdo,whois.afilias-srs.net xn--mgberp4a5d4ar,whois.nic.net.sa +xn--mgbgu82a,whois.registry.in xn--mgbi4ecexp,whois.aridnrs.net.au -xn--mgbt3dhd,whois.agitsys.net +xn--mgbt3dhd,whois.nic.xn--mgbt3dhd xn--mgbtx2b,whois.cmc.iq xn--mgbx4cd0ab,whois.mynic.my xn--mix891f,whois.monic.mo @@ -1239,14 +1273,15 @@ xn--p1ai,whois.tcinet.ru xn--pssy2u,whois.nic.xn--pssy2u xn--q9jyb4c,whois.nic.google xn--qcka1pmc,whois.nic.google -xn--s9brj9c,whois.inregistry.net +xn--rvc1e0am3e,whois.registry.in +xn--s9brj9c,whois.registry.in xn--ses554g,whois.registry.knet.cn xn--t60b56a,whois.nic.xn--t60b56a xn--tckwe,whois.nic.xn--tckwe xn--tiq49xqyj,whois.aridnrs.net.au xn--unup4y,whois.nic.xn--unup4y -xn--vermgensberater-ctb,whois.ksregistry.net -xn--vermgensberatung-pwb,whois.ksregistry.net +xn--vermgensberater-ctb,whois.nic.xn--vermgensberater-ctb +xn--vermgensberatung-pwb,whois.nic.xn--vermgensberatung-pwb xn--vhquv,whois.nic.xn--vhquv xn--vuq861b,whois.teleinfo.cn xn--w4r85el8fhu5dnra,whois.nic.xn--w4r85el8fhu5dnra @@ -1255,17 +1290,19 @@ xn--wgbh1c,whois.dotmasr.eg xn--wgbl6a,whois.registry.qa xn--xhq521b,whois.teleinfo.cn xn--xkc2al3hye2a,whois.nic.lk -xn--xkc2dl3a5ee0h,whois.inregistry.net +xn--xkc2dl3a5ee0h,whois.registry.in xn--y9a3aq,whois.amnic.net xn--yfro4i67o,whois.sgnic.sg xn--ygbi2ammx,whois.pnina.ps xn--zfr164b,whois.conac.cn -xperia,whois.nic.xperia xxx,whois.nic.xxx xyz,whois.nic.xyz yachts,whois.afilias-srs.net +yamaxun,whois.nic.yamaxun yodobashi,whois.nic.gmo yoga,whois.nic.yoga +yokohama,whois.nic.yokohama +you,whois.nic.you youtube,whois.nic.google yt,whois.nic.yt yun,whois.teleinfo.cn @@ -1276,8 +1313,10 @@ gov.za,whois.gov.za net.za,net-whois.registry.net.za org.za,org-whois.registry.net.za web.za,web-whois.registry.net.za +zappos,whois.nic.zappos zara,whois.afilias-srs.net +zero,whois.nic.zero zip,whois.nic.google -zm,whois.nic.zm +zm,whois.zicta.zm zone,whois.nic.zone -zuerich,whois.ksregistry.net +zuerich,whois.nic.zuerich diff --git a/priv/tld.json b/priv/tld.json index df180ba..e89440f 100644 --- a/priv/tld.json +++ b/priv/tld.json @@ -1,7 +1,7 @@ { "_": { "schema": "2", - "updated": "2018-03-26 13:29:17 UTC" + "updated": "2023-11-08 10:10:00 UTC" }, "aaa": { "_type": "newgtld", @@ -73,11 +73,6 @@ "_type": "newgtld", "host": "whois.afilias-srs.net" }, - "active": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "actor": { "_group": "donuts", "_type": "newgtld", @@ -194,7 +189,7 @@ "allfinanz": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.allfinanz" }, "allstate": { "_group": "afiliassrs", @@ -356,7 +351,7 @@ "host": "whois.nic.attorney" }, "au": { - "host": "whois.audns.net.au" + "host": "whois.auda.org.au" }, "auction": { "_group": "donuts", @@ -371,7 +366,7 @@ "audible": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.audible" }, "audio": { "_group": "uniregistry", @@ -385,7 +380,7 @@ "author": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.author" }, "auto": { "_group": "uniregistry", @@ -408,7 +403,7 @@ "aws": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.aws" }, "ax": { "host": "whois.ax" @@ -431,7 +426,7 @@ }, "baby": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.baby" }, "baidu": { "_group": "knet", @@ -625,10 +620,6 @@ "_type": "newgtld", "host": "whois.uniregistry.net" }, - "blanco": { - "_type": "newgtld", - "host": "whois.nic.blanco" - }, "blockbuster": { "_group": "afiliassrs", "_type": "newgtld", @@ -658,16 +649,11 @@ "bmw": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.bmw" }, "bn": { "host": "whois.bnnic.bn" }, - "bnl": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.nic.bnl" - }, "bnpparibas": { "_group": "afiliassrs", "_type": "newgtld", @@ -707,7 +693,7 @@ "book": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.book" }, "booking": { "_type": "newgtld", @@ -730,7 +716,7 @@ "bot": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.bot" }, "boutique": { "_group": "donuts", @@ -806,7 +792,7 @@ "buy": { "_group": "amazonregistry", "_type": "newgtld", - "host": "whois.afilias-srs.net" + "host": "whois.nic.buy" }, "buzz": { "_type": "newgtld", @@ -859,7 +845,7 @@ "call": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.call" }, "calvinklein": { "_type": "newgtld", @@ -868,7 +854,7 @@ "cam": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.cam" }, "camera": { "_group": "donuts", @@ -936,10 +922,6 @@ "_type": "newgtld", "host": "whois.uniregistry.net" }, - "cartier": { - "_type": "newgtld", - "adapter": "none" - }, "casa": { "_group": "mmregistry", "_type": "newgtld", @@ -966,9 +948,7 @@ "host": "whois.nic.casino" }, "cat": { - "host": "whois.nic.cat", - "adapter": "formatted", - "format": "-C US-ASCII ace %s" + "host": "whois.nic.cat" }, "catering": { "_group": "donuts", @@ -1049,6 +1029,11 @@ "_type": "newgtld", "host": "whois.nic.google" }, + "charity": { + "_group": "donuts", + "_type": "newgtld", + "host": "whois.nic.charity" + }, "chase": { "_type": "newgtld", "adapter": "none" @@ -1077,11 +1062,6 @@ "_type": "newgtld", "host": "whois.nic.google" }, - "chrysler": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "church": { "_group": "donuts", "_type": "newgtld", @@ -1098,7 +1078,7 @@ "circle": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.cirle" }, "cisco": { "_type": "newgtld", @@ -1207,7 +1187,7 @@ "cologne": { "_group": "knipp", "_type": "newgtld", - "host": "whois-fe1.pdt.cologne.tango.knipp.de" + "host": "whois.ryce-rsp.com" }, "com": { "host": "whois.verisign-grs.com", @@ -1401,14 +1381,14 @@ "host": "whois-corsica.nic.fr" }, "country": { - "_group": "mmregistry", + "_group": "uniregistry", "_type": "newgtld", - "host": "whois-dub.mm-registry.com" + "host": "whois.uniregistry.net" }, "coupon": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.coupon" }, "coupons": { "_group": "donuts", @@ -1560,7 +1540,7 @@ "deal": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.deal" }, "dealer": { "_type": "newgtld", @@ -1612,7 +1592,7 @@ "desi": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.desi" }, "design": { "_type": "newgtld", @@ -1700,20 +1680,11 @@ "_type": "newgtld", "host": "whois.nic.doctor" }, - "dodge": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "dog": { "_group": "donuts", "_type": "newgtld", "host": "whois.nic.dog" }, - "doha": { - "_type": "newgtld", - "host": "whois.nic.doha" - }, "domains": { "_group": "donuts", "_type": "newgtld", @@ -1754,10 +1725,6 @@ "_type": "newgtld", "host": "whois.nic.dunlop" }, - "duns": { - "_type": "newgtld", - "adapter": "none" - }, "dupont": { "_type": "newgtld", "adapter": "none" @@ -1770,7 +1737,7 @@ "dvag": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.dvag" }, "dvr": { "_group": "afiliassrs", @@ -1795,7 +1762,7 @@ "eco": { "_group": "afiliassrs", "_type": "newgtld", - "host": "whois.afilias-srs.net" + "host": "whois.nic.eco" }, "edeka": { "_group": "afiliassrs", @@ -1847,10 +1814,6 @@ "_type": "newgtld", "host": "whois.nic.enterprises" }, - "epost": { - "_type": "newgtld", - "adapter": "none" - }, "epson": { "_group": "aridnrs", "_type": "newgtld", @@ -1907,17 +1870,13 @@ "eus": { "_group": "coreregistry", "_type": "newgtld", - "host": "whois.eus.coreregistry.net" + "host": "whois.nic.eus" }, "events": { "_group": "donuts", "_type": "newgtld", "host": "whois.nic.events" }, - "everbank": { - "_type": "newgtld", - "host": "whois.nic.everbank" - }, "exchange": { "_group": "donuts", "_type": "newgtld", @@ -1967,7 +1926,7 @@ "host": "whois.nic.family" }, "fan": { - "_group": "centralnic", + "_group": "donuts", "_type": "newgtld", "host": "whois.nic.fan" }, @@ -1993,7 +1952,7 @@ "fast": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.fast" }, "fedex": { "_group": "afiliassrs", @@ -2053,7 +2012,7 @@ "fire": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.fire" }, "firestone": { "_type": "newgtld", @@ -2166,7 +2125,7 @@ }, "fox": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.fox" }, "fr": { "host": "whois.nic.fr" @@ -2229,12 +2188,12 @@ "free": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.free" }, "fresenius": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.fresenius" }, "frl": { "_type": "newgtld", @@ -2243,7 +2202,7 @@ "frogans": { "_group": "nicfr", "_type": "newgtld", - "host": "whois-frogans.nic.fr" + "host": "whois.nic.frogans" }, "frontdoor": { "_type": "newgtld", @@ -2297,7 +2256,7 @@ "gal": { "_group": "coreregistry", "_type": "newgtld", - "host": "whois.gal.coreregistry.net" + "host": "whois.nic.gal" }, "gallery": { "_group": "donuts", @@ -2341,6 +2300,7 @@ "host": "whois.nic.google" }, "gd": { + "_group": "centralnic", "host": "whois.nic.gd" }, "gdn": { @@ -2482,11 +2442,6 @@ "_type": "newgtld", "host": "whois.nic.gmo" }, - "goodhands": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "goodyear": { "_group": "afiliassrs", "_type": "newgtld", @@ -2510,7 +2465,7 @@ "got": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.got" }, "gov": { "host": "whois.dotgov.gov" @@ -2751,10 +2706,6 @@ "_type": "newgtld", "host": "whois.nic.honda" }, - "honeywell": { - "_type": "newgtld", - "adapter": "none" - }, "horse": { "_group": "mmregistry", "_type": "newgtld", @@ -2766,6 +2717,7 @@ "host": "whois.nic.hospital" }, "host": { + "_group": "centralnic", "_type": "newgtld", "host": "whois.nic.host" }, @@ -2777,7 +2729,7 @@ "hot": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.hot" }, "hoteles": { "_type": "newgtld", @@ -2848,7 +2800,7 @@ "host": "whois.id" }, "ie": { - "host": "whois.domainregistry.ie" + "host": "whois.iedr.ie" }, "ieee": { "_type": "newgtld", @@ -2876,7 +2828,7 @@ "imdb": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.imdb" }, "immo": { "_group": "donuts", @@ -2889,7 +2841,12 @@ "host": "whois.nic.immobilien" }, "in": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" + }, + "inc": { + "_group": "uniregistry", + "_type": "newgtld", + "host": "whois.nic.inc" }, "industries": { "_group": "donuts", @@ -2970,10 +2927,6 @@ "is": { "host": "whois.isnic.is" }, - "iselect": { - "_type": "newgtld", - "host": "whois.nic.iselect" - }, "ismaili": { "_group": "afiliassrs", "_type": "newgtld", @@ -3006,10 +2959,6 @@ "_type": "newgtld", "host": "whois.nic.iveco" }, - "iwc": { - "_type": "newgtld", - "adapter": "none" - }, "jaguar": { "_type": "newgtld", "host": "whois.nic.jaguar" @@ -3052,10 +3001,6 @@ "_type": "newgtld", "host": "whois.nic.jio" }, - "jlc": { - "_type": "newgtld", - "adapter": "none" - }, "jll": { "_group": "afiliassrs", "_type": "newgtld", @@ -3088,12 +3033,12 @@ "jot": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.jot" }, "joy": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.joy" }, "jp": { "host": "whois.jprs.jp", @@ -3172,7 +3117,7 @@ "kindle": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.kindle" }, "kitchen": { "_group": "donuts", @@ -3192,7 +3137,7 @@ "koeln": { "_group": "knipp", "_type": "newgtld", - "host": "whois-fe1.pdt.koeln.tango.knipp.de" + "host": "whois.ryce-rsp.com" }, "komatsu": { "_type": "newgtld", @@ -3251,10 +3196,6 @@ "_type": "newgtld", "host": "whois.nic.lacaixa" }, - "ladbrokes": { - "_type": "newgtld", - "host": "whois.nic.ladbrokes" - }, "lamborghini": { "_group": "afiliassrs", "_type": "newgtld", @@ -3275,10 +3216,6 @@ "_type": "newgtld", "host": "whois.afilias-srs.net" }, - "lancome": { - "_type": "newgtld", - "host": "whois.nic.lancome" - }, "land": { "_group": "donuts", "_type": "newgtld", @@ -3368,10 +3305,6 @@ "li": { "host": "whois.nic.li" }, - "liaison": { - "_type": "newgtld", - "host": "whois.nic.liaison" - }, "lidl": { "_type": "newgtld", "host": "whois.nic.lidl" @@ -3397,7 +3330,7 @@ "like": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.like" }, "lilly": { "_type": "newgtld", @@ -3589,7 +3522,7 @@ "mango": { "_group": "coreregistry", "_type": "newgtld", - "host": "whois.mango.coreregistry.net" + "host": "whois.nic.mango" }, "map": { "_group": "google", @@ -3684,10 +3617,6 @@ "_type": "newgtld", "host": "whois.nic.menu" }, - "meo": { - "_type": "newgtld", - "adapter": "none" - }, "merckmsd": { "_type": "newgtld", "adapter": "none" @@ -3718,7 +3647,7 @@ "mini": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.mini" }, "mint": { "_type": "newgtld", @@ -3771,10 +3700,6 @@ "_type": "newgtld", "host": "whois.nic.mobile" }, - "mobily": { - "_type": "newgtld", - "adapter": "none" - }, "moda": { "_group": "donuts", "_type": "newgtld", @@ -3787,7 +3712,7 @@ "moi": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.moi" }, "mom": { "_group": "uniregistry", @@ -3812,11 +3737,6 @@ "_type": "newgtld", "adapter": "none" }, - "mopar": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "mormon": { "_group": "afiliassrs", "_type": "newgtld", @@ -3850,11 +3770,6 @@ "_type": "newgtld", "host": "whois.nic.movie" }, - "movistar": { - "_group": "knipp", - "_type": "newgtld", - "host": "whois-fe.movistar.tango.knipp.de" - }, "mp": { "adapter": "none" }, @@ -3898,11 +3813,10 @@ "adapter": "none" }, "mw": { - "adapter": "web", - "url": "http://www.registrar.mw/" + "host": "whois.nic.mw" }, "mx": { - "host": "whois.nic.mx" + "host": "whois.mx" }, "my": { "host": "whois.mynic.my" @@ -3923,7 +3837,7 @@ }, "nagoya": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.nagoya" }, "name": { "host": "whois.nic.name", @@ -4112,12 +4026,12 @@ "now": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.now" }, "nowruz": { "_group": "agitsys", "_type": "newgtld", - "host": "whois.agitsys.net" + "host": "whois.nic.nowruz" }, "nowtv": { "_group": "afiliassrs", @@ -4174,7 +4088,7 @@ }, "okinawa": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.okinawa" }, "olayan": { "_type": "newgtld", @@ -4264,9 +4178,9 @@ "host": "whois.za.org" }, "organic": { - "_group": "afiliassrs", + "_group": "afilias", "_type": "newgtld", - "host": "whois.afilias-srs.net" + "host": "whois.afilias.net" }, "orientexpress": { "_group": "afiliassrs", @@ -4299,7 +4213,7 @@ "ovh": { "_group": "nicfr", "_type": "newgtld", - "host": "whois-ovh.nic.fr" + "host": "whois.ovh.com" }, "pa": { "adapter": "web", @@ -4315,10 +4229,6 @@ "_type": "newgtld", "host": "whois.nic.gmo" }, - "panerai": { - "_type": "newgtld", - "adapter": "none" - }, "paris": { "_group": "nicfr", "_type": "newgtld", @@ -4327,7 +4237,7 @@ "pars": { "_group": "agitsys", "_type": "newgtld", - "host": "whois.agitsys.net" + "host": "whois.nic.pars" }, "partners": { "_group": "donuts", @@ -4350,7 +4260,7 @@ "pay": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.pay" }, "pccw": { "_group": "afiliassrs", @@ -4417,10 +4327,6 @@ "_type": "newgtld", "host": "whois.nic.physio" }, - "piaget": { - "_type": "newgtld", - "adapter": "none" - }, "pics": { "_group": "uniregistry", "_type": "newgtld", @@ -4442,7 +4348,7 @@ "pin": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.pin" }, "ping": { "_type": "newgtld", @@ -4513,7 +4419,7 @@ "pohl": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.pohl" }, "poker": { "_group": "afilias", @@ -4551,7 +4457,7 @@ "prime": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.prime" }, "pro": { "host": "whois.afilias.net" @@ -4664,7 +4570,7 @@ "read": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.read" }, "realestate": { "_type": "newgtld", @@ -4837,7 +4743,7 @@ "room": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.room" }, "rs": { "host": "whois.rnids.rs" @@ -4876,7 +4782,7 @@ }, "ryukyu": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.ryukyu" }, "sa": { "host": "whois.nic.net.sa" @@ -4884,12 +4790,12 @@ "saarland": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.saarland" }, "safe": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.safe" }, "safety": { "_type": "newgtld", @@ -4933,10 +4839,6 @@ "_type": "newgtld", "host": "whois.nic.sap" }, - "sapo": { - "_type": "newgtld", - "adapter": "none" - }, "sarl": { "_group": "donuts", "_type": "newgtld", @@ -4949,7 +4851,7 @@ "save": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.save" }, "saxo": { "_group": "aridnrs", @@ -5023,7 +4925,7 @@ "scot": { "_group": "coreregistry", "_type": "newgtld", - "host": "whois.scot.coreregistry.net" + "host": "whois.nic.scot" }, "sd": { "adapter": "none" @@ -5048,7 +4950,7 @@ "secure": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.secure" }, "security": { "_group": "centralnic", @@ -5126,7 +5028,7 @@ "shia": { "_group": "agitsys", "_type": "newgtld", - "host": "whois.agitsys.net" + "host": "whois.nic.shia" }, "shiksha": { "_group": "afilias", @@ -5140,7 +5042,7 @@ }, "shop": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.shop" }, "shopping": { "_group": "donuts", @@ -5173,7 +5075,7 @@ "silk": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.silk" }, "sina": { "_group": "afiliassrs", @@ -5232,7 +5134,7 @@ "smile": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.smile" }, "sn": { "host": "whois.nic.sn" @@ -5281,7 +5183,7 @@ "song": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.song" }, "sony": { "_type": "newgtld", @@ -5297,11 +5199,6 @@ "_type": "newgtld", "host": "whois.nic.space" }, - "spiegel": { - "_group": "ksregistry", - "_type": "newgtld", - "host": "whois.ksregistry.net" - }, "sport": { "_type": "newgtld", "host": "whois.nic.sport" @@ -5309,7 +5206,7 @@ "spot": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.spot" }, "spreadbetting": { "_type": "newgtld", @@ -5323,10 +5220,8 @@ "_type": "newgtld", "host": "whois.afilias-srs.net" }, - "srt": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" + "ss": { + "host": "whois.nic.ss" }, "st": { "host": "whois.nic.st" @@ -5345,10 +5240,6 @@ "_type": "newgtld", "host": "whois.nic.star" }, - "starhub": { - "_type": "newgtld", - "host": "whois.nic.starhub" - }, "statebank": { "_group": "afiliassrs", "_type": "newgtld", @@ -5358,10 +5249,6 @@ "_type": "newgtld", "adapter": "none" }, - "statoil": { - "_type": "newgtld", - "host": "whois.nic.statoil" - }, "stc": { "_group": "centralnic", "_type": "newgtld", @@ -5383,12 +5270,13 @@ "host": "whois.nic.storage" }, "store": { + "_group": "centralnic", "_type": "newgtld", "host": "whois.nic.store" }, "stream": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.stream" }, "studio": { "_group": "donuts", @@ -5489,7 +5377,7 @@ "talk": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.talk" }, "taobao": { "_type": "newgtld", @@ -5528,7 +5416,7 @@ "tci": { "_group": "agitsys", "_type": "newgtld", - "host": "whois.agitsys.net" + "host": "whois.nic.tci" }, "td": { "adapter": "web", @@ -5556,15 +5444,6 @@ "tel": { "host": "whois.nic.tel" }, - "telecity": { - "_type": "newgtld", - "host": "whois.nic.telecity" - }, - "telefonica": { - "_group": "knipp", - "_type": "newgtld", - "host": "whois-fe.telefonica.tango.knipp.de" - }, "temasek": { "_group": "afiliassrs", "_type": "newgtld", @@ -5680,7 +5559,7 @@ }, "tokyo": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.tokyo" }, "tools": { "_group": "donuts", @@ -5777,20 +5656,20 @@ "tui": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.tui" }, "tunes": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.tunes" }, "tushu": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.tushu" }, "tv": { - "host": "tvwhois.verisign-grs.com", + "host": "whois.nic.tv", "adapter": "verisign" }, "tvs": { @@ -5818,11 +5697,6 @@ "_type": "newgtld", "host": "whois.nic.ubs" }, - "uconnect": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.afilias-srs.net" - }, "ug": { "host": "whois.co.ug" }, @@ -5872,8 +5746,9 @@ "host": "whois.nic.university" }, "uno": { + "_group": "centralnic", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.uno" }, "uol": { "_group": "nicbr", @@ -5999,14 +5874,6 @@ "_type": "newgtld", "host": "whois.nic.vision" }, - "vista": { - "_type": "newgtld", - "host": "whois.nic.vista" - }, - "vistaprint": { - "_type": "newgtld", - "host": "whois.nic.vistaprint" - }, "viva": { "_group": "centralnic", "_type": "newgtld", @@ -6084,11 +5951,7 @@ "wanggou": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" - }, - "warman": { - "_type": "newgtld", - "host": "whois.nic.warman" + "host": "whois.nic.wanggou" }, "watch": { "_group": "donuts", @@ -6116,6 +5979,7 @@ "host": "whois.nic.weber" }, "website": { + "_group": "centralnic", "_type": "newgtld", "host": "whois.nic.website" }, @@ -6205,7 +6069,7 @@ "wow": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.wow" }, "ws": { "host": "whois.website.ws" @@ -6257,7 +6121,7 @@ }, "xn--2scrj9c": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--30rr7y": { "_group": "knet", @@ -6277,6 +6141,9 @@ "xn--3e0b707e": { "host": "whois.kr" }, + "xn--3hcrj9c": { + "host": "whois.registry.in" + }, "xn--3oq18vl8pn36a": { "_group": "afiliassrs", "_type": "newgtld", @@ -6292,10 +6159,10 @@ }, "xn--45br5cyl": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--45brj9c": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--45q11c": { "_type": "newgtld", @@ -6447,11 +6314,6 @@ "_type": "newgtld", "host": "whois.nic.xn--efvy88h" }, - "xn--estv75g": { - "_group": "afiliassrs", - "_type": "newgtld", - "host": "whois.nic.xn--estv75g" - }, "xn--fct429k": { "_group": "amazonregistry", "_type": "newgtld", @@ -6487,7 +6349,7 @@ "host": "whois.nic.google" }, "xn--fpcrj9c3d": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--fzc2c9e2c": { "host": "whois.nic.lk" @@ -6508,7 +6370,7 @@ "adapter": "none" }, "xn--gecrj9c": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--gk3at1e": { "_group": "amazonregistry", @@ -6517,14 +6379,14 @@ }, "xn--h2breg3eve": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--h2brj9c": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--h2brj9c8c": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--hxt814e": { "_type": "newgtld", @@ -6613,7 +6475,10 @@ "xn--mgbab2bd": { "_group": "coreregistry", "_type": "newgtld", - "host": "whois.bazaar.coreregistry.net" + "host": "whois.nic.xn--mgbab2bd" + }, + "xn--mgbah1a3hjkrd": { + "host": "whois.nic.mr" }, "xn--mgbai9azgqp6j": { "_type": "newgtld", @@ -6623,16 +6488,12 @@ "adapter": "web", "url": "http://idn.jo/whois_a.aspx" }, - "xn--mgbb9fbpob": { - "_type": "newgtld", - "adapter": "none" - }, "xn--mgbbh1a": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--mgbbh1a71e": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--mgbc0a9azcg": { "adapter": "none" @@ -6647,7 +6508,7 @@ }, "xn--mgbgu82a": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--mgbi4ecexp": { "_group": "aridnrs", @@ -6660,7 +6521,7 @@ "xn--mgbt3dhd": { "_group": "agitsys", "_type": "newgtld", - "host": "whois.agitsys.net" + "host": "whois.nic.xn--mgbt3dhd" }, "xn--mgbtx2b": { "host": "whois.cmc.iq" @@ -6757,10 +6618,10 @@ }, "xn--rvc1e0am3e": { "_type": "newgtld", - "adapter": "none" + "host": "whois.registry.in" }, "xn--s9brj9c": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--ses554g": { "_type": "newgtld", @@ -6787,12 +6648,12 @@ "xn--vermgensberater-ctb": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.xn--vermgensberater-ctb" }, "xn--vermgensberatung-pwb": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.xn--vermgensberatung-pwb" }, "xn--vhquv": { "_group": "donuts", @@ -6827,7 +6688,7 @@ "host": "whois.nic.lk" }, "xn--xkc2dl3a5ee0h": { - "host": "whois.inregistry.net" + "host": "whois.registry.in" }, "xn--y9a3aq": { "host": "whois.amnic.net" @@ -6842,10 +6703,6 @@ "_type": "newgtld", "host": "whois.conac.cn" }, - "xperia": { - "_type": "newgtld", - "host": "whois.nic.xperia" - }, "xxx": { "host": "whois.nic.xxx" }, @@ -6866,7 +6723,7 @@ "yamaxun": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.yamaxun" }, "yandex": { "_type": "newgtld", @@ -6887,12 +6744,12 @@ }, "yokohama": { "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.yokohama" }, "you": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.you" }, "youtube": { "_group": "google", @@ -6934,7 +6791,7 @@ "zappos": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.zappos" }, "zara": { "_group": "afiliassrs", @@ -6944,19 +6801,15 @@ "zero": { "_group": "amazonregistry", "_type": "newgtld", - "adapter": "none" + "host": "whois.nic.zero" }, "zip": { "_group": "google", "_type": "newgtld", "host": "whois.nic.google" }, - "zippo": { - "_type": "newgtld", - "adapter": "none" - }, "zm": { - "host": "whois.nic.zm" + "host": "whois.zicta.zm" }, "zone": { "_group": "donuts", @@ -6966,9 +6819,9 @@ "zuerich": { "_group": "ksregistry", "_type": "newgtld", - "host": "whois.ksregistry.net" + "host": "whois.nic.zuerich" }, "zw": { "adapter": "none" } -} \ No newline at end of file +} From a8448942b0362cdf893ca96bce5b94bcf3f730be Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 10:43:52 -0600 Subject: [PATCH 05/20] Support querying for that include subdomains --- lib/whois/server.ex | 13 ++++++++++--- test/whois/server_test.exs | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 test/whois/server_test.exs diff --git a/lib/whois/server.ex b/lib/whois/server.ex index 3dc5c43..a886f2c 100644 --- a/lib/whois/server.ex +++ b/lib/whois/server.ex @@ -15,9 +15,16 @@ defmodule Whois.Server do @spec all :: map def all, do: @all - @spec for(String.t()) :: {:ok, t} | :error + @spec for(String.t()) :: {:ok, t} | {:error, :unsupported_tld} def for(domain) do - [_, tld] = String.split(domain, ".", parts: 2) - Map.fetch(@all, tld) + tld = + domain + |> String.split(".") + |> List.last() + + case @all[tld] do + nil -> {:error, :unsupported_tld} + server -> {:ok, server} + end end end diff --git a/test/whois/server_test.exs b/test/whois/server_test.exs new file mode 100644 index 0000000..f973980 --- /dev/null +++ b/test/whois/server_test.exs @@ -0,0 +1,14 @@ +defmodule Whois.ServerTest do + use ExUnit.Case, async: true + + test "handles .com" do + assert {:ok, %Whois.Server{host: host}} = Whois.Server.for("example.com") + assert host == "whois.verisign-grs.com" + end + + test "handles subdomains" do + {:ok, no_subdomain} = Whois.Server.for("example.com") + assert {:ok, ^no_subdomain} = Whois.Server.for("foo.example.com") + assert {:ok, ^no_subdomain} = Whois.Server.for("foo.bar.baz.example.com") + end +end From 5615f4414edc39708b35a32539665e710e1db6de Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 10:45:48 -0600 Subject: [PATCH 06/20] Support falling back to IANA WHOIS server for TLDs that don't support querying from scripts, like .ch --- lib/whois.ex | 45 +++++++++++++++---- lib/whois/record.ex | 2 +- test/fixtures/raw/jasstafel.michaelruoss.ch | 48 +++++++++++++++++++++ test/whois/record_test.exs | 28 +++++++++++- test/whois_test.exs | 19 ++++++-- 5 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 test/fixtures/raw/jasstafel.michaelruoss.ch diff --git a/lib/whois.ex b/lib/whois.ex index 13f132c..bdb1211 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -1,17 +1,46 @@ defmodule Whois do + @moduledoc """ + A WHOIS client for Elixir. + """ alias Whois.{Record, Server} - @type lookup_option :: {:server, String.t() | Server.t()} + @type lookup_option :: {:server, String.t() | Server.t()} | {:fall_back_to_iana?, boolean} @doc """ - Queries the appropriate WHOIS server for the domain name `domain` and returns - a `{:ok, %Whois.Record{}}` tuple on success, and `{:error, reason}` on - failure. + Queries the appropriate WHOIS server for the domain. + + ### Options + + - server: the WHOIS server to query. If not specified, we'll automatically + choose the appropriate server. + - fall_back_to_iana?: whether to fall back to the IANA WHOIS server if looking + up the domain on the specified or default server fails. Defaults to `true` + whenever the `:server` is not specified. + + ### Examples + + iex> {:ok, %Whois.Record{domain: "google.com"} = record} = Whois.lookup("google.com") + iex> NaiveDateTime.after?(record.expires_at, NaiveDateTime.utc_now()) + true """ @spec lookup(String.t(), [lookup_option]) :: {:ok, Record.t()} | {:error, atom} def lookup(domain, opts \\ []) do - with {:ok, raw} <- lookup_raw(domain, opts) do - {:ok, Record.parse(raw)} + with {:ok, raw} <- lookup_raw(domain, opts), + %Record{domain: d} = record when byte_size(d) > 0 <- Record.parse(raw) do + {:ok, record} + else + %Record{} = record -> + # We connected to the server, but got a totally garbage raw response, like: + # > Requests of this client are not permitted. Please use https://www.nic.ch/whois/ for queries." + # Unless the server was specified, we'll fall back to the IANA server. + if opts[:fall_back_to_iana?] || is_nil(opts[:server]) do + lookup(domain, server: "whois.iana.org", fall_back_to_iana?: false) + else + {:ok, record} + end + + other -> + other end end @@ -49,8 +78,8 @@ defmodule Whois do end end - :error -> - {:error, :unsupported} + {:error, _} = error -> + error end end diff --git a/lib/whois/record.ex b/lib/whois/record.ex index ba2899b..b00efa8 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -70,7 +70,7 @@ defmodule Whois.Record do c when c in ["creation date", "created"] -> %{record | created_at: parse_dt(value) || record.created_at} - u when u in ["updated date", "modified", "last updated"] -> + u when u in ["updated date", "modified", "last updated", "changed"] -> %{record | updated_at: parse_dt(value) || record.updated_at} e when e in ["expiration date", "expires", "registry expiry date"] -> diff --git a/test/fixtures/raw/jasstafel.michaelruoss.ch b/test/fixtures/raw/jasstafel.michaelruoss.ch new file mode 100644 index 0000000..a90b457 --- /dev/null +++ b/test/fixtures/raw/jasstafel.michaelruoss.ch @@ -0,0 +1,48 @@ +% IANA WHOIS server +% for more information on IANA, visit http://www.iana.org +% This query returned 1 object + +refer: whois.nic.ch + +domain: CH + +organisation: SWITCH The Swiss Education & Research Network +address: Werdstrasse 2 +address: Zurich CH-8021 +address: Switzerland + +contact: administrative +name: SWITCH TLD Administration +organisation: SWITCH The Swiss Education & Research Network +address: Werdstrasse 2 +address: Zurich CH-8021 +address: Switzerland +phone: +41 44 268 15 40 +fax-no: +41 44 268 15 78 +e-mail: tld-admin@switch.ch + +contact: technical +name: DNS Operations +organisation: SWITCH The Swiss Education & Research Network +address: Werdstrasse 2 +address: Zurich CH-8021 +address: Switzerland +phone: +41 44 268 15 40 +fax-no: +41 44 268 15 78 +e-mail: dns-operation@switch.ch + +nserver: A.NIC.CH 130.59.31.41 2001:620:0:ff:0:0:0:56 +nserver: B.NIC.CH 130.59.31.43 2001:620:0:ff:0:0:0:58 +nserver: D.NIC.CH 194.0.25.39 2001:678:20:0:0:0:0:39 +nserver: E.NIC.CH 194.0.17.1 2001:678:3:0:0:0:0:1 +nserver: F.NIC.CH 194.146.106.10 2001:67c:1010:2:0:0:0:53 +ds-rdata: 450 13 2 4994913d9ff4f0df95f08acdf1d66149d873368ff6c1836c64bdface62734fa2 + +whois: whois.nic.ch + +status: ACTIVE +remarks: Registration information: https://www.nic.ch/ + +created: 1987-05-20 +changed: 2023-11-30 +source: IANA diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index 2529300..5fde81f 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -1,5 +1,5 @@ defmodule Whois.RecordTest do - use ExUnit.Case + use ExUnit.Case, async: true doctest Whois.Record alias Whois.Contact @@ -137,6 +137,26 @@ defmodule Whois.RecordTest do refute record.expires_at end + test "parse jasstafel.michaelruoss.ch" do + record = parse("jasstafel.michaelruoss.ch") + + # This is a quirk of the IANA server + assert record.domain == "CH" + + assert record.nameservers == [ + "a.nic.ch 130.59.31.41 2001:620:0:ff:0:0:0:56", + "b.nic.ch 130.59.31.43 2001:620:0:ff:0:0:0:58", + "d.nic.ch 194.0.25.39 2001:678:20:0:0:0:0:39", + "e.nic.ch 194.0.17.1 2001:678:3:0:0:0:0:1", + "f.nic.ch 194.146.106.10 2001:67c:1010:2:0:0:0:53" + ] + + refute record.registrar + assert_dt(record.created_at, ~D[1987-05-20]) + assert_dt(record.updated_at, ~D[2023-11-30]) + refute record.expires_at + end + defp parse(domain) do "../fixtures/raw/#{domain}" |> Path.expand(__DIR__) @@ -144,7 +164,11 @@ defmodule Whois.RecordTest do |> Whois.Record.parse() end - defp assert_dt(datetime, date) do + defp assert_dt(%NaiveDateTime{} = datetime, date) do assert NaiveDateTime.to_date(datetime) == date end + + defp assert_dt(nil, _date) do + raise "expected a date" + end end diff --git a/test/whois_test.exs b/test/whois_test.exs index 90c4f25..1377022 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -1,22 +1,25 @@ defmodule WhoisTest do - use ExUnit.Case + use ExUnit.Case, async: true doctest Whois + setup do + wait() + end + @tag :live test "lookup/1" do assert {:ok, record} = Whois.lookup("google.com") assert record.domain == "google.com" for type <- [:administrator, :registrant, :technical] do - assert record.contacts[type].name == "Domain Administrator" assert record.contacts[type].organization == "Google LLC" - assert record.contacts[type].city == "Mountain View" + assert record.contacts[type].state == "CA" + assert record.contacts[type].country == "US" end end @tag :live test "lookup/2 with custom :server" do - wait() server = "whois.markmonitor.com" assert {:ok, record} = Whois.lookup("google.com", server: server) assert record.domain == "google.com" @@ -27,5 +30,13 @@ defmodule WhoisTest do assert record.domain == "google.com" end + @tag :live + test "lookup/2 falls back to IANA for TLDs that aren't amenable to robots" do + assert {:ok, record} = Whois.lookup("michaelruoss.ch") + + # This is a quirk of the IANA WHOIS server + assert record.domain == "CH" + end + defp wait, do: Process.sleep(2500) end From 6d48f60272f45b25c8b3b9c83d2e186fddbceacc Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 13:30:02 -0600 Subject: [PATCH 07/20] Add basic moduledocs --- lib/whois/contact.ex | 3 +++ lib/whois/record.ex | 3 +++ lib/whois/server.ex | 1 + test/support/fetch.ex | 2 ++ 4 files changed, 9 insertions(+) diff --git a/lib/whois/contact.ex b/lib/whois/contact.ex index 4250621..cac1bf3 100644 --- a/lib/whois/contact.ex +++ b/lib/whois/contact.ex @@ -1,4 +1,7 @@ defmodule Whois.Contact do + @moduledoc """ + Contact information listed in a WHOIS record. + """ defstruct [:name, :organization, :street, :city, :state, :zip, :country, :phone, :fax, :email] @type t :: %__MODULE__{ diff --git a/lib/whois/record.ex b/lib/whois/record.ex index b00efa8..4b6327e 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -1,4 +1,7 @@ defmodule Whois.Record do + @moduledoc """ + A parsed WHOIS record. + """ alias Whois.Contact defstruct [ diff --git a/lib/whois/server.ex b/lib/whois/server.ex index a886f2c..337921d 100644 --- a/lib/whois/server.ex +++ b/lib/whois/server.ex @@ -1,4 +1,5 @@ defmodule Whois.Server do + @moduledoc false defstruct [:host] @type t :: %__MODULE__{host: String.t()} diff --git a/test/support/fetch.ex b/test/support/fetch.ex index 3de88ac..0d14e3d 100644 --- a/test/support/fetch.ex +++ b/test/support/fetch.ex @@ -1,4 +1,6 @@ defmodule Mix.Tasks.Whois.Fetch do + @moduledoc false + def run(domains) do root = Path.expand("../fixtures/raw", __DIR__) File.mkdir_p!(root) From 7d5ec48a04ea9fe4e63ef73db92b8be9ced4ca26 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 13:30:10 -0600 Subject: [PATCH 08/20] Silence unnecessary compile warning --- config/config.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 53afe71..ac561f6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,5 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this From 234bdc9d7d88acfdfa5d74cdcd1a30d2598ce53d Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 13:30:32 -0600 Subject: [PATCH 09/20] Add CI tests --- .credo.exs | 216 ++++++++++++++++++++ .formatter.exs | 2 +- .github/actions/elixir-setup/action.yml | 132 ++++++++++++ .github/dependabot.yml | 8 + .github/workflows/elixir-build-and-test.yml | 58 ++++++ .github/workflows/elixir-dialyzer.yml | 56 +++++ .github/workflows/elixir-quality-checks.yml | 44 ++++ .gitignore | 1 + .recode.exs | 33 +++ lib/whois/record.ex | 2 +- mix.exs | 52 ++++- mix.lock | 19 +- 12 files changed, 613 insertions(+), 10 deletions(-) create mode 100644 .credo.exs create mode 100644 .github/actions/elixir-setup/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/elixir-build-and-test.yml create mode 100644 .github/workflows/elixir-dialyzer.yml create mode 100644 .github/workflows/elixir-quality-checks.yml create mode 100644 .recode.exs diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..66ee0c4 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,216 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, false}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, false}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index a1db3e9..2447b9f 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,6 @@ [ inputs: [ - "{lib,unicode,test}/**/*.{ex,exs}", + "{lib,config,test}/**/*.{ex,exs}", "*.exs", ".*.exs" ] diff --git a/.github/actions/elixir-setup/action.yml b/.github/actions/elixir-setup/action.yml new file mode 100644 index 0000000..a53dfa5 --- /dev/null +++ b/.github/actions/elixir-setup/action.yml @@ -0,0 +1,132 @@ +name: Setup Elixir Project +description: Checks out the code, configures Elixir, fetches dependencies, and manages build caching. +inputs: + elixir-version: + required: true + type: string + description: Elixir version to set up + otp-version: + required: true + type: string + description: OTP version to set up + ################################################################# + # Everything below this line is optional. + # + # It's designed to make compiling a reasonably standard Elixir + # codebase "just work," though there may be speed gains to be had + # by tweaking these flags. + ################################################################# + build-deps: + required: false + type: boolean + default: true + description: True if we should compile dependencies + build-app: + required: false + type: boolean + default: true + description: True if we should compile the application itself + build-flags: + required: false + type: string + default: '--all-warnings' + description: Flags to pass to mix compile + install-rebar: + required: false + type: boolean + default: true + description: By default, we will install Rebar (mix local.rebar --force). + install-hex: + required: false + type: boolean + default: true + description: By default, we will install Hex (mix local.hex --force). + cache-key: + required: false + type: string + default: 'v1' + description: If you need to reset the cache for some reason, you can change this key. +outputs: + otp-version: + description: "Exact OTP version selected by the BEAM setup step" + value: ${{ steps.beam.outputs.otp-version }} + elixir-version: + description: "Exact Elixir version selected by the BEAM setup step" + value: ${{ steps.beam.outputs.elixir-version }} +runs: + using: "composite" + steps: + - name: Setup elixir + uses: erlef/setup-beam@v1 + id: beam + with: + elixir-version: ${{ inputs.elixir-version }} + otp-version: ${{ inputs.otp-version }} + + - name: Get deps cache + uses: actions/cache@v2 + with: + path: deps/ + key: deps-${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + deps-${{ inputs.cache-key }}-${{ runner.os }}- + + - name: Get build cache + uses: actions/cache@v2 + id: build-cache + with: + path: _build/${{env.MIX_ENV}}/ + key: build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}- + + - name: Get Hex cache + uses: actions/cache@v2 + id: hex-cache + with: + path: ~/.hex + key: build-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + build-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}- + + # In my experience, I have issues with incremental builds maybe 1 in 100 + # times that are fixed by doing a full recompile. + # In order to not waste dev time on such trivial issues (while also reaping + # the time savings of incremental builds for *most* day-to-day development), + # I force a full recompile only on builds that we retry. + - name: Clean to rule out incremental build as a source of flakiness + if: github.run_attempt != '1' + run: | + mix deps.clean --all + mix clean + shell: sh + + - name: Install Rebar + run: mix local.rebar --force + shell: sh + if: inputs.install-rebar == 'true' + + - name: Install Hex + run: mix local.hex --force + shell: sh + if: inputs.install-hex == 'true' + + - name: Install Dependencies + run: mix deps.get + shell: sh + + # Normally we'd use `mix deps.compile` here, however that incurs a large + # performance penalty when the dependencies are already fully compiled: + # https://elixirforum.com/t/github-action-cache-elixir-always-recompiles-dependencies-elixir-1-13-3/45994/12 + # + # Accoring to Jose Valim at the above link `mix loadpaths` will check and + # compile missing dependencies + - name: Compile Dependencies + run: mix loadpaths + shell: sh + if: inputs.build-deps == 'true' + + - name: Compile Application + run: mix compile ${{ inputs.build-flags }} + shell: sh + if: inputs.build-app == 'true' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c8c5a6f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: mix + directory: "/" + schedule: + interval: daily + time: "12:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml new file mode 100644 index 0000000..43da59d --- /dev/null +++ b/.github/workflows/elixir-build-and-test.yml @@ -0,0 +1,58 @@ +name: Build and Test + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + env: + MIX_ENV: test + strategy: + matrix: + elixir: ["1.15.7"] + otp: ["26.1.2"] + + # Remove if you don't need a database + services: + db: + image: postgis/postgis:13-3.1 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: server_test + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-flags: --all-warnings --warnings-as-errors + + - name: Run Tests + run: mix coveralls.json --warnings-as-errors + if: always() + + # Optional, but Codecov has a bot that will comment on your PR with per-file + # coverage deltas. + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: ./cover/excoveralls.json diff --git a/.github/workflows/elixir-dialyzer.yml b/.github/workflows/elixir-dialyzer.yml new file mode 100644 index 0000000..8c61eaa --- /dev/null +++ b/.github/workflows/elixir-dialyzer.yml @@ -0,0 +1,56 @@ +name: Elixir Type Linting + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + build: + name: Run Dialyzer + runs-on: ubuntu-latest + env: + MIX_ENV: dev + strategy: + matrix: + elixir: ["1.15.7"] + otp: ["26.1.2"] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + id: beam + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-app: false + + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - name: Restore PLT cache + uses: actions/cache@v3 + id: plt_cache + with: + key: plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/*.ex') }} + restore-keys: | + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/*.ex') }} + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}- + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}- + path: priv/plts + + # Create PLTs if no cache was found. + # Always rebuild PLT when a job is retried + # (If they were cached at all, they'll be updated when we run mix dialyzer with no flags.) + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' || github.run_attempt != '1' + run: mix dialyzer --plt + + - name: Run Dialyzer + run: mix dialyzer --format github diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml new file mode 100644 index 0000000..d7bc24e --- /dev/null +++ b/.github/workflows/elixir-quality-checks.yml @@ -0,0 +1,44 @@ +name: Elixir Quality Checks + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + quality_checks: + name: Formatting, Credo, and Unused Deps + runs-on: ubuntu-latest + strategy: + matrix: + elixir: ["1.15.7"] + otp: ["26.1.2"] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-app: false + + - name: Check for unused deps + run: mix deps.unlock --check-unused + - name: Check code formatting + run: mix format --check-formatted + # Check formatting even if there were unused deps so that + # we give devs as much feedback as possible & save some time. + if: always() + - name: Run Credo + run: mix credo suggest --min-priority=normal + # Run Credo even if formatting or the unused deps check failed + if: always() + - name: Check for compile-time dependencies + run: mix xref graph --label compile-connected --fail-above 0 + if: always() diff --git a/.gitignore b/.gitignore index 755b605..7a2d509 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /deps erl_crash.dump *.ez +priv/plts/ diff --git a/.recode.exs b/.recode.exs new file mode 100644 index 0000000..de493ca --- /dev/null +++ b/.recode.exs @@ -0,0 +1,33 @@ +[ + version: "0.6.4", + # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. + autocorrect: true, + # With "--dry" no changes will be written to the files. + # Can also be set/reset with `--dry`/`--no-dry`. + # If dry is true then verbose is also active. + dry: false, + # Can also be set/reset with `--verbose`/`--no-verbose`. + verbose: false, + # Can be overwritten by calling `mix recode "lib/**/*.ex"`. + inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], + formatter: {Recode.Formatter, []}, + tasks: [ + # Tasks could be added by a tuple of the tasks module name and an options + # keyword list. A task can be deactivated by `active: false`. The execution of + # a deactivated task can be forced by calling `mix recode --task ModuleName`. + {Recode.Task.AliasExpansion, []}, + {Recode.Task.AliasOrder, []}, + {Recode.Task.Dbg, [autocorrect: false]}, + {Recode.Task.EnforceLineLength, [active: false]}, + {Recode.Task.FilterCount, []}, + {Recode.Task.IOInspect, [autocorrect: false]}, + {Recode.Task.Nesting, [active: false]}, + {Recode.Task.PipeFunOne, []}, + {Recode.Task.SinglePipe, []}, + {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]}, + {Recode.Task.TagFIXME, [exit_code: 2]}, + {Recode.Task.TagTODO, [exit_code: 4]}, + {Recode.Task.TestFileExt, []}, + {Recode.Task.UnusedVariable, [active: false]} + ] +] diff --git a/lib/whois/record.ex b/lib/whois/record.ex index 4b6327e..3c6be1b 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -116,7 +116,7 @@ defmodule Whois.Record do %{record | nameservers: nameservers, status: status} end - def split_key_and_value(line) do + defp split_key_and_value(line) do line |> String.trim() |> String.split(":", parts: 2, trim: true) diff --git a/mix.exs b/mix.exs index e5651f9..89270ca 100644 --- a/mix.exs +++ b/mix.exs @@ -1,25 +1,41 @@ defmodule Whois.Mixfile do use Mix.Project + @spec project() :: Keyword.t() def project do [ app: :whois, version: "0.1.1", - elixir: "~> 1.2", - build_embedded: Mix.env() == :prod, - start_permanent: Mix.env() == :prod, + elixir: "~> 1.12", + consolidate_protocols: Mix.env() != :test, elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), + aliases: aliases(), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + check: :test, + coveralls: :test, + "coveralls.detail": :test, + "coveralls.html": :test, + dialyzer: :dev + ], description: "Pure Elixir WHOIS client and parser.", - package: package() + package: package(), + dialyzer: [ + plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, + flags: [:error_handling, :unknown], + # Error out when an ignore rule is no longer useful so we can remove it + list_unused_filters: true + ] ] end # Configuration for the OTP application # # Type "mix help compile.app" for more information + @spec application() :: Keyword.t() def application do - [applications: [:logger]] + [extra_applications: [:logger]] end defp elixirc_paths(:test), do: ["lib", "test/support"] @@ -35,7 +51,13 @@ defmodule Whois.Mixfile do # # Type "mix help deps" for more examples and options defp deps do - [{:ex_doc, "~> 0.18.1", only: :dev}] + [ + {:ex_doc, "~> 0.18", only: :dev}, + {:credo, "~> 1.7.0", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, + {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, + {:recode, "~> 0.6", only: [:dev, :test]} + ] end defp package do @@ -45,4 +67,22 @@ defmodule Whois.Mixfile do links: %{GitHub: "https://github.com/utkarshkukreti/whois.ex"} ] end + + defp aliases do + [ + check: [ + "clean", + "deps.unlock --check-unused", + "compile --warnings-as-errors", + "test --warnings-as-errors", + "format --check-formatted", + "deps.unlock --check-unused", + "recode", + "check.circular", + "check.dialyzer" + ], + "check.circular": "cmd MIX_ENV=dev mix xref graph --label compile-connected --fail-above 0", + "check.dialyzer": "cmd MIX_ENV=dev mix dialyzer" + ] + end end diff --git a/mix.lock b/mix.lock index c5cc860..622686f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,19 @@ %{ - "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm", "3b1dcad3067985dd8618c38399a8ee9c4e652d52a17a4aae7a6d6fc4fcc24856"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "f050061c87ad39478c942995b5a20c40f2c0bc06525404613b8b0474cb8bd796"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, + "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "glob_ex": {:hex, :glob_ex, "0.1.6", "3a311ade50f6b71d638af660edcc844c3ab4eb2a2c816cfebb73a1d521bb2f9d", [:mix], [], "hexpm", "fda1e90e10f6029bd72967fef0c9891d0d14da89ca7163076e6028bfcb2c42fa"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "recode": {:hex, :recode, "0.6.5", "067335c383e807c1a6f5df22a92856f83e852f1acdbedaa2cced52df082dbe25", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}], "hexpm", "d03c66c1dd1ca5b19ad14a3a5fbb2218060352e91d4ad7925fb52f1915a4aabe"}, + "rewrite": {:hex, :rewrite, "0.10.0", "5d756b6dc67679e7156ff6055f9654be02dbaeb177aaf1ff6af7ee8da8718248", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.13", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "68d7808cf549e7bf51b0119a8edc14d50970bad479115249030baa580c1d7b50"}, + "sourceror": {:hex, :sourceror, "0.14.1", "c6fb848d55bd34362880da671debc56e77fd722fa13b4dcbeac89a8998fc8b09", [:mix], [], "hexpm", "8b488a219e4c4d7d9ff29d16346fd4a5858085ccdd010e509101e226bbfd8efc"}, } From b0003febea2d28d2e3f38bbb104d4784478cd95f Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 13:49:29 -0600 Subject: [PATCH 10/20] Pass through timeouts on TCP recv --- .github/workflows/elixir-build-and-test.yml | 2 +- lib/whois.ex | 48 ++++++++++----------- mix.exs | 3 +- mix.lock | 1 + test/whois/server_test.exs | 9 ++++ test/whois_test.exs | 12 ++++++ 6 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index 43da59d..a4154bd 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -46,7 +46,7 @@ jobs: build-flags: --all-warnings --warnings-as-errors - name: Run Tests - run: mix coveralls.json --warnings-as-errors + run: mix coveralls.json --warnings-as-errors --include live if: always() # Optional, but Codecov has a bot that will comment on your PR with per-file diff --git a/lib/whois.ex b/lib/whois.ex index bdb1211..9e67256 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -2,7 +2,8 @@ defmodule Whois do @moduledoc """ A WHOIS client for Elixir. """ - alias Whois.{Record, Server} + alias Whois.Record + alias Whois.Server @type lookup_option :: {:server, String.t() | Server.t()} | {:fall_back_to_iana?, boolean} @@ -23,7 +24,8 @@ defmodule Whois do iex> NaiveDateTime.after?(record.expires_at, NaiveDateTime.utc_now()) true """ - @spec lookup(String.t(), [lookup_option]) :: {:ok, Record.t()} | {:error, atom} + @spec lookup(String.t(), [lookup_option]) :: + {:ok, Record.t()} | {:error, :timed_out | :unsupported_tld} def lookup(domain, opts \\ []) do with {:ok, raw} <- lookup_raw(domain, opts), %Record{domain: d} = record when byte_size(d) > 0 <- Record.parse(raw) do @@ -52,40 +54,36 @@ defmodule Whois do :error -> Server.for(domain) end - case server do - {:ok, %Server{host: host}} -> - with {:ok, socket} <- - :gen_tcp.connect(String.to_charlist(host), 43, [:binary, active: false]), - :ok <- :gen_tcp.send(socket, [domain, "\r\n"]) do - raw = recv(socket) + with {:ok, %Server{host: host}} <- server, + {:ok, socket} <- + :gen_tcp.connect(String.to_charlist(host), 43, [:binary, active: false]), + :ok <- :gen_tcp.send(socket, [domain, "\r\n"]), + raw when is_binary(raw) <- recv(socket) do + case next_server(raw) do + nil -> + {:ok, raw} - case next_server(raw) do - nil -> - {:ok, raw} + "" -> + {:ok, raw} - "" -> - {:ok, raw} + ^host -> + {:ok, raw} - ^host -> - {:ok, raw} + next_server -> + opts = Keyword.put(opts, :server, next_server) - next_server -> - opts = opts |> Keyword.put(:server, next_server) - - with {:ok, raw2} <- lookup_raw(domain, opts) do - {:ok, raw <> raw2} - end + with {:ok, raw2} <- lookup_raw(domain, opts) do + {:ok, raw <> raw2} end - end - - {:error, _} = error -> - error + end end end + @spec recv(socket :: :gen_tcp.socket(), acc :: String.t()) :: String.t() | {:error, :timed_out} defp recv(socket, acc \\ "") do case :gen_tcp.recv(socket, 0) do {:ok, data} -> recv(socket, acc <> data) + {:error, :etimedout} -> {:error, :timed_out} {:error, :closed} -> acc end end diff --git a/mix.exs b/mix.exs index 89270ca..2b3a8e6 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,8 @@ defmodule Whois.Mixfile do {:credo, "~> 1.7.0", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, - {:recode, "~> 0.6", only: [:dev, :test]} + {:recode, "~> 0.6", only: [:dev, :test]}, + {:patch, "~> 0.13.0", only: [:test]} ] end diff --git a/mix.lock b/mix.lock index 622686f..4f4d377 100644 --- a/mix.lock +++ b/mix.lock @@ -13,6 +13,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "patch": {:hex, :patch, "0.13.0", "da48728f9086a835956200a671210fe88f67ff48bb1f92626989886493ac2081", [:mix], [], "hexpm", "d65a840d485dfa05bf6673269b56680e7537a05050684e713de125a351b28112"}, "recode": {:hex, :recode, "0.6.5", "067335c383e807c1a6f5df22a92856f83e852f1acdbedaa2cced52df082dbe25", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}], "hexpm", "d03c66c1dd1ca5b19ad14a3a5fbb2218060352e91d4ad7925fb52f1915a4aabe"}, "rewrite": {:hex, :rewrite, "0.10.0", "5d756b6dc67679e7156ff6055f9654be02dbaeb177aaf1ff6af7ee8da8718248", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.13", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "68d7808cf549e7bf51b0119a8edc14d50970bad479115249030baa580c1d7b50"}, "sourceror": {:hex, :sourceror, "0.14.1", "c6fb848d55bd34362880da671debc56e77fd722fa13b4dcbeac89a8998fc8b09", [:mix], [], "hexpm", "8b488a219e4c4d7d9ff29d16346fd4a5858085ccdd010e509101e226bbfd8efc"}, diff --git a/test/whois/server_test.exs b/test/whois/server_test.exs index f973980..e162c36 100644 --- a/test/whois/server_test.exs +++ b/test/whois/server_test.exs @@ -1,6 +1,11 @@ defmodule Whois.ServerTest do use ExUnit.Case, async: true + test "supports lots of TLDs" do + tlds = Whois.Server.all() + assert map_size(tlds) > 1000 + end + test "handles .com" do assert {:ok, %Whois.Server{host: host}} = Whois.Server.for("example.com") assert host == "whois.verisign-grs.com" @@ -11,4 +16,8 @@ defmodule Whois.ServerTest do assert {:ok, ^no_subdomain} = Whois.Server.for("foo.example.com") assert {:ok, ^no_subdomain} = Whois.Server.for("foo.bar.baz.example.com") end + + test "handles unsupported TLDs" do + assert {:error, :unsupported_tld} = Whois.Server.for("example.notatld") + end end diff --git a/test/whois_test.exs b/test/whois_test.exs index 1377022..40c8544 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -40,3 +40,15 @@ defmodule WhoisTest do defp wait, do: Process.sleep(2500) end + +defmodule WhoisSyncTest do + # Can't be async due to the use of Patch + use ExUnit.Case, async: false + use Patch + + @tag :live + test "handles timeouts" do + Patch.patch(:gen_tcp, :recv, {:error, :etimedout}) + assert Whois.lookup("google.com") == {:error, :timed_out} + end +end From dc12f112fab846c72aa1ba3c098c85d0ad6d4355 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Wed, 20 Dec 2023 13:50:51 -0600 Subject: [PATCH 11/20] Update Codecov action --- .github/workflows/elixir-build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index a4154bd..f2d631d 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -52,7 +52,7 @@ jobs: # Optional, but Codecov has a bot that will comment on your PR with per-file # coverage deltas. - name: Upload to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos files: ./cover/excoveralls.json From 4fa8fc7e2a117bf367b5b716280eb934386474c1 Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Wed, 20 Dec 2023 14:10:53 -0600 Subject: [PATCH 12/20] Add status badges to README (#3) * Add status badges to README * Run checks on merge to the main branch --- .github/workflows/elixir-build-and-test.yml | 1 + .github/workflows/elixir-dialyzer.yml | 1 + .github/workflows/elixir-quality-checks.yml | 1 + README.md | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index f2d631d..c28211e 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - master pull_request: branches: - '*' diff --git a/.github/workflows/elixir-dialyzer.yml b/.github/workflows/elixir-dialyzer.yml index 8c61eaa..8da41df 100644 --- a/.github/workflows/elixir-dialyzer.yml +++ b/.github/workflows/elixir-dialyzer.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - master pull_request: branches: - '*' diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml index d7bc24e..21456c0 100644 --- a/.github/workflows/elixir-quality-checks.yml +++ b/.github/workflows/elixir-quality-checks.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - master pull_request: branches: - '*' diff --git a/README.md b/README.md index 06264d1..b824a28 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Whois [![Build Status](https://travis-ci.org/utkarshkukreti/whois.ex.svg?branch=master)](https://travis-ci.org/utkarshkukreti/whois.ex) +# Whois [![Build and Test](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-build-and-test.yml) [![Elixir Quality Checks](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-quality-checks.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-quality-checks.yml) [![Elixir Type Linting](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-dialyzer.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-dialyzer.yml) [![Code coverage](https://codecov.io/gh/s3cur3/whois.ex/graph/badge.svg?token=Xe9iuK8f63)](https://codecov.io/gh/s3cur3/whois.ex) Pure Elixir WHOIS client and parser. From 17eb0fcd66c9096f96e0929e297efe6a1115e523 Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Thu, 21 Dec 2023 08:44:19 -0600 Subject: [PATCH 13/20] Pull out the ill-advised IANA fallback, and return a clear error when no data is provided (#4) * Pull out the ill-advised IANA fallback, and return a clear error when no data is provided * Don't run live tests in CI * Fix Credo * Fix formatting --- .github/workflows/elixir-build-and-test.yml | 2 +- .github/workflows/elixir-quality-checks.yml | 3 -- .travis.yml | 2 +- coveralls.json | 5 +++ lib/whois.ex | 37 ++++++++-------- lib/whois/contact.ex | 20 ++++----- lib/whois/record.ex | 23 +++++++--- mix.exs | 11 ++--- test/fixtures/raw/jasstafel.michaelruoss.ch | 49 +-------------------- test/support/record_fixtures.ex | 15 +++++++ test/whois/record_test.exs | 28 +++--------- test/whois_test.exs | 41 ++++++++++++++--- 12 files changed, 119 insertions(+), 117 deletions(-) create mode 100644 coveralls.json create mode 100644 test/support/record_fixtures.ex diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index c28211e..3b4862f 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -47,7 +47,7 @@ jobs: build-flags: --all-warnings --warnings-as-errors - name: Run Tests - run: mix coveralls.json --warnings-as-errors --include live + run: mix coveralls.json --warnings-as-errors if: always() # Optional, but Codecov has a bot that will comment on your PR with per-file diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml index 21456c0..54d81af 100644 --- a/.github/workflows/elixir-quality-checks.yml +++ b/.github/workflows/elixir-quality-checks.yml @@ -40,6 +40,3 @@ jobs: run: mix credo suggest --min-priority=normal # Run Credo even if formatting or the unused deps check failed if: always() - - name: Check for compile-time dependencies - run: mix xref graph --label compile-connected --fail-above 0 - if: always() diff --git a/.travis.yml b/.travis.yml index 34ee896..b4d2d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: elixir -script: mix test --include live +script: mix test elixir: 1.5 otp_release: 20.1 diff --git a/coveralls.json b/coveralls.json new file mode 100644 index 0000000..520c804 --- /dev/null +++ b/coveralls.json @@ -0,0 +1,5 @@ +{ + "skip_files": [ + "test/support/fetch.ex" + ] +} diff --git a/lib/whois.ex b/lib/whois.ex index 9e67256..f78d5fa 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -2,47 +2,48 @@ defmodule Whois do @moduledoc """ A WHOIS client for Elixir. """ + import Whois.Record, only: [is_empty: 1] alias Whois.Record alias Whois.Server - @type lookup_option :: {:server, String.t() | Server.t()} | {:fall_back_to_iana?, boolean} + @type lookup_option :: {:server, String.t() | Server.t()} @doc """ Queries the appropriate WHOIS server for the domain. + Returns `{:ok, record}` if we were able to look up WHOIS records (at the minimum, + the date the domain was registered). + + Note that for some TLDs (especially country-specific TLDs in the European Union), + WHOIS information is considered private, and the respective WHOIS servers will return + limited information, or even none at all (resulting in `{:error, :no_data_provided}`). + For this reason, it's not generally possible to distinguish between cases where the + domain is registered (but our WHOIS queries are blocked), versus cases where the domain + is not registered at all. + ### Options - server: the WHOIS server to query. If not specified, we'll automatically choose the appropriate server. - - fall_back_to_iana?: whether to fall back to the IANA WHOIS server if looking - up the domain on the specified or default server fails. Defaults to `true` - whenever the `:server` is not specified. ### Examples iex> {:ok, %Whois.Record{domain: "google.com"} = record} = Whois.lookup("google.com") iex> NaiveDateTime.after?(record.expires_at, NaiveDateTime.utc_now()) true + + iex> Whois.lookup("scha.ch") + {:error, :no_data_provided} """ @spec lookup(String.t(), [lookup_option]) :: - {:ok, Record.t()} | {:error, :timed_out | :unsupported_tld} + {:ok, Record.t()} | {:error, :no_data_provided | :timed_out | :unsupported_tld} def lookup(domain, opts \\ []) do with {:ok, raw} <- lookup_raw(domain, opts), - %Record{domain: d} = record when byte_size(d) > 0 <- Record.parse(raw) do + %Record{} = record when not is_empty(record) <- Record.parse(raw) do {:ok, record} else - %Record{} = record -> - # We connected to the server, but got a totally garbage raw response, like: - # > Requests of this client are not permitted. Please use https://www.nic.ch/whois/ for queries." - # Unless the server was specified, we'll fall back to the IANA server. - if opts[:fall_back_to_iana?] || is_nil(opts[:server]) do - lookup(domain, server: "whois.iana.org", fall_back_to_iana?: false) - else - {:ok, record} - end - - other -> - other + %Record{} -> {:error, :no_data_provided} + other -> other end end diff --git a/lib/whois/contact.ex b/lib/whois/contact.ex index cac1bf3..7852af8 100644 --- a/lib/whois/contact.ex +++ b/lib/whois/contact.ex @@ -5,15 +5,15 @@ defmodule Whois.Contact do defstruct [:name, :organization, :street, :city, :state, :zip, :country, :phone, :fax, :email] @type t :: %__MODULE__{ - name: String.t(), - organization: String.t(), - street: String.t(), - city: String.t(), - state: String.t(), - zip: String.t(), - country: String.t(), - phone: String.t(), - fax: String.t(), - email: String.t() + name: String.t() | nil, + organization: String.t() | nil, + street: String.t() | nil, + city: String.t() | nil, + state: String.t() | nil, + zip: String.t() | nil, + country: String.t() | nil, + phone: String.t() | nil, + fax: String.t() | nil, + email: String.t() | nil } end diff --git a/lib/whois/record.ex b/lib/whois/record.ex index 3c6be1b..b9aa3be 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -16,15 +16,19 @@ defmodule Whois.Record do :contacts ] + defguard is_empty(record) + when (not is_binary(record.domain) or byte_size(record.domain) == 0) and + is_nil(record.created_at) + @type t :: %__MODULE__{ - domain: String.t(), + domain: String.t() | nil, raw: String.t(), nameservers: [String.t()], status: [String.t()], - registrar: String.t(), - created_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t(), - expires_at: NaiveDateTime.t(), + registrar: String.t() | nil, + created_at: NaiveDateTime.t() | nil, + updated_at: NaiveDateTime.t() | nil, + expires_at: NaiveDateTime.t() | nil, contacts: %{ registrant: Contact.t(), administrator: Contact.t(), @@ -130,6 +134,15 @@ defmodule Whois.Record do end defp parse_dt(string) do + with {:ok, datetime, _} <- DateTime.from_iso8601(string), + {:ok, utc} <- DateTime.shift_zone(datetime, "Etc/UTC") do + DateTime.to_naive(utc) + else + _ -> parse_naive_dt(string) + end + end + + defp parse_naive_dt(string) do case NaiveDateTime.from_iso8601(string) do {:ok, datetime} -> datetime {:error, :invalid_format} -> parse_date_as_dt(string) diff --git a/mix.exs b/mix.exs index 2b3a8e6..553f433 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,10 @@ defmodule Whois.Mixfile do elixirc_paths: elixirc_paths(Mix.env()), deps: deps(), aliases: aliases(), - test_coverage: [tool: ExCoveralls], + test_coverage: [ + tool: ExCoveralls, + ignore_modules: [~r/Mix.Tasks.Whois/] + ], preferred_cli_env: [ check: :test, coveralls: :test, @@ -78,11 +81,9 @@ defmodule Whois.Mixfile do "test --warnings-as-errors", "format --check-formatted", "deps.unlock --check-unused", - "recode", - "check.circular", - "check.dialyzer" + "check.dialyzer", + "recode" ], - "check.circular": "cmd MIX_ENV=dev mix xref graph --label compile-connected --fail-above 0", "check.dialyzer": "cmd MIX_ENV=dev mix dialyzer" ] end diff --git a/test/fixtures/raw/jasstafel.michaelruoss.ch b/test/fixtures/raw/jasstafel.michaelruoss.ch index a90b457..95fcb23 100644 --- a/test/fixtures/raw/jasstafel.michaelruoss.ch +++ b/test/fixtures/raw/jasstafel.michaelruoss.ch @@ -1,48 +1 @@ -% IANA WHOIS server -% for more information on IANA, visit http://www.iana.org -% This query returned 1 object - -refer: whois.nic.ch - -domain: CH - -organisation: SWITCH The Swiss Education & Research Network -address: Werdstrasse 2 -address: Zurich CH-8021 -address: Switzerland - -contact: administrative -name: SWITCH TLD Administration -organisation: SWITCH The Swiss Education & Research Network -address: Werdstrasse 2 -address: Zurich CH-8021 -address: Switzerland -phone: +41 44 268 15 40 -fax-no: +41 44 268 15 78 -e-mail: tld-admin@switch.ch - -contact: technical -name: DNS Operations -organisation: SWITCH The Swiss Education & Research Network -address: Werdstrasse 2 -address: Zurich CH-8021 -address: Switzerland -phone: +41 44 268 15 40 -fax-no: +41 44 268 15 78 -e-mail: dns-operation@switch.ch - -nserver: A.NIC.CH 130.59.31.41 2001:620:0:ff:0:0:0:56 -nserver: B.NIC.CH 130.59.31.43 2001:620:0:ff:0:0:0:58 -nserver: D.NIC.CH 194.0.25.39 2001:678:20:0:0:0:0:39 -nserver: E.NIC.CH 194.0.17.1 2001:678:3:0:0:0:0:1 -nserver: F.NIC.CH 194.146.106.10 2001:67c:1010:2:0:0:0:53 -ds-rdata: 450 13 2 4994913d9ff4f0df95f08acdf1d66149d873368ff6c1836c64bdface62734fa2 - -whois: whois.nic.ch - -status: ACTIVE -remarks: Registration information: https://www.nic.ch/ - -created: 1987-05-20 -changed: 2023-11-30 -source: IANA +Requests of this client are not permitted. Please use https://www.nic.ch/whois/ for queries. diff --git a/test/support/record_fixtures.ex b/test/support/record_fixtures.ex new file mode 100644 index 0000000..7cb3ee0 --- /dev/null +++ b/test/support/record_fixtures.ex @@ -0,0 +1,15 @@ +defmodule Whois.RecordFixtures do + @moduledoc false + + def record_fixture(domain) do + "../fixtures/raw/#{domain}" + |> Path.expand(__DIR__) + |> File.read!() + end + + def parsed_record_fixture(domain) do + domain + |> record_fixture() + |> Whois.Record.parse() + end +end diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index 5fde81f..f290025 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -137,32 +137,18 @@ defmodule Whois.RecordTest do refute record.expires_at end - test "parse jasstafel.michaelruoss.ch" do + test "parse a domain that refuses bot queries" do record = parse("jasstafel.michaelruoss.ch") - - # This is a quirk of the IANA server - assert record.domain == "CH" - - assert record.nameservers == [ - "a.nic.ch 130.59.31.41 2001:620:0:ff:0:0:0:56", - "b.nic.ch 130.59.31.43 2001:620:0:ff:0:0:0:58", - "d.nic.ch 194.0.25.39 2001:678:20:0:0:0:0:39", - "e.nic.ch 194.0.17.1 2001:678:3:0:0:0:0:1", - "f.nic.ch 194.146.106.10 2001:67c:1010:2:0:0:0:53" - ] - + assert Whois.Record.is_empty(record) + refute record.domain + assert record.nameservers == [] refute record.registrar - assert_dt(record.created_at, ~D[1987-05-20]) - assert_dt(record.updated_at, ~D[2023-11-30]) + refute record.created_at + refute record.updated_at refute record.expires_at end - defp parse(domain) do - "../fixtures/raw/#{domain}" - |> Path.expand(__DIR__) - |> File.read!() - |> Whois.Record.parse() - end + defp parse(domain), do: Whois.RecordFixtures.parsed_record_fixture(domain) defp assert_dt(%NaiveDateTime{} = datetime, date) do assert NaiveDateTime.to_date(datetime) == date diff --git a/test/whois_test.exs b/test/whois_test.exs index 40c8544..b83a63d 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -31,11 +31,20 @@ defmodule WhoisTest do end @tag :live - test "lookup/2 falls back to IANA for TLDs that aren't amenable to robots" do - assert {:ok, record} = Whois.lookup("michaelruoss.ch") + test "lookup/2 provides a clear error for live TLDs that aren't amenable to robots" do + assert {:error, :no_data_provided} = Whois.lookup("michaelruoss.ch") + end + + defp wait, do: Process.sleep(2500) +end - # This is a quirk of the IANA WHOIS server - assert record.domain == "CH" +defmodule WhoisDocTest do + use ExUnit.Case, async: true + @moduletag :live + doctest Whois + + setup do + wait() end defp wait, do: Process.sleep(2500) @@ -46,9 +55,31 @@ defmodule WhoisSyncTest do use ExUnit.Case, async: false use Patch - @tag :live test "handles timeouts" do + Patch.patch(:gen_tcp, :connect, {:ok, %{}}) + Patch.patch(:gen_tcp, :send, :ok) Patch.patch(:gen_tcp, :recv, {:error, :etimedout}) assert Whois.lookup("google.com") == {:error, :timed_out} end + + test "handles usable records" do + Patch.patch(Whois, :lookup_raw, {:ok, Whois.RecordFixtures.record_fixture("google.com")}) + + assert {:ok, record} = Whois.lookup("google.com") + assert record.domain == "google.com" + assert record.created_at == ~N[1997-09-15T07:00:00] + assert record.expires_at == ~N[2020-09-14T04:00:00] + assert record.updated_at == ~N[2017-09-07T15:50:36] + + for type <- [:administrator, :registrant, :technical] do + assert record.contacts[type].organization == "Google Inc." + assert record.contacts[type].state == "CA" + assert record.contacts[type].country == "US" + end + end + + test "lookup/2 provides a clear error for TLDs that aren't supported" do + Patch.patch(Whois, :lookup_raw, {:ok, "Automated lookup for this domain is blocked."}) + assert {:error, :no_data_provided} = Whois.lookup("unsupported.com") + end end From 537515bea32c7436aae13611a537ab3ebdfac72e Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Thu, 21 Dec 2023 12:44:32 -0600 Subject: [PATCH 14/20] Don't run doctest in CI (#5) --- test/whois_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/whois_test.exs b/test/whois_test.exs index b83a63d..6b788d3 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -1,6 +1,5 @@ defmodule WhoisTest do use ExUnit.Case, async: true - doctest Whois setup do wait() From 79b210f32fe92199b01053e5677ad8375aa2001d Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Wed, 3 Jan 2024 19:58:39 -0600 Subject: [PATCH 15/20] Fix handling of .im domains (#6) --- lib/whois/record.ex | 20 ++++++++++++++++++-- mix.exs | 1 + mix.lock | 2 ++ test/fixtures/raw/wheel.im | 27 +++++++++++++++++++++++++++ test/whois/record_test.exs | 17 +++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/raw/wheel.im diff --git a/lib/whois/record.ex b/lib/whois/record.ex index b9aa3be..866d94f 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -80,7 +80,7 @@ defmodule Whois.Record do u when u in ["updated date", "modified", "last updated", "changed"] -> %{record | updated_at: parse_dt(value) || record.updated_at} - e when e in ["expiration date", "expires", "registry expiry date"] -> + e when e in ["expiration date", "expires", "registry expiry date", "expiry date"] -> %{record | expires_at: parse_dt(value) || record.expires_at} "registrant " <> name -> @@ -154,7 +154,23 @@ defmodule Whois.Record do {:ok, datetime} <- NaiveDateTime.new(date, Time.new!(0, 0, 0)) do datetime else - _ -> nil + _ -> guess_date(string) + end + end + + defp guess_date(string) do + case DateTimeParser.parse_datetime(string) do + {:ok, %NaiveDateTime{} = naive} -> + naive + + {:ok, %DateTime{} = dt} -> + case DateTime.shift_zone(dt, "Etc/UTC") do + {:ok, utc} -> DateTime.to_naive(utc) + {:error, _} -> nil + end + + {:error, _} -> + nil end end diff --git a/mix.exs b/mix.exs index 553f433..00b5b3c 100644 --- a/mix.exs +++ b/mix.exs @@ -57,6 +57,7 @@ defmodule Whois.Mixfile do [ {:ex_doc, "~> 0.18", only: :dev}, {:credo, "~> 1.7.0", only: [:dev, :test], runtime: false}, + {:date_time_parser, "~> 1.2"}, {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, {:recode, "~> 0.6", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index 4f4d377..c19dc0e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "date_time_parser": {:hex, :date_time_parser, "1.2.0", "3d5a816b91967f51e0f94dcb16a34b2cb780f22cd48931779e81d72f7d3eadb1", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "0cf09ada9f42c0b3bfba02dc0ea2e4b4d2f543d9d2bf99b831a29e6b4a4160e5"}, "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, @@ -9,6 +10,7 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "glob_ex": {:hex, :glob_ex, "0.1.6", "3a311ade50f6b71d638af660edcc844c3ab4eb2a2c816cfebb73a1d521bb2f9d", [:mix], [], "hexpm", "fda1e90e10f6029bd72967fef0c9891d0d14da89ca7163076e6028bfcb2c42fa"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "kday": {:hex, :kday, "1.0.2", "e96035c439323eeb8505268959122e9d30194a4e5a23a357dc75f1cae696e918", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "f040b9b6de21eea4a96dbf71753f4eeb0772a6ebd6c18cacd114694af7cabc9a"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, diff --git a/test/fixtures/raw/wheel.im b/test/fixtures/raw/wheel.im new file mode 100644 index 0000000..d16f995 --- /dev/null +++ b/test/fixtures/raw/wheel.im @@ -0,0 +1,27 @@ +Domain Name: wheel.im +Domain Managers +Name: Redacted +Address +Redacted +Domain Owners / Registrant +Name: Redacted +Address +Redacted +Administrative Contact +Name: Redacted +Address +Redacted +Billing Contact +Name: Redacted +Address +Redacted +Technical Contact +Name: Redacted +Address +Redacted +Domain Details +Expiry Date: 29/02/2024 00:59:50 +Name Server:ns-1031.awsdns-00.org. +Name Server:ns-1904.awsdns-46.co.uk. +Name Server:ns-297.awsdns-37.com. +Name Server:ns-621.awsdns-13.net. diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index f290025..e9ff9a9 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -137,6 +137,23 @@ defmodule Whois.RecordTest do refute record.expires_at end + test "parse wheel.im" do + record = parse("wheel.im") + assert record.domain == "wheel.im" + + assert record.nameservers == [ + "ns-1031.awsdns-00.org.", + "ns-1904.awsdns-46.co.uk.", + "ns-297.awsdns-37.com.", + "ns-621.awsdns-13.net." + ] + + refute record.registrar + refute record.created_at + refute record.updated_at + assert_dt(record.expires_at, ~D[2024-02-29]) + end + test "parse a domain that refuses bot queries" do record = parse("jasstafel.michaelruoss.ch") assert Whois.Record.is_empty(record) From 1aaf7373938d9714b48dcc0eb1de19e0c03320a4 Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Thu, 4 Jan 2024 10:44:25 -0600 Subject: [PATCH 16/20] Fix .africa domains, where the server from the Ruby whois reference is rejecting connections (#7) --- README.md | 13 +++++++++++++ lib/whois/server.ex | 9 +++++++++ priv/Makefile | 4 ++-- test/whois_test.exs | 8 ++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b824a28..59547b0 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,16 @@ iex(1)> Whois.lookup("google.com") updated_at: ~N[2018-02-21 10:45:07] }} ``` + +## Development + +### Updating the list of Whois servers + +The `priv` directory contains a Makefile that will download the latest TLD reference file from the web and parse it into a structure we can use at compile time. Run it like this: + +```sh +cd priv +make clean +make tld.json +make tld.csv +``` diff --git a/lib/whois/server.ex b/lib/whois/server.ex index 337921d..9c99389 100644 --- a/lib/whois/server.ex +++ b/lib/whois/server.ex @@ -4,6 +4,11 @@ defmodule Whois.Server do @type t :: %__MODULE__{host: String.t()} + @overrides Map.new( + %{"africa" => "whois.nic.africa"}, + fn {tld, host} -> {tld, %{__struct__: __MODULE__, host: host}} end + ) + @all File.read!(Application.app_dir(:whois, "priv/tld.csv")) |> String.trim() |> String.split("\n") @@ -12,7 +17,11 @@ defmodule Whois.Server do {tld, %{__struct__: __MODULE__, host: host}} end) |> Map.new() + |> Map.merge(@overrides) + @doc """ + A map from TLD to the WHOIS server we'll query by default. + """ @spec all :: map def all, do: @all diff --git a/priv/Makefile b/priv/Makefile index 7ddf8b4..d52dae2 100644 --- a/priv/Makefile +++ b/priv/Makefile @@ -1,10 +1,10 @@ default: tld.csv tld.json: - curl -s -L https://github.com/weppos/whois/raw/master/data/tld.json > tld.json + curl -s -L https://github.com/weppos/whois/raw/main/data/tld.json > tld.json tld.csv: tld.json cat tld.json | jq -r 'to_entries | .[] | select(.value.host) | .key + "," + .value.host' > tld.csv clean: - rm -f tld.json tld.csv \ No newline at end of file + rm -f tld.json tld.csv diff --git a/test/whois_test.exs b/test/whois_test.exs index 6b788d3..af4473d 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -34,6 +34,14 @@ defmodule WhoisTest do assert {:error, :no_data_provided} = Whois.lookup("michaelruoss.ch") end + @tag :live + test "lookup/1 can check .africa domains" do + assert {:ok, record} = Whois.lookup("mche.africa") + assert record.domain == "mche.africa" + assert record.created_at == ~N[2023-12-20T07:45:20.00] + assert %NaiveDateTime{} = record.expires_at + end + defp wait, do: Process.sleep(2500) end From f455a39d0f80be97f367eea1dcbf6ad890a5be0c Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Thu, 4 Jan 2024 12:27:11 -0600 Subject: [PATCH 17/20] Fix handling of referral servers that end in a / (#8) * Fix handling of referral servers that end in a / * Slightly improve parsing for tvnet.lv --- lib/whois.ex | 7 ++++++- lib/whois/record.ex | 4 ++-- test/whois_test.exs | 8 ++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/whois.ex b/lib/whois.ex index f78d5fa..0f79583 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -71,7 +71,12 @@ defmodule Whois do {:ok, raw} next_server -> - opts = Keyword.put(opts, :server, next_server) + next_host = + next_server + |> String.split("/") + |> List.first() + + opts = Keyword.put(opts, :server, next_host) with {:ok, raw2} <- lookup_raw(domain, opts) do {:ok, raw <> raw2} diff --git a/lib/whois/record.ex b/lib/whois/record.ex index 866d94f..b71f2a0 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -60,7 +60,7 @@ defmodule Whois.Record do {key, value} -> case String.downcase(key) do n when n in ["domain name", "domain"] -> - %{record | domain: value} + %{record | domain: String.downcase(value)} ns when ns in ["name server", "nserver"] -> %{record | nameservers: record.nameservers ++ [value]} @@ -77,7 +77,7 @@ defmodule Whois.Record do c when c in ["creation date", "created"] -> %{record | created_at: parse_dt(value) || record.created_at} - u when u in ["updated date", "modified", "last updated", "changed"] -> + u when u in ["updated", "updated date", "modified", "last updated", "changed"] -> %{record | updated_at: parse_dt(value) || record.updated_at} e when e in ["expiration date", "expires", "registry expiry date", "expiry date"] -> diff --git a/test/whois_test.exs b/test/whois_test.exs index af4473d..254e88a 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -42,6 +42,14 @@ defmodule WhoisTest do assert %NaiveDateTime{} = record.expires_at end + @tag :live + test "lookup/1 can check .me domains" do + assert {:ok, record} = Whois.lookup("aswinmohan.me") + assert record.domain == "aswinmohan.me" + assert record.created_at == ~N[2019-10-19 13:16:19] + assert %NaiveDateTime{} = record.expires_at + end + defp wait, do: Process.sleep(2500) end From c89f8decf1e6a6fd4c0038b503e59f9012f5e24f Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Fri, 5 Jan 2024 10:20:53 -0600 Subject: [PATCH 18/20] Add support for .io, .com.au, and .de domains (#9) * Add support for .io, .com.au, and .de domains * Fix test --- lib/whois.ex | 40 +++++++++++++----- lib/whois/record.ex | 12 +++++- test/fixtures/raw/rumdash.io | 56 ++++++++++++++++++++++++++ test/fixtures/raw/thinkactively.com.au | 21 ++++++++++ test/fixtures/raw/zweitag.de | 24 +++++++++++ test/whois/record_test.exs | 42 +++++++++++++++++++ test/whois_test.exs | 28 +++++++++++++ 7 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/raw/rumdash.io create mode 100644 test/fixtures/raw/thinkactively.com.au create mode 100644 test/fixtures/raw/zweitag.de diff --git a/lib/whois.ex b/lib/whois.ex index 0f79583..1ac1fc2 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -55,10 +55,10 @@ defmodule Whois do :error -> Server.for(domain) end - with {:ok, %Server{host: host}} <- server, + with {:ok, %Server{host: host} = server} <- server, {:ok, socket} <- :gen_tcp.connect(String.to_charlist(host), 43, [:binary, active: false]), - :ok <- :gen_tcp.send(socket, [domain, "\r\n"]), + :ok <- :gen_tcp.send(socket, [query(server, domain), "\r\n"]), raw when is_binary(raw) <- recv(socket) do case next_server(raw) do nil -> @@ -70,15 +70,8 @@ defmodule Whois do ^host -> {:ok, raw} - next_server -> - next_host = - next_server - |> String.split("/") - |> List.first() - - opts = Keyword.put(opts, :server, next_host) - - with {:ok, raw2} <- lookup_raw(domain, opts) do + next -> + with {:ok, raw2} <- lookup_raw(domain, [{:server, next} | opts]) do {:ok, raw <> raw2} end end @@ -94,6 +87,17 @@ defmodule Whois do end end + # Denic.de says: + # + # > To query the status of a domain, please use whois.denic – to query the technical data and + # > the date of the last change to the domain data please use + # > "whois -h whois.denic.de -T dn ". + # + # https://www.denic.de/en/service/whois-service/ + defp query(%Server{host: "whois.denic.de"}, domain), do: "-T dn #{domain}" + + defp query(_, domain), do: domain + defp next_server(raw) do raw |> String.split("\n") @@ -102,10 +106,24 @@ defmodule Whois do |> String.trim() |> String.downcase() |> case do + "whois:" <> host -> String.trim(host) "whois server:" <> host -> String.trim(host) "registrar whois server:" <> host -> String.trim(host) _ -> nil end end) + |> case do + "http://" <> _ = url -> URI.parse(url).host + "https://" <> _ = url -> URI.parse(url).host + host when is_binary(host) -> remove_trailing_path(host) + nil -> nil + end + end + + # Handles non-URL cases like "godaddy.com/" + defp remove_trailing_path(next_server) do + next_server + |> String.split("/") + |> List.first() end end diff --git a/lib/whois/record.ex b/lib/whois/record.ex index b71f2a0..bfaf8f1 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -68,7 +68,7 @@ defmodule Whois.Record do s when s in ["domain status", "status"] -> %{record | status: record.status ++ [value]} - r when r in ["registrar", "registrar handle"] -> + r when r in ["registrar", "registrar handle", "registrar name"] -> %{record | registrar: value} "sponsoring registrar" -> @@ -77,7 +77,15 @@ defmodule Whois.Record do c when c in ["creation date", "created"] -> %{record | created_at: parse_dt(value) || record.created_at} - u when u in ["updated", "updated date", "modified", "last updated", "changed"] -> + u + when u in [ + "updated", + "updated date", + "modified", + "last updated", + "changed", + "last modified" + ] -> %{record | updated_at: parse_dt(value) || record.updated_at} e when e in ["expiration date", "expires", "registry expiry date", "expiry date"] -> diff --git a/test/fixtures/raw/rumdash.io b/test/fixtures/raw/rumdash.io new file mode 100644 index 0000000..e332e43 --- /dev/null +++ b/test/fixtures/raw/rumdash.io @@ -0,0 +1,56 @@ +Domain Name: rumdash.io +Registry Domain ID: bfd4358fa39c45e180f51bf420ca0fd0-DONUTS +Registrar WHOIS Server: http://whois.cloudflare.com +Registrar URL: http://cloudflare.com +Updated Date: 2023-10-26T20:56:46Z +Creation Date: 2022-11-20T17:43:37Z +Registry Expiry Date: 2024-11-20T17:43:37Z +Registrar: Cloudflare, Inc +Registrar IANA ID: 1910 +Registrar Abuse Contact Email: +Registrar Abuse Contact Phone: +Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited +Registry Registrant ID: REDACTED FOR PRIVACY +Registrant Name: REDACTED FOR PRIVACY +Registrant Organization: +Registrant Street: REDACTED FOR PRIVACY +Registrant City: REDACTED FOR PRIVACY +Registrant State/Province: California +Registrant Postal Code: REDACTED FOR PRIVACY +Registrant Country: US +Registrant Phone: REDACTED FOR PRIVACY +Registrant Phone Ext: REDACTED FOR PRIVACY +Registrant Fax: REDACTED FOR PRIVACY +Registrant Fax Ext: REDACTED FOR PRIVACY +Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name. +Registry Admin ID: REDACTED FOR PRIVACY +Admin Name: REDACTED FOR PRIVACY +Admin Organization: REDACTED FOR PRIVACY +Admin Street: REDACTED FOR PRIVACY +Admin City: REDACTED FOR PRIVACY +Admin State/Province: REDACTED FOR PRIVACY +Admin Postal Code: REDACTED FOR PRIVACY +Admin Country: REDACTED FOR PRIVACY +Admin Phone: REDACTED FOR PRIVACY +Admin Phone Ext: REDACTED FOR PRIVACY +Admin Fax: REDACTED FOR PRIVACY +Admin Fax Ext: REDACTED FOR PRIVACY +Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name. +Registry Tech ID: REDACTED FOR PRIVACY +Tech Name: REDACTED FOR PRIVACY +Tech Organization: REDACTED FOR PRIVACY +Tech Street: REDACTED FOR PRIVACY +Tech City: REDACTED FOR PRIVACY +Tech State/Province: REDACTED FOR PRIVACY +Tech Postal Code: REDACTED FOR PRIVACY +Tech Country: REDACTED FOR PRIVACY +Tech Phone: REDACTED FOR PRIVACY +Tech Phone Ext: REDACTED FOR PRIVACY +Tech Fax: REDACTED FOR PRIVACY +Tech Fax Ext: REDACTED FOR PRIVACY +Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name. +Name Server: tara.ns.cloudflare.com +Name Server: clyde.ns.cloudflare.com +DNSSEC: unsigned +URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/ +>>> Last update of WHOIS database: 2024-01-05T15:33:07Z <<< diff --git a/test/fixtures/raw/thinkactively.com.au b/test/fixtures/raw/thinkactively.com.au new file mode 100644 index 0000000..922e0f9 --- /dev/null +++ b/test/fixtures/raw/thinkactively.com.au @@ -0,0 +1,21 @@ +Domain Name: THINKACTIVELY.COM.AU +Registry Domain ID: D407400000001139569-AU +Registrar WHOIS Server: whois.auda.org.au +Registrar URL: https://synergywholesale.com/partner-lookup +Last Modified: 2023-09-24T04:39:42Z +Registrar Name: SYNERGY WHOLESALE ACCREDITATIONS PTY LTD +Registrar Abuse Contact Email: registry-abuse@nexigen.digital +Registrar Abuse Contact Phone: +61.383999483 +Reseller Name: +Status: serverRenewProhibited https://identitydigital.au/get-au/whois-status-codes#serverRenewProhibited +Status Reason: Not Currently Eligible For Renewal +Registrant Contact ID: 12222222222 +Registrant Contact Name: John Doe +Tech Contact ID: 19999999999999 +Tech Contact Name: Jane Doe +Name Server: DNS1.ALWAYSDATA.COM +Name Server: DNS2.ALWAYSDATA.COM +DNSSEC: unsigned +Registrant: PB & DB PTY LTD +Registrant ID: ABN 27631750493 +Eligibility Type: Company diff --git a/test/fixtures/raw/zweitag.de b/test/fixtures/raw/zweitag.de new file mode 100644 index 0000000..d07e4b0 --- /dev/null +++ b/test/fixtures/raw/zweitag.de @@ -0,0 +1,24 @@ +% Restricted rights. +% +% Terms and Conditions of Use +% +% The above data may only be used within the scope of technical or +% administrative necessities of Internet operation or to remedy legal +% problems. +% The use for other purposes, in particular for advertising, is not permitted. +% +% The DENIC whois service on port 43 doesn't disclose any information concerning +% the domain holder, general request and abuse contact. +% This information can be obtained through use of our web-based whois service +% available at the DENIC website: +% http://www.denic.de/en/domains/whois-service/web-whois.html +% +% + +Domain: zweitag.de +Nserver: ns1.zweitag.de 205.251.197.248 +Nserver: ns2.zweitag.de 205.251.198.68 +Nserver: ns3.zweitag.de 205.251.194.50 +Nserver: ns4.zweitag.de 205.251.192.69 +Status: connect +Changed: 2022-06-20T22:15:54+02:00 diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index e9ff9a9..a268ad1 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -165,6 +165,48 @@ defmodule Whois.RecordTest do refute record.expires_at end + test "parse a .io domain" do + record = parse("rumdash.io") + refute Whois.Record.is_empty(record) + assert record.domain == "rumdash.io" + assert record.status == ["clientTransferProhibited"] + assert record.nameservers == ["tara.ns.cloudflare.com", "clyde.ns.cloudflare.com"] + assert record.registrar == "Cloudflare, Inc" + assert_dt(record.created_at, ~D[2022-11-20]) + assert_dt(record.updated_at, ~D[2023-10-26]) + assert_dt(record.expires_at, ~D[2024-11-20]) + end + + test "parse a .com.au domain" do + record = parse("thinkactively.com.au") + refute Whois.Record.is_empty(record) + assert record.domain == "thinkactively.com.au" + assert record.status == ["serverRenewProhibited"] + assert record.nameservers == ["dns1.alwaysdata.com", "dns2.alwaysdata.com"] + assert record.registrar == "SYNERGY WHOLESALE ACCREDITATIONS PTY LTD" + refute record.created_at + assert_dt(record.updated_at, ~D[2023-09-24]) + refute record.expires_at + end + + test "parse a .de domain" do + record = parse("zweitag.de") + refute Whois.Record.is_empty(record) + assert record.domain == "zweitag.de" + + assert record.nameservers == [ + "ns1.zweitag.de 205.251.197.248", + "ns2.zweitag.de 205.251.198.68", + "ns3.zweitag.de 205.251.194.50", + "ns4.zweitag.de 205.251.192.69" + ] + + refute record.registrar + refute record.created_at + assert_dt(record.updated_at, ~D[2022-06-20]) + refute record.expires_at + end + defp parse(domain), do: Whois.RecordFixtures.parsed_record_fixture(domain) defp assert_dt(%NaiveDateTime{} = datetime, date) do diff --git a/test/whois_test.exs b/test/whois_test.exs index 254e88a..a0ffd5d 100644 --- a/test/whois_test.exs +++ b/test/whois_test.exs @@ -50,6 +50,34 @@ defmodule WhoisTest do assert %NaiveDateTime{} = record.expires_at end + @tag :live + test "lookup/1 can check .de domains" do + assert {:ok, record} = Whois.lookup("spiegel.de") + assert record.domain == "spiegel.de" + assert record.status == ["connect"] + + assert record.nameservers == [ + "pns101.cloudns.net", + "pns102.cloudns.net", + "pns103.cloudns.net", + "pns104.cloudns.net" + ] + + refute record.created_at + assert %NaiveDateTime{} = record.updated_at + refute record.expires_at + end + + @tag :live + test "lookup/1 can check .io domains" do + assert {:ok, record} = Whois.lookup("rumdash.io") + assert record.domain == "rumdash.io" + assert record.registrar == "Cloudflare, Inc." + assert record.created_at == ~N[2022-11-20 17:43:37] + assert %NaiveDateTime{} = record.updated_at + assert %NaiveDateTime{} = record.expires_at + end + defp wait, do: Process.sleep(2500) end From ed5a9678b6109ac391391f416b21874f36f32f74 Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Sat, 6 Jan 2024 12:59:00 -0600 Subject: [PATCH 19/20] Add support for .com.br domains (#10) --- lib/whois/record.ex | 16 +++++++++++++- test/fixtures/raw/algoltech.com.br | 34 ++++++++++++++++++++++++++++++ test/whois/record_test.exs | 15 +++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/raw/algoltech.com.br diff --git a/lib/whois/record.ex b/lib/whois/record.ex index bfaf8f1..67c43ac 100644 --- a/lib/whois/record.ex +++ b/lib/whois/record.ex @@ -68,7 +68,7 @@ defmodule Whois.Record do s when s in ["domain status", "status"] -> %{record | status: record.status ++ [value]} - r when r in ["registrar", "registrar handle", "registrar name"] -> + r when r in ["registrar", "registrar handle", "registrar name", "provider"] -> %{record | registrar: value} "sponsoring registrar" -> @@ -161,6 +161,20 @@ defmodule Whois.Record do with {:ok, %Date{} = date} <- Date.from_iso8601(string), {:ok, datetime} <- NaiveDateTime.new(date, Time.new!(0, 0, 0)) do datetime + else + _ -> parse_smooshed_together_date(string) + end + end + + # Handles dates use on .com.br domains like 20240526 + defp parse_smooshed_together_date(string) do + with [<> | _] <- String.split(string), + {year, ""} when year > 1980 and year < 2200 <- Integer.parse(year), + {month, ""} when month >= 1 and month <= 12 <- Integer.parse(month), + {day, ""} when day >= 1 and day <= 31 <- Integer.parse(day), + {:ok, date} <- Date.new(year, month, day), + {:ok, naive} <- NaiveDateTime.new(date, Time.new!(0, 0, 0)) do + naive else _ -> guess_date(string) end diff --git a/test/fixtures/raw/algoltech.com.br b/test/fixtures/raw/algoltech.com.br new file mode 100644 index 0000000..3d8b3e4 --- /dev/null +++ b/test/fixtures/raw/algoltech.com.br @@ -0,0 +1,34 @@ +domain: algoltech.com.br +owner: Jane Doe +ownerid: 281.905.708-03 +country: BR +owner-c: TOGON12 +tech-c: TOGON13 +nserver: ns59.domaincontrol.com +nsstat: 20231231 AA +nslastaa: 20231231 +nserver: ns60.domaincontrol.com +nsstat: 20231231 AA +nslastaa: 20231231 +saci: yes +created: 20210526 #22911015 +changed: 20230708 +expires: 20240526 +status: published +provider: GODADDY (86) + +nic-hdl-br: TOGON12 +person: Jane Doe +e-mail: me@janedoe.me +country: BR +created: 20210526 +changed: 20210526 +provider: GODADDY (86) + +nic-hdl-br: TOGON13 +person: Jane Doe +e-mail: me@janedoe.me +country: BR +created: 20210526 +changed: 20210526 +provider: GODADDY (86) diff --git a/test/whois/record_test.exs b/test/whois/record_test.exs index a268ad1..727bb0d 100644 --- a/test/whois/record_test.exs +++ b/test/whois/record_test.exs @@ -207,6 +207,21 @@ defmodule Whois.RecordTest do refute record.expires_at end + test "parse a .com.br domain" do + record = parse("algoltech.com.br") + refute Whois.Record.is_empty(record) + assert record.domain == "algoltech.com.br" + assert record.status == ["published"] + + assert record.nameservers == [ + "ns59.domaincontrol.com", + "ns60.domaincontrol.com" + ] + + assert record.registrar == "GODADDY (86)" + assert_dt(record.expires_at, ~D[2024-05-26]) + end + defp parse(domain), do: Whois.RecordFixtures.parsed_record_fixture(domain) defp assert_dt(%NaiveDateTime{} = datetime, date) do From 4ead554d7d1a5a1ef17a6e31c1f76a90bddcc20e Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Sun, 7 Jan 2024 07:41:05 -0600 Subject: [PATCH 20/20] Prepare for merging back into the main repo (#11) * Prepare for merging back into the main repo * Fix OTP dependency specification * OTP 22 is no longer supported on ubuntu-22.04, and 26 isn't supported on ubuntu-20.04. Let's favor the latest stuff. * Split test files that need to run on old Ubuntu * Only support back to Elixir 1.12 * Try to fix Elixir 1.12 compatibility by marking recode as optional * Remove dependency on NaiveDateTime function from 1.15 * Okay, don't test 1.12 in CI * No need to report code coverage on old versions * Remove Recode so we can test 1.12 --- .../elixir-build-and-test-old-versions.yml | 43 +++++++++++++++++++ .github/workflows/elixir-build-and-test.yml | 23 +++------- .recode.exs | 33 -------------- README.md | 12 +++++- lib/whois.ex | 4 +- mix.exs | 4 +- mix.lock | 4 -- test/whois/server_test.exs | 10 +++-- 8 files changed, 70 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/elixir-build-and-test-old-versions.yml delete mode 100644 .recode.exs diff --git a/.github/workflows/elixir-build-and-test-old-versions.yml b/.github/workflows/elixir-build-and-test-old-versions.yml new file mode 100644 index 0000000..39af9cd --- /dev/null +++ b/.github/workflows/elixir-build-and-test-old-versions.yml @@ -0,0 +1,43 @@ +name: Build and Test Old Versions + +on: + push: + branches: + - main + - master + pull_request: + branches: + - '*' + +jobs: + build: + name: Build and test + runs-on: ubuntu-20.04 + env: + MIX_ENV: test + strategy: + matrix: + elixir: ["1.12.3", "1.13.4", "1.14.4"] + otp: ["22.3", "23.3.4"] + exclude: + # Elixir 1.13 doesn't support the latest OTP + - elixir: "1.13.4" + otp: "26.0.2" + # Elixir 1.14 requires at least OTP 23 + - elixir: "1.14.4" + otp: "22.3" + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-flags: --all-warnings --warnings-as-errors + + - name: Run Tests + run: mix coveralls.json --warnings-as-errors + if: always() diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index 3b4862f..1282dea 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -17,23 +17,12 @@ jobs: MIX_ENV: test strategy: matrix: - elixir: ["1.15.7"] - otp: ["26.1.2"] - - # Remove if you don't need a database - services: - db: - image: postgis/postgis:13-3.1 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: server_test - ports: ["5432:5432"] - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + elixir: ["1.13.4", "1.14.4", "1.15.7", "1.16.0"] + otp: ["24.3.4", "25.3.2", "26.2.1"] + exclude: + # Elixir 1.13 doesn't support the latest OTP + - elixir: "1.13.4" + otp: "26.2.1" steps: - name: Checkout repository diff --git a/.recode.exs b/.recode.exs deleted file mode 100644 index de493ca..0000000 --- a/.recode.exs +++ /dev/null @@ -1,33 +0,0 @@ -[ - version: "0.6.4", - # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. - autocorrect: true, - # With "--dry" no changes will be written to the files. - # Can also be set/reset with `--dry`/`--no-dry`. - # If dry is true then verbose is also active. - dry: false, - # Can also be set/reset with `--verbose`/`--no-verbose`. - verbose: false, - # Can be overwritten by calling `mix recode "lib/**/*.ex"`. - inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], - formatter: {Recode.Formatter, []}, - tasks: [ - # Tasks could be added by a tuple of the tasks module name and an options - # keyword list. A task can be deactivated by `active: false`. The execution of - # a deactivated task can be forced by calling `mix recode --task ModuleName`. - {Recode.Task.AliasExpansion, []}, - {Recode.Task.AliasOrder, []}, - {Recode.Task.Dbg, [autocorrect: false]}, - {Recode.Task.EnforceLineLength, [active: false]}, - {Recode.Task.FilterCount, []}, - {Recode.Task.IOInspect, [autocorrect: false]}, - {Recode.Task.Nesting, [active: false]}, - {Recode.Task.PipeFunOne, []}, - {Recode.Task.SinglePipe, []}, - {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]}, - {Recode.Task.TagFIXME, [exit_code: 2]}, - {Recode.Task.TagTODO, [exit_code: 4]}, - {Recode.Task.TestFileExt, []}, - {Recode.Task.UnusedVariable, [active: false]} - ] -] diff --git a/README.md b/README.md index 59547b0..2d50a5f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Whois [![Build and Test](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-build-and-test.yml) [![Elixir Quality Checks](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-quality-checks.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-quality-checks.yml) [![Elixir Type Linting](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-dialyzer.yml/badge.svg)](https://github.com/s3cur3/whois.ex/actions/workflows/elixir-dialyzer.yml) [![Code coverage](https://codecov.io/gh/s3cur3/whois.ex/graph/badge.svg?token=Xe9iuK8f63)](https://codecov.io/gh/s3cur3/whois.ex) +# Whois [![Build and Test](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-build-and-test.yml) [![Elixir Quality Checks](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-quality-checks.yml/badge.svg)](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-quality-checks.yml) [![Elixir Type Linting](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-dialyzer.yml/badge.svg)](https://github.com/utkarshkukreti/whois.ex/actions/workflows/elixir-dialyzer.yml) [![Code coverage](https://codecov.io/gh/utkarshkukreti/whois.ex/graph/badge.svg?token=Xe9iuK8f63)](https://codecov.io/gh/utkarshkukreti/whois.ex) Pure Elixir WHOIS client and parser. @@ -73,6 +73,16 @@ iex(1)> Whois.lookup("google.com") ## Development +### Preparing a PR + +There are a handful of code quality checks that CI runs. To run them locally, you can use: + +```sh +mix check +``` + +This does *not* adequately test the TCP connection to real Whois servers, because the GitHub Actions IPs are generally blocked by the servers our "live" (i.e., full end-to-end) tests rely on. Full full test coverage, you'll need to run `mix test --include live` locally. + ### Updating the list of Whois servers The `priv` directory contains a Makefile that will download the latest TLD reference file from the web and parse it into a structure we can use at compile time. Run it like this: diff --git a/lib/whois.ex b/lib/whois.ex index 1ac1fc2..ea46e60 100644 --- a/lib/whois.ex +++ b/lib/whois.ex @@ -29,8 +29,8 @@ defmodule Whois do ### Examples iex> {:ok, %Whois.Record{domain: "google.com"} = record} = Whois.lookup("google.com") - iex> NaiveDateTime.after?(record.expires_at, NaiveDateTime.utc_now()) - true + iex> NaiveDateTime.compare(record.expires_at, NaiveDateTime.utc_now()) + :gt iex> Whois.lookup("scha.ch") {:error, :no_data_provided} diff --git a/mix.exs b/mix.exs index 00b5b3c..53f4916 100644 --- a/mix.exs +++ b/mix.exs @@ -60,7 +60,6 @@ defmodule Whois.Mixfile do {:date_time_parser, "~> 1.2"}, {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, - {:recode, "~> 0.6", only: [:dev, :test]}, {:patch, "~> 0.13.0", only: [:test]} ] end @@ -82,8 +81,7 @@ defmodule Whois.Mixfile do "test --warnings-as-errors", "format --check-formatted", "deps.unlock --check-unused", - "check.dialyzer", - "recode" + "check.dialyzer" ], "check.dialyzer": "cmd MIX_ENV=dev mix dialyzer" ] diff --git a/mix.lock b/mix.lock index c19dc0e..466ea59 100644 --- a/mix.lock +++ b/mix.lock @@ -8,7 +8,6 @@ "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "glob_ex": {:hex, :glob_ex, "0.1.6", "3a311ade50f6b71d638af660edcc844c3ab4eb2a2c816cfebb73a1d521bb2f9d", [:mix], [], "hexpm", "fda1e90e10f6029bd72967fef0c9891d0d14da89ca7163076e6028bfcb2c42fa"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "kday": {:hex, :kday, "1.0.2", "e96035c439323eeb8505268959122e9d30194a4e5a23a357dc75f1cae696e918", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "f040b9b6de21eea4a96dbf71753f4eeb0772a6ebd6c18cacd114694af7cabc9a"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, @@ -16,7 +15,4 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "patch": {:hex, :patch, "0.13.0", "da48728f9086a835956200a671210fe88f67ff48bb1f92626989886493ac2081", [:mix], [], "hexpm", "d65a840d485dfa05bf6673269b56680e7537a05050684e713de125a351b28112"}, - "recode": {:hex, :recode, "0.6.5", "067335c383e807c1a6f5df22a92856f83e852f1acdbedaa2cced52df082dbe25", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}], "hexpm", "d03c66c1dd1ca5b19ad14a3a5fbb2218060352e91d4ad7925fb52f1915a4aabe"}, - "rewrite": {:hex, :rewrite, "0.10.0", "5d756b6dc67679e7156ff6055f9654be02dbaeb177aaf1ff6af7ee8da8718248", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.13", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "68d7808cf549e7bf51b0119a8edc14d50970bad479115249030baa580c1d7b50"}, - "sourceror": {:hex, :sourceror, "0.14.1", "c6fb848d55bd34362880da671debc56e77fd722fa13b4dcbeac89a8998fc8b09", [:mix], [], "hexpm", "8b488a219e4c4d7d9ff29d16346fd4a5858085ccdd010e509101e226bbfd8efc"}, } diff --git a/test/whois/server_test.exs b/test/whois/server_test.exs index e162c36..d11eb4e 100644 --- a/test/whois/server_test.exs +++ b/test/whois/server_test.exs @@ -12,9 +12,13 @@ defmodule Whois.ServerTest do end test "handles subdomains" do - {:ok, no_subdomain} = Whois.Server.for("example.com") - assert {:ok, ^no_subdomain} = Whois.Server.for("foo.example.com") - assert {:ok, ^no_subdomain} = Whois.Server.for("foo.bar.baz.example.com") + {:ok, dot_com} = Whois.Server.for("example.com") + assert {:ok, ^dot_com} = Whois.Server.for("foo.example.com") + assert {:ok, ^dot_com} = Whois.Server.for("foo.bar.baz.example.com") + + {:ok, dot_co_dot_ca} = Whois.Server.for("example.co.ca") + assert {:ok, ^dot_co_dot_ca} = Whois.Server.for("foo.example.co.ca") + assert {:ok, ^dot_co_dot_ca} = Whois.Server.for("foo.bar.baz.example.co.ca") end test "handles unsupported TLDs" do