From 2a2ffecf5c03280fb67176d9504eab042a50baa6 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 10 Oct 2023 17:20:49 -0400 Subject: [PATCH 1/4] feat: add ssl opt to httpc by default fixes #293 Signed-off-by: Yordis Prieto --- lib/tesla/adapter/httpc.ex | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex index aa6f9550..132a3160 100644 --- a/lib/tesla/adapter/httpc.ex +++ b/lib/tesla/adapter/httpc.ex @@ -18,6 +18,7 @@ defmodule Tesla.Adapter.Httpc do @impl Tesla.Adapter def call(env, opts) do opts = Tesla.Adapter.opts(@override_defaults, env, opts) + opts = Tesla.Adapter.opts(default_ssl_opt(), env, opts) with {:ok, {status, headers, body}} <- request(env, opts) do {:ok, format_response(env, status, headers, body)} @@ -28,6 +29,18 @@ defmodule Tesla.Adapter.Httpc do %{env | status: status, headers: format_headers(headers), body: format_body(body)} end + defp default_ssl_opt do + # TODO: verify that requires OTP 25+ + # TODO: verify that does not require any Elixir version + [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + ] + end + # from http://erlang.org/doc/man/httpc.html # headers() = [header()] # header() = {field(), value()} From 063f26097c11c6d6d5551cfe999e7ed354ee4673 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 10 Oct 2023 18:19:43 -0400 Subject: [PATCH 2/4] add badssl test --- lib/tesla/adapter/httpc.ex | 3 ++- test/tesla/adapter/httpc_test.exs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex index 132a3160..30a48b30 100644 --- a/lib/tesla/adapter/httpc.ex +++ b/lib/tesla/adapter/httpc.ex @@ -18,7 +18,7 @@ defmodule Tesla.Adapter.Httpc do @impl Tesla.Adapter def call(env, opts) do opts = Tesla.Adapter.opts(@override_defaults, env, opts) - opts = Tesla.Adapter.opts(default_ssl_opt(), env, opts) +# opts = Tesla.Adapter.opts(default_ssl_opt(), env, opts) with {:ok, {status, headers, body}} <- request(env, opts) do {:ok, format_response(env, status, headers, body)} @@ -32,6 +32,7 @@ defmodule Tesla.Adapter.Httpc do defp default_ssl_opt do # TODO: verify that requires OTP 25+ # TODO: verify that does not require any Elixir version + # TODO: maybe use Castore for now? cacertfile: CAStore.file_path(), [ verify: :verify_peer, cacerts: :public_key.cacerts_get(), diff --git a/test/tesla/adapter/httpc_test.exs b/test/tesla/adapter/httpc_test.exs index 4e3a7aa8..b089c046 100644 --- a/test/tesla/adapter/httpc_test.exs +++ b/test/tesla/adapter/httpc_test.exs @@ -45,4 +45,32 @@ defmodule Tesla.Adapter.HttpcTest do assert data["headers"]["content-type"] == "text/plain" end + + describe "badssl" do + @describetag :integration + + test "expired.badssl.com" do + assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://expired.badssl.com") + end + + test "wrong.host.badssl.com" do + assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://wrong.host.badssl.com") + end + + test "self-signed.badssl.com" do + assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://self-signed.badssl.com") + end + + test "untrusted-root.badssl.com" do + assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://untrusted-root.badssl.com") + end + + test "revoked.badssl.com" do + assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://revoked.badssl.com") + end +# TODO: figure out how to test this +# test "pinning-test.badssl.com" do +# assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://pinning-test.badssl.com") +# end + end end From 42740e721acefa1705259003a4c9455f8915611f Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 10 Oct 2023 18:36:07 -0400 Subject: [PATCH 3/4] remove test case --- test/tesla/adapter/httpc_test.exs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/tesla/adapter/httpc_test.exs b/test/tesla/adapter/httpc_test.exs index b089c046..99c1da56 100644 --- a/test/tesla/adapter/httpc_test.exs +++ b/test/tesla/adapter/httpc_test.exs @@ -68,9 +68,5 @@ defmodule Tesla.Adapter.HttpcTest do test "revoked.badssl.com" do assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://revoked.badssl.com") end -# TODO: figure out how to test this -# test "pinning-test.badssl.com" do -# assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://pinning-test.badssl.com") -# end end end From c534b28c9d5a56a15c7813a35f0943e3221f7512 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 10 Oct 2023 19:09:48 -0400 Subject: [PATCH 4/4] check otp version --- lib/tesla/adapter/httpc.ex | 41 ++++++++++++++++++++----------- mix.exs | 2 +- test/tesla/adapter/httpc_test.exs | 18 ++++++++++---- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex index 30a48b30..10b365e7 100644 --- a/lib/tesla/adapter/httpc.ex +++ b/lib/tesla/adapter/httpc.ex @@ -8,6 +8,8 @@ defmodule Tesla.Adapter.Httpc do consistency between adapters """ + current_otp_version = List.to_integer(:erlang.system_info(:otp_release)) + @behaviour Tesla.Adapter import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1] alias Tesla.Multipart @@ -18,28 +20,39 @@ defmodule Tesla.Adapter.Httpc do @impl Tesla.Adapter def call(env, opts) do opts = Tesla.Adapter.opts(@override_defaults, env, opts) -# opts = Tesla.Adapter.opts(default_ssl_opt(), env, opts) + opts = add_default_ssl_opt(env, opts) with {:ok, {status, headers, body}} <- request(env, opts) do {:ok, format_response(env, status, headers, body)} end end - defp format_response(env, {_, status, _}, headers, body) do - %{env | status: status, headers: format_headers(headers), body: format_body(body)} + # TODO: remove this once OTP 25+ is required + if current_otp_version >= 25 do + def add_default_ssl_opt(env, opts) do + default_ssl_opt = [ + ssl: [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + depth: 3, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + crl_check: true, + crl_cache: {:ssl_crl_cache, {:internal, [http: 1000]}} + ] + ] + + Tesla.Adapter.opts(default_ssl_opt, env, opts) + end + else + def add_default_ssl_opt(_env, opts) do + opts + end end - defp default_ssl_opt do - # TODO: verify that requires OTP 25+ - # TODO: verify that does not require any Elixir version - # TODO: maybe use Castore for now? cacertfile: CAStore.file_path(), - [ - verify: :verify_peer, - cacerts: :public_key.cacerts_get(), - customize_hostname_check: [ - match_fun: :public_key.pkix_verify_hostname_match_fun(:https) - ] - ] + defp format_response(env, {_, status, _}, headers, body) do + %{env | status: status, headers: format_headers(headers), body: format_body(body)} end # from http://erlang.org/doc/man/httpc.html diff --git a/mix.exs b/mix.exs index 9acf4a0d..47fdeca8 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule Tesla.Mixfile do test_coverage: [tool: ExCoveralls], dialyzer: [ plt_core_path: "_build/#{Mix.env()}", - plt_add_apps: [:mix, :inets, :idna, :ssl_verify_fun, :ex_unit], + plt_add_apps: [:public_key, :mix, :inets, :idna, :ssl_verify_fun, :ex_unit], plt_add_deps: :apps_direct ], docs: docs(), diff --git a/test/tesla/adapter/httpc_test.exs b/test/tesla/adapter/httpc_test.exs index 99c1da56..a498288f 100644 --- a/test/tesla/adapter/httpc_test.exs +++ b/test/tesla/adapter/httpc_test.exs @@ -50,23 +50,31 @@ defmodule Tesla.Adapter.HttpcTest do @describetag :integration test "expired.badssl.com" do - assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://expired.badssl.com") + assert {:error, :econnrefused} = + Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://expired.badssl.com") end test "wrong.host.badssl.com" do - assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://wrong.host.badssl.com") + assert {:error, :econnrefused} = + Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://wrong.host.badssl.com") end test "self-signed.badssl.com" do - assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://self-signed.badssl.com") + assert {:error, :econnrefused} = + Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://self-signed.badssl.com") end test "untrusted-root.badssl.com" do - assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://untrusted-root.badssl.com") + assert {:error, :econnrefused} = + Tesla.get( + Tesla.client([], Tesla.Adapter.Httpc), + "https://untrusted-root.badssl.com" + ) end test "revoked.badssl.com" do - assert {:error, :econnrefused} = Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://revoked.badssl.com") + assert {:error, :econnrefused} = + Tesla.get(Tesla.client([], Tesla.Adapter.Httpc), "https://revoked.badssl.com") end end end