defmodule X509.Test.CRLServer do
@moduledoc """
Simple CRL responder for use in test suites.
"""
use GenServer
@doc """
Starts a CRL responder.
## 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`
"""
@spec start_link(Keyword.t()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, 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
@doc """
Adds or updates the CRL at the given path.
"""
@spec put_crl(pid(), String.t(), X509.CRL.t()) :: :ok
def put_crl(pid, path, crl) do
GenServer.call(pid, {:put_crl, path, crl})
end
# Callbacks
defmodule State do
@moduledoc false
defstruct [:listen_socket, :port, :crl_map]
end
@impl true
def init(opts) do
port = Keyword.get(opts, :port, 0)
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, crl_map: %{}}}
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_call({:put_crl, path, crl}, _from, %State{crl_map: crl_map} = state) do
{:reply, :ok, %{state | crl_map: Map.put(crl_map, path, crl)}}
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.crl_map)
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, crl_map) do
:inet.setopts(socket, packet: :http_bin)
case :gen_tcp.recv(socket, 0) do
{:ok, {:http_request, :GET, {:abs_path, path}, {1, 1}}} ->
:inet.setopts(socket, packet: :httph_bin)
flush_headers(socket, path, crl_map)
_ ->
:gen_tcp.close(socket)
end
end
defp flush_headers(socket, path, crl_map) do
case :gen_tcp.recv(socket, 0) do
{:ok, {:http_header, _, _, _, _}} ->
flush_headers(socket, path, crl_map)
{:ok, :http_eoh} ->
case Map.get(crl_map, path) do
nil ->
X509.Util.warn("No CRL defined for #{path}")
respond(socket, 404)
:gen_tcp.close(socket)
crl ->
# Logger.info("CRL requested: #{path}")
respond(socket, 200, X509.CRL.to_der(crl))
:gen_tcp.close(socket)
end
_ ->
:gen_tcp.close(socket)
end
end
defp respond(socket, 404) do
:gen_tcp.send(socket, [
"HTTP/1.1 404 Not found\r\n",
"Content-Length: 0\r\n",
"Connection: close\r\n",
"\r\n"
])
end
defp respond(socket, 200, der) do
:gen_tcp.send(socket, [
"HTTP/1.1 200 OK\r\n",
"Content-Type: application/x-pkcs7-crl\r\n",
"Content-Length: #{byte_size(der)}\r\n",
"Connection: close\r\n",
"\r\n",
der
])
end
end