@@ -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
132169end
0 commit comments