Skip to content

Commit f07aa0d

Browse files
committed
Cleanup validation logic
1 parent 6eafd6e commit f07aa0d

File tree

2 files changed

+87
-41
lines changed

2 files changed

+87
-41
lines changed

lib/safeurl.ex

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ defmodule SafeURL do
4646
"240.0.0.0/4"
4747
]
4848

49+
50+
51+
# Public API
52+
# ----------
53+
54+
4955
@doc """
5056
Validate a URL and execute a GET request using `HTTPoison` with the specified headers and options.
5157
@@ -71,62 +77,93 @@ defmodule SafeURL do
7177
end
7278
end
7379

80+
7481
@doc """
7582
Validate a string URL against a blacklist or whitelist.
7683
7784
See documentation for `SafeURL.get()` for available options and defaults.
7885
7986
Returns `true` if the URL meets the requirements, `false` otherwise.
8087
"""
81-
def allowed?(url, options \\ []) do
82-
blacklist_private = Keyword.get(options, :blacklist_private, true)
83-
blacklist = Keyword.get(options, :blacklist, [])
84-
whitelist = Keyword.get(options, :whitelist, [])
85-
schemes = Keyword.get(options, :schemes, ["http", "https"])
86-
host_info = URI.parse(url)
87-
88-
# TODO: Refactor this to use idiomatic elixir control flow
89-
if validate_scheme(host_info.scheme, schemes) == false do
90-
false
91-
else
92-
addr = resolve_address(host_info.host)
93-
if length(whitelist) != 0 do
94-
validate_whitelist(addr, whitelist)
88+
@spec allowed?(binary(), Keyword.t()) :: boolean()
89+
def allowed?(url, opts \\ []) do
90+
uri = URI.parse(url)
91+
opts = build_options(opts)
92+
ip = resolve_address(uri.host)
93+
94+
cond do
95+
uri.scheme not in opts.schemes ->
96+
false
97+
98+
opts.whitelist != [] ->
99+
ip_in_ranges?(address, opts.whitelist)
100+
101+
true ->
102+
!ip_in_ranges?(address, opts.blacklist)
103+
end
104+
end
105+
106+
107+
108+
109+
# Private Helpers
110+
# ---------------
111+
112+
113+
# Return a map of calculated options
114+
defp build_options(opts) do
115+
schemes = get_option(opts, :schemes)
116+
whitelist = get_option(opts, :whitelist)
117+
blacklist = get_option(opts, :blacklist)
118+
119+
blacklist =
120+
if get_option(opts, :blacklist_reserved) do
121+
blacklist ++ @reserved_ranges
95122
else
96-
if blacklist_private == false do
97-
validate_blacklist(addr, blacklist)
98-
else
99-
validate_blacklist(addr, @reserved_ranges ++ blacklist)
100-
end
123+
blacklist
101124
end
102-
end
125+
126+
%{schemes: schemes, whitelist: whitelist, blacklist: blacklist}
103127
end
104128

105-
defp resolve_address(hostname) do
106-
# Don't resolve hostname in DNS if it's an IP address
107-
{result, value} = hostname |> to_charlist() |> :inet.parse_address()
108-
if result != :ok do
109-
{_, ip} = DNS.resolve(hostname)
110-
# TODO: safely handle multiple IPs/round-robin DNS
111-
List.first(ip)
129+
130+
# Get the value of a specific option, either from the application
131+
# configs or overrides explicitly passed as arguments.
132+
defp get_option(opts, key) do
133+
if Keyword.has_key?(opts, key) do
134+
Keyword.get(opts, key)
112135
else
113-
value
136+
Application.get_env(:safeurl, key)
114137
end
115138
end
116139

117-
defp validate_scheme(scheme, allowed_schemes) do
118-
Enum.member?(allowed_schemes, scheme)
119-
end
120140

121-
defp validate_whitelist(address, whitelist) do
122-
Enum.any?(whitelist, fn range ->
123-
InetCidr.contains?(InetCidr.parse(range), address)
124-
end)
141+
# Resolve hostname in DNS to an IP address (if not already an IP)
142+
defp resolve_address(hostname) do
143+
hostname
144+
|> to_charlist()
145+
|> :inet.parse_address()
146+
|> case do
147+
{:ok, ip} ->
148+
ip
149+
150+
{:error, :einval} ->
151+
# TODO: safely handle multiple IPs/round-robin DNS
152+
case DNS.resolve(hostname) do
153+
{:ok, ips} -> List.first(ips)
154+
{:error, _reason} -> nil
155+
end
156+
end
125157
end
126158

127-
defp validate_blacklist(address, blacklist) do
128-
!Enum.any?(blacklist, fn range ->
129-
InetCidr.contains?(InetCidr.parse(range), address)
159+
160+
defp ip_in_ranges?({_, _, _, _} = addr, ranges) when is_list(ranges) do
161+
Enum.any?(ranges, fn range ->
162+
range
163+
|> InetCidr.parse()
164+
|> InetCidr.contains?(addr)
130165
end)
131166
end
167+
168+
defp ip_in_ranges?(_addr, _ranges), do: false
132169
end

mix.exs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@ defmodule SafeURL.MixProject do
77
version: "0.1.0",
88
elixir: "~> 1.10",
99
start_permanent: Mix.env() == :prod,
10-
deps: deps()
10+
deps: deps(),
1111
]
1212
end
1313

1414
def application do
15-
[]
15+
[env: default_configs()]
16+
end
17+
18+
defp default_configs do
19+
[
20+
schemes: ~w[http https],
21+
blacklist_reserved: true,
22+
blacklist: [],
23+
whitelist: [],
24+
]
1625
end
1726

1827
defp deps do
1928
[
2029
{:httpoison, "~> 1.8"},
2130
{:inet_cidr, "~> 1.0"},
22-
{:dns, "~> 2.2"}
31+
{:dns, "~> 2.2"},
2332
]
2433
end
2534
end

0 commit comments

Comments
 (0)