defmodule X509.Test.Server do
@moduledoc """
Simple TLS server for hosting `X509.Test.Suite` scenarios.
"""
use GenServer
@doc """
Starts a test server for the given test suite.
## Options:
* `:port` - the TCP port to listen on; defaults to 0, meaning an ephemeral
port is selected by the operating system, which may be retrieved using
`get_port/1`
* `:response` - the data to send back to clients when a successful connection
is established (default: "OK")
"""
@spec start_link({X509.Test.Suite.t(), Keyword.t()}) :: GenServer.on_start()
def start_link({suite, opts}) do
GenServer.start_link(__MODULE__, [suite, opts])
end
@doc """
Returns the TCP port number on which the specified X509.Test.Server instance
is listening.
"""
@spec get_port(pid()) :: :inet.port_number()
def get_port(pid) do
GenServer.call(pid, :get_port)
end
# Callbacks
defmodule State do
@moduledoc false
defstruct [:listen_socket, :port, :suite, :response]
end
@impl true
def init([suite, opts]) do
Application.ensure_all_started(:ssl)
port = Keyword.get(opts, :port, 0)
response = Keyword.get(opts, :response, "OK")
with {:ok, listen_socket} <- :gen_tcp.listen(port, []),
{:ok, {_, port}} <- :inet.sockname(listen_socket),
{:ok, _} <- :prim_inet.async_accept(listen_socket, -1) do
{:ok, %State{listen_socket: listen_socket, port: port, suite: suite, response: response}}
else
error ->
{:stop, error}
end
end
@impl true
def handle_call(:get_port, _from, %State{port: port} = state) do
{:reply, port, state}
end
@impl true
def handle_info({:inet_async, listen_socket, _ref, {:ok, socket}}, state) do
:inet_db.register_socket(socket, :inet_tcp)
pid =
spawn_link(fn ->
receive do
:start -> worker(socket, state.suite, state.response)
after
250 -> :gen_tcp.close(socket)
end
end)
:gen_tcp.controlling_process(socket, pid)
send(pid, :start)
{:ok, _} = :prim_inet.async_accept(listen_socket, -1)
{:noreply, state}
end
defp worker(socket, suite, response) do
opts =
[
active: false,
sni_fun: X509.Test.Suite.sni_fun(suite),
reuse_sessions: false
] ++ log_opts()
case handshake(socket, opts, 1000) do
{:ok, ssl_socket} ->
flush(ssl_socket)
:ssl.send(ssl_socket, response)
:ssl.close(ssl_socket)
{:error, _reason} ->
:gen_tcp.close(socket)
end
end
if Code.ensure_loaded?(:ssl) and function_exported?(:ssl, :handshake, 3) do
defp handshake(socket, opts, timeout) do
:ssl.handshake(socket, opts, timeout)
end
else
defp handshake(socket, opts, timeout) do
:ssl.ssl_accept(socket, opts, timeout)
end
end
defp flush(ssl_socket) do
case :ssl.recv(ssl_socket, 0, 100) do
{:ok, _data} ->
flush(ssl_socket)
_done ->
:done
end
end
def log_opts do
if version(:ssl) >= [9, 3] do
[log_level: :emergency]
else
[log_alert: false]
end
end
defp version(application) do
application
|> Application.spec()
|> Keyword.get(:vsn)
|> to_string()
|> String.split(".")
|> Enum.map(&String.to_integer/1)
end
end