defmodule Mix.Tasks.X509.TestServer do @shortdoc "Runs a server for manual TLS client testing" @moduledoc """ Runs a server for manual TLS client testing mix x509.test_server mix x509.test_server test.local Please refer to the documentation for `X509.Test.Suite` for information about the scenarios that may be tested using the server. Requires OTP 20 or later. """ use Mix.Task @http_response ["HTTP/1.1 200 OK", "Content-Length: 2", "Connection: close", "", "OK"] |> Enum.join("\r\n") @doc false def run(all_args) do {_opts, args} = OptionParser.parse!( all_args, aliases: [], strict: [] ) # Start the CRL responder {:ok, crl_server_pid} = X509.Test.CRLServer.start_link([]) crl_server_port = X509.Test.CRLServer.get_port(crl_server_pid) crl_server_uri = "http://localhost:#{crl_server_port}/" suite = case args do [] -> X509.Test.Suite.new(crl_server: crl_server_uri) [domain] -> X509.Test.Suite.new(crl_server: crl_server_uri, domain: domain) end cacertfile = write_cacerts!("cacerts.pem", suite.cacerts) Mix.shell().info("Primary CA certificate store: #{cacertfile}") alternate_cacertfile = write_cacerts!("alternate_cacerts.pem", suite.alternate_cacerts) Mix.shell().info("Secondary CA certificate store: #{alternate_cacertfile}") client_certfile = write_cert!("client.pem", suite.client) write_key!("client_key.pem", suite.client_key) Mix.shell().info("Client certificate and key: #{client_certfile} / [...]/client_key.pem") # Update the CRL server with the generated CRLs suite.crls |> Enum.each(fn {path, crl} -> X509.Test.CRLServer.put_crl(crl_server_pid, "/#{path}", crl) end) # Start the test server with a canned HTTP response, and get the TCP port # number of the server {:ok, pid} = X509.Test.Server.start_link({suite, [response: @http_response]}) port = X509.Test.Server.get_port(pid) Mix.shell().info(""" Server is running on port #{port}. Sample invocation of `curl`: curl --cacert #{cacertfile} https://valid.#{suite.domain}:#{port}/ Please refer to the documenation for X509.Test.Suite for a list of available endpoints and their expected behaviour. """) unless iex_running?(), do: Process.sleep(:infinity) end defp write_cacerts!(filename, certs) do pem = certs |> Enum.map(&certificate_der_to_pem/1) |> Enum.join() path = Path.join(System.tmp_dir!(), filename) File.write!(path, pem) path end defp write_cert!(filename, cert) do path = Path.join(System.tmp_dir!(), filename) File.write!(path, X509.Certificate.to_pem(cert)) path end defp write_key!(filename, key) do path = Path.join(System.tmp_dir!(), filename) File.write!(path, X509.PrivateKey.to_pem(key)) path end defp certificate_der_to_pem(der) do der |> X509.Certificate.from_der!() |> X509.Certificate.to_pem() end defp iex_running? do Code.ensure_loaded?(IEx) and IEx.started?() end end