Skip to content
Open
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
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
erlang 28.5
elixir 1.19.4
11 changes: 6 additions & 5 deletions lib/httpoison.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ defmodule HTTPoison.Request do
* `:socks5_user`- socks5 username
* `:socks5_pass`- socks5 password
* `:ssl` - SSL options supported by the `ssl` erlang module. SSL defaults will be used where options
are not specified. Note: under hackney 4.0 the bare `verify: :verify_none` idiom no longer disables
certificate verification on its own, because hackney injects its own hostname-checking `verify_fun`
that OTP still invokes. HTTPoison detects `verify: :verify_none` given without a custom `:verify_fun`
and injects a permissive one so verification is actually skipped. Supply your own `:verify_fun` for
finer-grained control.
are not specified. When available, trusted CA certificates default to the OS trust store loaded by
`:public_key.cacerts_get/0`. Note: under hackney 4.0 the bare `verify: :verify_none` idiom no longer
disables certificate verification on its own, because hackney injects its own hostname-checking
`verify_fun` that OTP still invokes. HTTPoison detects `verify: :verify_none` given without a custom
`:verify_fun` and injects a permissive one so verification is actually skipped. Supply your own
`:verify_fun` for finer-grained control.
* `:ssl_override` - if `:ssl` is specified, this option is ignored, otherwise it can be used to
completely override SSL settings.
* `:follow_redirect` - a boolean that causes redirects to be followed (resolved internally by
Expand Down
63 changes: 54 additions & 9 deletions lib/httpoison/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -766,15 +766,7 @@ defmodule HTTPoison.Base do
stream_to = Keyword.get(options, :stream_to)
async = Keyword.get(options, :async)

ssl =
if ssl_opts = Keyword.get(options, :ssl) do
# Extract the host from the URL just like hackney does
host = hackney_url_record(:hackney_url.parse_url(url), :host)

:hackney_ssl.ssl_opts(host, [{:ssl_options, allow_insecure_verify(ssl_opts)}])
else
Keyword.get(options, :ssl_override)
end
ssl = build_ssl_options(url, options)

follow_redirect = Keyword.get(options, :follow_redirect)
max_redirect = Keyword.get(options, :max_redirect)
Expand Down Expand Up @@ -826,6 +818,59 @@ defmodule HTTPoison.Base do

defp allow_insecure_verify(ssl_opts), do: ssl_opts

defp build_ssl_options(url, options) do
cond do
Keyword.has_key?(options, :ssl) ->
# Extract the host from the URL just like hackney does.
host = hackney_url_record(:hackney_url.parse_url(url), :host)

ssl_opts =
options
|> Keyword.get(:ssl, [])
|> put_default_cacerts()
|> allow_insecure_verify()

:hackney_ssl.ssl_opts(host, [{:ssl_options, ssl_opts}])

Keyword.has_key?(options, :ssl_override) ->
Keyword.get(options, :ssl_override)

hackney_insecure?(options) ->
nil

https_url?(url) ->
host = hackney_url_record(:hackney_url.parse_url(url), :host)
:hackney_ssl.ssl_opts(host, [{:ssl_options, default_cacerts()}])

true ->
nil
end
end

defp put_default_cacerts(ssl_opts) do
case Keyword.has_key?(ssl_opts, :cacerts) || Keyword.has_key?(ssl_opts, :cacertfile) do
true -> ssl_opts
_ -> Keyword.merge(default_cacerts(), ssl_opts)
end
end

if Code.ensure_loaded?(:public_key) and function_exported?(:public_key, :cacerts_get, 0) do
defp default_cacerts do
[cacerts: :public_key.cacerts_get()]
end
else
defp default_cacerts(), do: []
end

defp https_url?(url) when is_binary(url), do: URI.parse(url).scheme == "https"
defp https_url?(_url), do: false

defp hackney_insecure?(options) do
hackney_options = Keyword.get(options, :hackney, [])

Keyword.get(hackney_options, :insecure, false) || :insecure in hackney_options
end

defp build_hackney_proxy_options(%Request{options: options, url: request_url}) do
proxy =
if Keyword.has_key?(options, :proxy) do
Expand Down
38 changes: 37 additions & 1 deletion test/httpoison_base_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,17 @@ defmodule HTTPoisonBaseTest do
end

defp expect_hackney_post_with_proxy(url, proxy) do
expect_hackney_post(url, proxy: proxy)
if String.starts_with?(url, "https://") do
expect(:hackney, :request, fn :post, ^url, [], "body", options ->
assert options[:proxy] == proxy
assert options[:ssl_options][:verify] == :verify_peer
assert is_list(options[:ssl_options][:cacerts])

{:ok, 200, "headers", "response"}
end)
else
expect_hackney_post(url, proxy: proxy)
end
end

defp expect_hackney_post_with_no_proxy(url) do
Expand Down Expand Up @@ -598,6 +608,32 @@ defmodule HTTPoisonBaseTest do
)
end

test "https requests use public key CA certs by default" do
expect(:hackney, :request, fn :get, "https://localhost", [], "", [ssl_options: opts] ->
assert opts[:verify] == :verify_peer
assert opts[:customize_hostname_check][:match_fun]
assert is_list(opts[:cacerts])
assert opts[:cacerts] == :public_key.cacerts_get()

{:ok, 200, "headers", "response"}
end)

assert %HTTPoison.Response{status_code: 200} = HTTPoison.get!("https://localhost")
end

test "explicit cacertfile is not replaced by public key CA certs" do
expect(:hackney, :request, fn :get, "https://localhost", [], "", [ssl_options: opts] ->
assert opts[:verify] == :verify_peer
refute Keyword.has_key?(opts, :cacerts)
assert opts[:cacertfile] == "certs/ca.crt"

{:ok, 200, "headers", "response"}
end)

assert %HTTPoison.Response{status_code: 200} =
HTTPoison.get!("https://localhost", [], ssl: [cacertfile: "certs/ca.crt"])
end

test "passing follow_redirect option" do
expect(:hackney, :request, fn :post,
"http://localhost",
Expand Down