Skip to content

Commit

Permalink
Gracefully handle connection timeouts for registrars that point to no…
Browse files Browse the repository at this point in the history
…n-functional WHOIS servers

This fixes an issue see with the `pairdomains.com` registrar, whose WHOIS record terminate with a record that includes this:

```
    Registrar WHOIS Server: pairdomains.com
```

The actual WHOIS server for the registrar is `whois.pairdomains.com`, so prior to this patch, `Whois.lookup/1` would fail after 60 seconds with `{:error, :timed_out}`. Now, we'll fall back to the most recent successfully retrieved record after `:connect_timeout` (10 seconds by default).
  • Loading branch information
s3cur3 committed Feb 9, 2024
1 parent 4e78f29 commit 94ca739
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 11 deletions.
56 changes: 45 additions & 11 deletions lib/whois.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ defmodule Whois do
alias Whois.Record
alias Whois.Server

@type lookup_option :: {:server, String.t() | Server.t()}
@type lookup_option ::
{:server, String.t() | Server.t()}
| {:connect_timeout, timeout()}
| {:recv_timeout, timeout()}

@default_connect_timeout :timer.seconds(10)
@default_recv_timeout :timer.seconds(10)

@doc """
Queries the appropriate WHOIS server for the domain.
Expand All @@ -31,6 +37,10 @@ defmodule Whois do
- server: the WHOIS server to query. If not specified, we'll automatically
choose the appropriate server.
- connect_timeout: milliseconds to wait for the WHOIS server to accept our connection.
Defaults to 10,000 ms (10 seconds).
- recv_timeout: milliseconds to wait for the WHOIS server to reply after connecting.
Defaults to 10,000 ms (10 seconds).
### Examples
Expand Down Expand Up @@ -61,11 +71,13 @@ defmodule Whois do
:error -> Server.for(domain)
end

timeout = Access.get(opts, :connect_timeout, @default_connect_timeout)

with {:ok, %Server{host: host} = server} <- server,
{:ok, socket} <-
:gen_tcp.connect(String.to_charlist(host), 43, [:binary, active: false]),
:gen_tcp.connect(String.to_charlist(host), 43, [:binary, active: false], timeout),
:ok <- :gen_tcp.send(socket, [query(server, domain), "\r\n"]),
raw when is_binary(raw) <- recv(socket) do
raw when is_binary(raw) <- recv(socket, "", opts) do
case next_server(raw) do
nil ->
{:ok, raw}
Expand All @@ -77,26 +89,48 @@ defmodule Whois do
{:ok, raw}

next ->
with {:ok, raw2} <- lookup_raw(domain, [{:server, next} | opts]) do
{:ok, raw <> raw2}
case lookup_raw(domain, [{:server, next} | opts]) do
{:ok, raw2} ->
{:ok, raw <> raw2}

{:error, :timed_out} ->
# Sometimes we get malformed WHOIS records where the record actually
# includes all the information that exists, but incorrectly also
# points to a Registrar WHOIS Server that just doesn't respond.
# In these cases, if the record we received actually does belong to
# the domain, we'll just return what we've got.
if String.contains?(raw, domain) do
{:ok, raw}
else
{:error, :timed_out}
end

error ->
error
end
end
else
{:error, :timeout} -> {:error, :timed_out}
error -> error
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)
@spec recv(socket :: :gen_tcp.socket(), acc :: String.t(), [lookup_option()]) ::
String.t() | {:error, :timed_out}
defp recv(socket, acc, opts) do
timeout = Access.get(opts, :recv_timeout, @default_recv_timeout)

case :gen_tcp.recv(socket, 0, timeout) do
{:ok, data} -> recv(socket, acc <> data, opts)
{:error, :etimedout} -> {:error, :timed_out}
{:error, :closed} -> acc
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
# > 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 <domain.de>".
#
# https://www.denic.de/en/service/whois-service/
Expand Down
10 changes: 10 additions & 0 deletions test/whois_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ defmodule WhoisTest do
assert %NaiveDateTime{} = record.expires_at
end

@tag :live
test "lookup/1 can deal with domains that incorrectly point to fake WHOIS servers" do
assert {:ok, record} = Whois.lookup("storehub.io", recv_timeout: 5_000)
assert record.domain == "storehub.io"
assert record.registrar == "Pair Domains"
assert record.created_at == ~N[2019-09-12 10:17:27]
assert %NaiveDateTime{} = record.updated_at
assert %NaiveDateTime{} = record.expires_at
end

defp wait, do: Process.sleep(2500)
end

Expand Down

0 comments on commit 94ca739

Please sign in to comment.