diff --git a/test/support/tls.ex b/test/support/tls.ex new file mode 100644 index 00000000..c2db05f9 --- /dev/null +++ b/test/support/tls.ex @@ -0,0 +1,59 @@ +defmodule Tesla.TestSupport.TLS do + @moduledoc """ + TLS material for the local HTTPS test servers, generated once per test run. + + The certificate bundled with httparrot expires, so tests use a freshly issued + chain instead of files that can go stale. + """ + + @key {__MODULE__, :files} + + def generate! do + %{server_config: server_config, client_config: client_config} = + :public_key.pkix_test_data(%{ + server_chain: %{ + root: cert_opts(), + peer: [extensions: [localhost_subject_alt_name()]] ++ cert_opts() + }, + client_chain: %{root: cert_opts(), peer: cert_opts()} + }) + + dir = Path.join(System.tmp_dir!(), "tesla-test-tls-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + + {key_type, key_der} = Keyword.fetch!(server_config, :key) + + files = %{ + cacertfile: + write_pem!(dir, "ca.crt", Enum.map(client_config[:cacerts], &{:Certificate, &1})), + certfile: + write_pem!(dir, "server.crt", [{:Certificate, Keyword.fetch!(server_config, :cert)}]), + keyfile: write_pem!(dir, "server.key", [{key_type, key_der}]) + } + + :persistent_term.put(@key, files) + files + end + + def cacertfile, do: fetch!(:cacertfile) + def certfile, do: fetch!(:certfile) + def keyfile, do: fetch!(:keyfile) + + defp fetch!(name), do: Map.fetch!(:persistent_term.get(@key), name) + + defp write_pem!(dir, name, entries) do + path = Path.join(dir, name) + + pem = + :public_key.pem_encode(Enum.map(entries, fn {type, der} -> {type, der, :not_encrypted} end)) + + File.write!(path, pem) + path + end + + defp cert_opts, do: [key: {:namedCurve, :secp256r1}, digest: :sha256] + + defp localhost_subject_alt_name do + {:Extension, {2, 5, 29, 17}, false, [dNSName: ~c"localhost", iPAddress: <<127, 0, 0, 1>>]} + end +end diff --git a/test/tesla/adapter/finch_test.exs b/test/tesla/adapter/finch_test.exs index aeede193..657413f0 100644 --- a/test/tesla/adapter/finch_test.exs +++ b/test/tesla/adapter/finch_test.exs @@ -17,7 +17,7 @@ defmodule Tesla.Adapter.FinchTest do pools: %{ @https => [ conn_opts: [ - transport_opts: [cacertfile: "#{:code.priv_dir(:httparrot)}/ssl/server-ca.crt"] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ] ] } diff --git a/test/tesla/adapter/gun_test.exs b/test/tesla/adapter/gun_test.exs index e0f6d157..9e27daf0 100644 --- a/test/tesla/adapter/gun_test.exs +++ b/test/tesla/adapter/gun_test.exs @@ -14,7 +14,7 @@ defmodule Tesla.Adapter.GunTest do use Tesla.AdapterCase.SSL, certificates_verification: true, transport_opts: [ - cacertfile: Path.join([to_string(:code.priv_dir(:httparrot)), "/ssl/server-ca.crt"]) + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] alias Tesla.Adapter.Gun @@ -119,7 +119,7 @@ defmodule Tesla.Adapter.GunTest do call(request, certificates_verification: true, transport_opts: [ - cacertfile: "#{:code.priv_dir(:httparrot)}/ssl/server-ca.crt" + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] ) end @@ -678,7 +678,7 @@ defmodule Tesla.Adapter.GunTest do call(request, tls_opts: [ verify: :verify_peer, - cacertfile: "#{:code.priv_dir(:httparrot)}/ssl/server-ca.crt" + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] ) end diff --git a/test/tesla/adapter/hackney_test.exs b/test/tesla/adapter/hackney_test.exs index f97b53e8..69e038a9 100644 --- a/test/tesla/adapter/hackney_test.exs +++ b/test/tesla/adapter/hackney_test.exs @@ -10,7 +10,7 @@ defmodule Tesla.Adapter.HackneyTest do use Tesla.AdapterCase.SSL, ssl_options: [ verify: :verify_peer, - cacertfile: Path.join([to_string(:code.priv_dir(:httparrot)), "/ssl/server-ca.crt"]) + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] alias Tesla.Env diff --git a/test/tesla/adapter/httpc_test.exs b/test/tesla/adapter/httpc_test.exs index 745b648a..5672e305 100644 --- a/test/tesla/adapter/httpc_test.exs +++ b/test/tesla/adapter/httpc_test.exs @@ -9,7 +9,7 @@ defmodule Tesla.Adapter.HttpcTest do use Tesla.AdapterCase.SSL, ssl: [ verify: :verify_peer, - cacertfile: Path.join([to_string(:code.priv_dir(:httparrot)), "/ssl/server-ca.crt"]) + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] # :httpc accepts only a fixed set of method atoms, which as of OTP 29 does diff --git a/test/tesla/adapter/ibrowse_test.exs b/test/tesla/adapter/ibrowse_test.exs index 7ea28f67..18b3c50a 100644 --- a/test/tesla/adapter/ibrowse_test.exs +++ b/test/tesla/adapter/ibrowse_test.exs @@ -15,7 +15,7 @@ defmodule Tesla.Adapter.IbrowseTest do # use Tesla.AdapterCase.SSL, # ssl_options: [ # verify: :verify_peer, - # cacertfile: Path.join([to_string(:code.priv_dir(:httparrot)), "/ssl/server-ca.crt"]) + # cacertfile: Tesla.TestSupport.TLS.cacertfile() # ] # ibrowse supports only a fixed set of method atoms and would crash its diff --git a/test/tesla/adapter/mint_test.exs b/test/tesla/adapter/mint_test.exs index 8171af9f..5958b81b 100644 --- a/test/tesla/adapter/mint_test.exs +++ b/test/tesla/adapter/mint_test.exs @@ -17,7 +17,7 @@ defmodule Tesla.Adapter.MintTest do use Tesla.AdapterCase.SSL, transport_opts: [ - cacertfile: Path.join([to_string(:code.priv_dir(:httparrot)), "/ssl/server-ca.crt"]) + cacertfile: Tesla.TestSupport.TLS.cacertfile() ] test "timeout request" do @@ -312,7 +312,7 @@ defmodule Tesla.Adapter.MintTest do assert {:ok, %Env{} = response} = call(request, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ) assert response.status == 200 @@ -336,7 +336,7 @@ defmodule Tesla.Adapter.MintTest do assert {:ok, %Env{} = response} = call(request, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ) assert response.status == 200 @@ -357,7 +357,7 @@ defmodule Tesla.Adapter.MintTest do assert {:ok, %Env{} = response} = call(request, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ) assert response.status == 200 @@ -380,7 +380,7 @@ defmodule Tesla.Adapter.MintTest do assert {:ok, %Env{} = response} = call(request, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ) assert response.status == 200 @@ -402,7 +402,7 @@ defmodule Tesla.Adapter.MintTest do assert {:ok, %Env{} = response} = call(request, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()] + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()] ) assert response.status == 200 @@ -414,15 +414,14 @@ defmodule Tesla.Adapter.MintTest do setup do listener_ref = :"mint-early-response-#{System.unique_integer([:positive])}" dispatch = early_response_dispatch() - priv_dir = :code.priv_dir(:httparrot) {:ok, _pid} = :cowboy.start_tls( listener_ref, [ port: 0, - certfile: priv_dir ++ ~c"/ssl/server.crt", - keyfile: priv_dir ++ ~c"/ssl/server.key" + certfile: Tesla.TestSupport.TLS.certfile(), + keyfile: Tesla.TestSupport.TLS.keyfile() ], %{env: %{dispatch: dispatch}} ) @@ -433,7 +432,7 @@ defmodule Tesla.Adapter.MintTest do {:ok, early_response_url: "https://localhost:#{port}", - early_response_cacertfile: Path.join([to_string(priv_dir), "ssl/server-ca.crt"])} + early_response_cacertfile: Tesla.TestSupport.TLS.cacertfile()} end test "returns the response body without waiting for another packet", %{ @@ -508,15 +507,14 @@ defmodule Tesla.Adapter.MintTest do describe "issue #394 - handle HTTP/2 connection window exhaustion" do setup do listener_ref = :"mint-connection-window-#{System.unique_integer([:positive])}" - priv_dir = :code.priv_dir(:httparrot) {:ok, _pid} = :cowboy.start_tls( listener_ref, [ port: 0, - certfile: priv_dir ++ ~c"/ssl/server.crt", - keyfile: priv_dir ++ ~c"/ssl/server.key" + certfile: Tesla.TestSupport.TLS.certfile(), + keyfile: Tesla.TestSupport.TLS.keyfile() ], %{ env: %{dispatch: upload_echo_dispatch()}, @@ -532,7 +530,7 @@ defmodule Tesla.Adapter.MintTest do {:ok, upload_url: "https://localhost:#{port}", - upload_cacertfile: Path.join([to_string(priv_dir), "ssl/server-ca.crt"])} + upload_cacertfile: Tesla.TestSupport.TLS.cacertfile()} end test "uploads a body that exhausts the connection window before the stream window", %{ @@ -712,15 +710,14 @@ defmodule Tesla.Adapter.MintTest do setup do listener_ref = @internal_error_listener_ref dispatch = internal_error_dispatch() - priv_dir = :code.priv_dir(:httparrot) {:ok, _pid} = :cowboy.start_tls( listener_ref, [ port: 0, - certfile: priv_dir ++ ~c"/ssl/server.crt", - keyfile: priv_dir ++ ~c"/ssl/server.key" + certfile: Tesla.TestSupport.TLS.certfile(), + keyfile: Tesla.TestSupport.TLS.keyfile() ], %{ env: %{dispatch: dispatch}, @@ -734,7 +731,7 @@ defmodule Tesla.Adapter.MintTest do {:ok, reset_url: "https://localhost:#{port}", - reset_cacertfile: Path.join([to_string(priv_dir), "ssl/server-ca.crt"])} + reset_cacertfile: Tesla.TestSupport.TLS.cacertfile()} end test "Mint emits server_closed_request from a live HTTP/2 peer", %{ @@ -878,23 +875,18 @@ defmodule Tesla.Adapter.MintTest do Jason.decode!(body) end - defp httparrot_cacertfile do - Path.join([to_string(:code.priv_dir(:httparrot)), "ssl/server-ca.crt"]) - end - describe "issue #450 - handle missing Mint response types" do setup do listener_ref = @push_promise_listener_ref dispatch = push_promise_dispatch() - priv_dir = :code.priv_dir(:httparrot) {:ok, _pid} = :cowboy.start_tls( listener_ref, [ port: 0, - certfile: priv_dir ++ ~c"/ssl/server.crt", - keyfile: priv_dir ++ ~c"/ssl/server.key" + certfile: Tesla.TestSupport.TLS.certfile(), + keyfile: Tesla.TestSupport.TLS.keyfile() ], %{env: %{dispatch: dispatch}} ) @@ -904,8 +896,7 @@ defmodule Tesla.Adapter.MintTest do {_, port} = :ranch.get_addr(listener_ref) {:ok, - push_url: "https://localhost:#{port}", - push_cacertfile: Path.join([to_string(priv_dir), "ssl/server-ca.crt"])} + push_url: "https://localhost:#{port}", push_cacertfile: Tesla.TestSupport.TLS.cacertfile()} end test "handles connection errors gracefully" do @@ -1051,7 +1042,6 @@ defmodule Tesla.Adapter.MintTest do describe "HTTP/2 connection shared with another request" do setup do listener_ref = :"mint-shared-http2-#{System.unique_integer([:positive])}" - priv_dir = :code.priv_dir(:httparrot) dispatch = :cowboy_router.compile([ @@ -1067,8 +1057,8 @@ defmodule Tesla.Adapter.MintTest do listener_ref, [ port: 0, - certfile: priv_dir ++ ~c"/ssl/server.crt", - keyfile: priv_dir ++ ~c"/ssl/server.key" + certfile: Tesla.TestSupport.TLS.certfile(), + keyfile: Tesla.TestSupport.TLS.keyfile() ], %{ env: %{dispatch: dispatch}, @@ -1083,7 +1073,7 @@ defmodule Tesla.Adapter.MintTest do {:ok, conn} = Mint.HTTP.connect(:https, "localhost", port, protocols: [:http2], - transport_opts: [cacertfile: httparrot_cacertfile()], + transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()], mode: :passive ) @@ -1175,7 +1165,7 @@ defmodule Tesla.Adapter.MintTest do end test "verifies the peer with the configured cacertfile" do - Application.put_env(:tesla, Tesla.Adapter.Mint, cacert: httparrot_cacertfile()) + Application.put_env(:tesla, Tesla.Adapter.Mint, cacert: Tesla.TestSupport.TLS.cacertfile()) request = %Env{method: :get, url: "#{@https}/ip"} @@ -1183,7 +1173,7 @@ defmodule Tesla.Adapter.MintTest do end test "adds the configured cacertfile to the transport options it was given" do - Application.put_env(:tesla, Tesla.Adapter.Mint, cacert: httparrot_cacertfile()) + Application.put_env(:tesla, Tesla.Adapter.Mint, cacert: Tesla.TestSupport.TLS.cacertfile()) request = %Env{method: :get, url: "#{@https}/ip"} @@ -1196,7 +1186,7 @@ defmodule Tesla.Adapter.MintTest do request = %Env{method: :get, url: "#{@https}/ip"} assert {:ok, %Env{status: 200}} = - call(request, transport_opts: [cacertfile: httparrot_cacertfile()]) + call(request, transport_opts: [cacertfile: Tesla.TestSupport.TLS.cacertfile()]) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 485190c4..e71c5b48 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,20 @@ clients = [:ibrowse, :hackney, :gun, :finch, :castore, :mint] Enum.map(clients, &Application.ensure_all_started/1) +tls = Tesla.TestSupport.TLS.generate!() +:ok = :cowboy.stop_listener(:https) + +{:ok, _} = + :cowboy.start_tls( + :https, + [ + port: Application.fetch_env!(:httparrot, :https_port), + certfile: tls.certfile, + keyfile: tls.keyfile + ], + :ranch.get_protocol_options(:http) + ) + Mox.defmock(Tesla.TestSupport.MockAdapter, for: Tesla.Adapter) ExUnit.start()