Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 12 additions & 32 deletions extra/lib/plausible/installation_support/checks/url.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
defmodule Plausible.InstallationSupport.Checks.Url do
@moduledoc """
Checks if site domain has an A record.
If not, checks if prepending `www.` helps,
because we have specifically requested customers to register the domain with `www.` prefix.
Checks if site domain resolves (either A or AAAA record) to a public address.
If not, checks if prepending `www.` helps, because we have specifically
requested customers to register the domain with `www.` prefix.

If not, skips all further checks.
"""

Expand All @@ -15,11 +16,11 @@ defmodule Plausible.InstallationSupport.Checks.Url do
@spec perform(State.t(), Keyword.t()) :: State.t()
def perform(%State{url: url} = state, _opts) when is_binary(url) do
with {:ok, %URI{scheme: scheme} = uri} when scheme in ["http", "https"] <- URI.new(url),
:ok <- check_domain(uri.host) do
:ok <- dns_lookup(uri.host) do
stripped_url = URI.to_string(%URI{uri | query: nil, fragment: nil})
%State{state | url: stripped_url}
else
{:error, :no_a_record} ->
{:error, :resolve_host_error} ->
put_diagnostics(%State{state | skip_further_checks?: true},
service_error: %{code: :domain_not_found}
)
Expand Down Expand Up @@ -54,40 +55,19 @@ defmodule Plausible.InstallationSupport.Checks.Url do
"www.#{domain_without_path}"
]
|> Enum.reduce_while({:error, :domain_not_found}, fn d, _acc ->
# For local testing, run the server with ALLOW_RESERVED_IPS=true
case dns_lookup(d) do
:ok -> {:halt, {:ok, "https://" <> unsplit_domain(d, rest)}}
{:error, :no_a_record} -> {:cont, {:error, :domain_not_found}}
{:error, :resolve_host_error} -> {:cont, {:error, :domain_not_found}}
end
end)
end

@spec dns_lookup(String.t()) :: :ok | {:error, :no_a_record}
@spec dns_lookup(String.t()) :: :ok | {:error, :resolve_host_error}
defp dns_lookup(domain) do
lookup_timeout = 1_000
resolve_timeout = 1_000

case Plausible.DnsLookup.impl().lookup(
to_charlist(domain),
:in,
:a,
[timeout: resolve_timeout],
lookup_timeout
) do
[{a, b, c, d} | _]
when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) ->
:ok

# this may mean timeout or no DNS record
[] ->
{:error, :no_a_record}
end
end

defp check_domain(domain) do
if Application.get_env(:plausible, :environment) == "dev" and domain == "localhost" do
:ok
else
dns_lookup(domain)
case Plausible.SSRF.resolve_host(domain) do
{:ok, _ips} -> :ok
{:error, _reason} -> {:error, :resolve_host_error}
end
end

Expand Down
101 changes: 71 additions & 30 deletions test/plausible/installation_support/checks/url_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
use Plausible.DataCase, async: true

on_ee do
import Mox
use Plausible.Test.Support.DNS

alias Plausible.InstallationSupport.{State, Checks, Verification}
alias Plausible.InstallationSupport.{Checks, State, Verification}

@check Checks.Url

Expand All @@ -20,14 +20,7 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
{"plausible.io/sites", ~c"plausible.io"}
] do
test "guesses 'https://#{site_domain}' if A-record is found for '#{site_domain}'" do
Plausible.DnsLookup.Mock
|> expect(:lookup, fn unquote(expected_lookup_domain),
_type,
_record,
_opts,
_timeout ->
[{192, 168, 1, 1}]
end)
expect_dns_lookup(unquote(expected_lookup_domain), [{93, 184, 216, 34}])

state =
@check.perform(
Expand All @@ -48,13 +41,8 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
test "guesses 'www.{domain}' if A record is not found for 'domain'" do
site_domain = "example.com/any/deeper/path"

Plausible.DnsLookup.Mock
|> expect(:lookup, fn ~c"example.com", _type, _record, _opts, _timeout ->
[]
end)
|> expect(:lookup, fn ~c"www.example.com", _type, _record, _opts, _timeout ->
[{192, 168, 1, 2}]
end)
expect_dns_lookup("example.com", [])
expect_dns_lookup("www.example.com", [{93, 184, 216, 34}])

state =
@check.perform(
Expand All @@ -72,15 +60,53 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
end

test "fails if no A-record is found for 'domain' or 'www.{domain}'" do
expected_lookups = 2
domain = "any.example.com"

expect_dns_lookup("any.example.com", [])
expect_dns_lookup("www.any.example.com", [])

state =
@check.perform(
%State{
data_domain: domain,
url: nil,
diagnostics: %Verification.Diagnostics{}
},
[]
)

assert state.url == nil
assert state.diagnostics.service_error == %{code: :domain_not_found}
assert state.skip_further_checks?
end

test "fails if 'domain' only resolves to a private/reserved address" do
domain = "any.example.com"

expect_dns_lookup("any.example.com", [{192, 168, 1, 1}])
expect_dns_lookup("www.any.example.com", [])

state =
@check.perform(
%State{
data_domain: domain,
url: nil,
diagnostics: %Verification.Diagnostics{}
},
[]
)

Plausible.DnsLookup.Mock
|> expect(:lookup, expected_lookups, fn _domain, _type, _record, _opts, _timeout ->
[]
end)
assert state.url == nil
assert state.diagnostics.service_error == %{code: :domain_not_found}
assert state.skip_further_checks?
end

test "fails if 'domain' only resolves to a private/reserved AAAA address" do
domain = "any.example.com"

expect_dns_lookup("any.example.com", [], [{0xFC00, 0, 0, 0, 0, 0, 0, 1}])
expect_dns_lookup("www.any.example.com", [])

state =
@check.perform(
%State{
Expand All @@ -102,10 +128,7 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
site_domain = "example-com-rollup"
url = "https://blog.example.com/recipes?foo=bar#baz"

Plausible.DnsLookup.Mock
|> expect(:lookup, fn ~c"blog.example.com", _type, _record, _opts, _timeout ->
[{192, 168, 1, 1}]
end)
expect_dns_lookup("blog.example.com", [{93, 184, 216, 34}])

state =
@check.perform(
Expand Down Expand Up @@ -142,10 +165,28 @@ defmodule Plausible.InstallationSupport.Checks.UrlTest do
site_domain = "example-com-rollup"
url = "https://example.com/archives/news?p=any#fragment"

Plausible.DnsLookup.Mock
|> expect(:lookup, fn ~c"example.com", _type, _record, _opts, _timeout ->
[]
end)
expect_dns_lookup("example.com", [])

state =
@check.perform(
%State{
data_domain: site_domain,
url: url,
diagnostics: %Verification.Diagnostics{}
},
[]
)

assert state.url == url
assert state.diagnostics.service_error == %{code: :domain_not_found}
assert state.skip_further_checks?
end

test "rejects urls whose host only resolves to a private/reserved address" do
site_domain = "example-com-rollup"
url = "https://example.com/archives/news?p=any#fragment"

expect_dns_lookup("example.com", [{127, 0, 0, 1}])

state =
@check.perform(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "successful detection -> no logs, no sentry, telemetry :success" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub =
json_response_detection_stub(%{
Expand All @@ -59,7 +59,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "domain not found -> customer website issue" do
stub_lookup_a_records(@expected_domain, [])
expect_dns_unresolvable(@expected_domain)

detection_counter =
Req.Test.stub(Plausible.InstallationSupport.Checks.Detection, fn _conn ->
Expand Down Expand Up @@ -88,7 +88,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do

for msg <- ["Execution context destroyed", "net::ERR_CONNECTION_REFUSED"] do
test "failure due to a known :browserless_client_error (#{msg}) -> customer website issue" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub =
json_response_detection_stub(%{
Expand All @@ -112,7 +112,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "failure due to an unknown :browserless_client_error -> unknown failure" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub =
json_response_detection_stub(%{
Expand All @@ -136,7 +136,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "failure hitting the catch-all interpret clause -> unknown failure" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub = fn conn ->
Req.Test.transport_error(conn, :econnrefused)
Expand All @@ -158,7 +158,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "failure due to a flaky browserless issue -> browserless issue" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub = fn conn ->
conn
Expand All @@ -182,7 +182,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "failure due to a browserless timeout -> browserless issue" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub = fn conn ->
Req.Test.transport_error(conn, :timeout)
Expand All @@ -203,7 +203,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksObservabilityTest do
end

test "failure due to internal_check_timeout -> browserless issue" do
stub_lookup_a_records(@expected_domain)
stub_dns()

detection_stub = fn _conn ->
# times out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule Plausible.InstallationSupport.Detection.ChecksTest do
test "handles wordpress detection, retrying on 429" do
url_to_verify = nil
test = self()
stub_lookup_a_records(@expected_domain)
stub_dns()

get_context_from_body = fn conn ->
{:ok, body, _conn} = Plug.Conn.read_body(conn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksObservabilityTest do
Process.sleep(1000)
end

stub_lookup_a_records(@expected_domain)
stub_dns()
stub_verification_result(verification_stub)

state =
Expand Down Expand Up @@ -161,7 +161,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksObservabilityTest do
end

defp run_checks(verification_stub) do
stub_lookup_a_records(@expected_domain)
stub_dns()
stub_verification_result(verification_stub)

Checks.run(@url_to_verify, @expected_domain, "manual",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do

describe "URL check" do
test "returns error when DNS check fails with domain not found error, offers custom URL input" do
stub_lookup_a_records(@expected_domain, [])
expect_dns_unresolvable(@expected_domain)

assert_matches %Result{
ok?: false,
Expand All @@ -43,7 +43,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do

test "returns error when DNS check fails with invalid URL error, offers custom URL input" do
url_to_verify = "file://#{@expected_domain}"
stub_lookup_a_records(@expected_domain, [])
expect_dns_unresolvable(@expected_domain)

assert_matches %Result{
ok?: false,
Expand Down Expand Up @@ -539,7 +539,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
installation_type = Keyword.get(opts, :installation_type, "manual")
expected_req_count = Keyword.get(opts, :expected_req_count)

stub_lookup_a_records(@expected_domain)
stub_dns()

if is_integer(expected_req_count) do
counter = :atomics.new(1, [])
Expand Down
8 changes: 2 additions & 6 deletions test/plausible_web/live/change_domain_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule PlausibleWeb.Live.ChangeDomainTest do
import Phoenix.LiveViewTest

on_ee do
import Mox
use Plausible.Test.Support.DNS
end

alias Plausible.Repo
Expand All @@ -14,11 +14,7 @@ defmodule PlausibleWeb.Live.ChangeDomainTest do

on_ee do
setup do
# mock all domains resolve
Plausible.DnsLookup.Mock
|> stub(:lookup, fn _domain, _type, _record, _opts, _timeout ->
[{192, 168, 1, 2}]
end)
stub_dns()

# Stub detection by default to prevent async task race conditions
# Tests that need specific detection results can override this stub
Expand Down
Loading
Loading