1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
defmodule Lithium.Util.PublicSuffix do
use GenServer
require Logger
@moduledoc """
A GenServer that loads the public suffix list from a file and provides
functionality to find the organisational domain of a given domain name.
"""
@public_suffix_list_path Application.app_dir(:lithium, "priv/public_suffix_list.dat")
@public_suffix_list_url "https://publicsuffix.org/list/public_suffix_list.dat"
@public_suffix_max_age :timer.hours(7 * 24)
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
if File.exists?(@public_suffix_list_path) do
{:ok, load_public_suffix_list()}
else
{:ok, []}
end
end
@impl true
def handle_info(:update_public_suffix_list, state) do
case fetch_public_suffix_list() do
{:ok, new_list} ->
{:noreply, new_list}
{:error, reason} ->
Logger.error("Failed to update public suffix list: #{reason}")
{:noreply, state}
end
end
@impl true
def handle_call({:get_domain, domain}, _from, state) do
{:reply, find_organizational_domain(domain, state), state}
end
def load_public_suffix_list do
case File.read(@public_suffix_list_path) do
{:ok, content} ->
content
|> String.split("\n", trim: true)
|> Enum.reject(&String.starts_with?(&1, "//"))
|> Enum.map(&String.trim/1)
|> Enum.map(&String.downcase/1)
|> Enum.map(&String.replace(&1, "*.", ""))
{:error, reason} ->
Logger.error("Failed to load public suffix list: #{reason}")
[]
end
end
def fetch_public_suffix_list do
request = :httpc.request(:get, {@public_suffix_list_url, []}, [], [])
case request do
{:ok, {{_, 200, _}, _, body}} ->
File.write(@public_suffix_list_path, body)
{:ok, load_public_suffix_list()}
{:error, reason} ->
{:error, reason}
end
end
def find_organizational_domain(domain, public_suffix_list) do
# x.y.z.jb3.dev -> jb3.dev
# ["com", "jb3", "example"]
domain_parts = String.split(domain, ".") |> Enum.reverse()
# [["com", "public-suffix"], ...]
suffix_parts =
Enum.map(public_suffix_list, &String.split(&1, ".")) |> Enum.map(&Enum.reverse/1)
# Add domain parts together until it is no longer in the suffix_parts list
Enum.reduce_while(domain_parts, [], fn part, acc ->
new_acc = acc ++ [part]
if Enum.member?(suffix_parts, new_acc) do
{:cont, new_acc}
else
{:halt, new_acc}
end
end)
|> Enum.reverse()
|> Enum.join(".")
|> String.downcase()
|> String.trim()
end
def get_domain(domain) do
GenServer.call(__MODULE__, {:get_domain, domain})
end
end
|