@@ -34,14 +34,14 @@ defmodule Plug.SSL do
3434 defaults to `false`
3535 * `:subdomains` - a boolean on including subdomains or not in HSTS,
3636 defaults to `false`
37- * `:exclude` - exclude the given hosts from redirecting to the `https`
38- scheme. Defaults to `["localhost", "127.0.0.1"]`. It may be set to a list of binaries
39- or a tuple [`{module, function, args}` ](#module-excluded-hosts-tuple).
37+ * `:exclude` - exclude certain request from redirecting to the `https` scheme.
38+ It defaults to `[hosts: [ "localhost", "127.0.0.1"]] `. See the
39+ ["Exclude option" ](#module-exclude-option) section below
4040 * `:host` - a new host to redirect to if the request's scheme is `http`,
4141 defaults to `conn.host`. It may be set to a binary or a tuple
4242 `{module, function, args}` that will be invoked on demand
4343 * `:log` - The log level at which this plug should log its request info.
44- Default is `:info`. Can be `false` to disable logging.
44+ Default is `:info`. Can be `false` to disable logging
4545
4646 ## Port
4747
@@ -50,23 +50,37 @@ defmodule Plug.SSL do
5050 want to redirect to HTTPS on another port, you can sneak it alongside
5151 the host, for example: `host: "example.com:443"`.
5252
53- ## Excluded hosts tuple
53+ ## Exclude option
5454
55- Tuple `{module, function, args}` can be passed to be invoked each time
56- the plug is checking whether to redirect host. Provided function needs
57- to receive at least one argument (`host`) .
55+ There are many situations where one may want to avoid `Plug.SSL` from
56+ redirecting, such as requests coming from `localhost` or `127.0.0.1`,
57+ or from health check endpoints .
5858
59- For example, you may define it as:
59+ This can be done via the `:exclude` option, which allows you to specify
60+ conditions to skip the redirect. As long as any of the conditions match,
61+ the route will be excluded, it must be one of:
6062
61- plug Plug.SSL,
62- rewrite_on: [:x_forwarded_proto],
63- exclude: {__MODULE__, :excluded_host?, []}
63+ * `[hosts: list_of_hosts, ...]` - skips redirection if the request
64+ matches any of the given hosts
6465
65- where:
66+ * `[paths: list_of_paths, ...]` - skips redirection if the request
67+ matches any of the given paths
6668
67- def excluded_host?(host) do
68- # Custom logic
69- end
69+ * `[conn: {mod, fun, args}, ...]` - calls the given `mod`, `fun`,
70+ and `args` with `Plug.Conn` prepended to the list of arguments.
71+ The plug will be excluded if the call returns `true`
72+
73+ The default value is `[hosts: ["localhost", "127.0.0.1"]]`. If you pass
74+ any additional value, you must explicitly preserve the above if you want
75+ the hosts to remain excluded.
76+
77+ For example, you may define it as:
78+
79+ plug Plug.SSL,
80+ exclude: [
81+ hosts: ["localhost", "127.0.0.1"],
82+ paths: ["/health"]
83+ ]
7084
7185 """
7286 @ behaviour Plug
@@ -139,12 +153,12 @@ defmodule Plug.SSL do
139153 Layer Security Cheat Sheet. General purpose web applications should default to
140154 TLSv1.3 with ALL other protocols disabled.
141155
142- The **Compatible** cipher suite supports TLSv1.2 and TLSv1.3. This
143- suite provides strong security while maintaining compatibility with a wide
144- range of modern clients.
156+ The **Compatible** cipher suite supports TLSv1.2 and TLSv1.3. This
157+ suite provides strong security while maintaining compatibility with a wide
158+ range of modern clients.
145159
146- Legacy protocols TLSv1.1 and TLSv1.0 are officially deprecated by
147- [RFC 8996](https://www.rfc-editor.org/rfc/rfc8996.html) and are
160+ Legacy protocols TLSv1.1 and TLSv1.0 are officially deprecated by
161+ [RFC 8996](https://www.rfc-editor.org/rfc/rfc8996.html) and are
148162 considered insecure.
149163
150164 [Test your ssl configuration](https://ssl-config.mozilla.org/)
@@ -343,25 +357,72 @@ defmodule Plug.SSL do
343357 :ok
344358 end
345359
346- rewrite_on = Plug.RewriteOn . init ( Keyword . get ( opts , :rewrite_on ) )
360+ exclude =
361+ if exclude = Keyword . get ( opts , :exclude ) do
362+ validate_exclude! ( exclude )
363+ else
364+ [ hosts: [ "localhost" , "127.0.0.1" ] ]
365+ end
366+
347367 log = Keyword . get ( opts , :log , :info )
348- exclude = Keyword . get ( opts , :exclude , [ "localhost" , "127.0.0.1" ] )
368+ rewrite_on = Plug.RewriteOn . init ( Keyword . get ( opts , :rewrite_on ) )
349369 { hsts_header ( opts ) , exclude , host , rewrite_on , log }
350370 end
351371
372+ defp validate_exclude! ( exclude ) when is_list ( exclude ) do
373+ Enum . map ( exclude , fn
374+ # TODO: Deprecate me on Plug v1.20
375+ binary when is_binary ( binary ) ->
376+ { :hosts , [ binary ] }
377+
378+ { :hosts , hosts } when is_list ( hosts ) ->
379+ { :hosts , hosts }
380+
381+ { :paths , paths } when is_list ( paths ) ->
382+ { :paths , Enum . map ( paths , & Plug.Router.Utils . split / 1 ) }
383+
384+ { :conn , { mod , fun , args } } when is_atom ( mod ) and is_atom ( fun ) and is_list ( args ) ->
385+ { :conn , { mod , fun , args } }
386+
387+ other ->
388+ raise ArgumentError ,
389+ "invalid entry in :exclude, expected host or path, got: #{ inspect ( other ) } "
390+ end )
391+ end
392+
393+ defp validate_exclude! ( { m , f , a } ) do
394+ IO . warn (
395+ "exclude: {mod, fun, args} is deprecated, " <>
396+ "please use exclude: [conn: {mod, fun, args}], which will receive the whole connection instead"
397+ )
398+
399+ { m , f , a }
400+ end
401+
402+ defp validate_exclude! ( exclude ) do
403+ raise ArgumentError , ":exclude must be a list, got: #{ inspect ( exclude ) } "
404+ end
405+
352406 @ impl true
353407 def call ( conn , { hsts , exclude , host , rewrite_on , log_level } ) do
354408 conn = Plug.RewriteOn . call ( conn , rewrite_on )
355409
356410 cond do
357- excluded? ( conn . host , exclude ) -> conn
411+ excluded? ( conn , exclude ) -> conn
358412 conn . scheme == :https -> put_hsts_header ( conn , hsts )
359413 true -> redirect_to_https ( conn , host , log_level )
360414 end
361415 end
362416
363- defp excluded? ( host , list ) when is_list ( list ) , do: :lists . member ( host , list )
364- defp excluded? ( host , { mod , fun , args } ) , do: apply ( mod , fun , [ host | args ] )
417+ defp excluded? ( conn , list ) when is_list ( list ) do
418+ Enum . any? ( list , fn
419+ { :hosts , hosts } -> conn . host in hosts
420+ { :paths , paths } -> conn . path_info in paths
421+ { :conn , { mod , fun , args } } -> apply ( mod , fun , [ conn | args ] )
422+ end )
423+ end
424+
425+ defp excluded? ( conn , { mod , fun , args } ) , do: apply ( mod , fun , [ conn . host | args ] )
365426
366427 # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
367428 defp hsts_header ( opts ) do
0 commit comments