Current section
Files
Jump to
Current section
Files
lib/test_server/http_server.ex
defmodule TestServer.HTTPServer do
@moduledoc """
HTTP server adapter module.
## Usage
defmodule MyApp.MyHTTPServer do
@behaviour TestServer.HTTPServer
@impl true
def start(instance, port, scheme, tls_options, server_options) do
# ...
end
def stop(instance, server_options) do
# ...
end
def get_socket_pid(conn) do
# ...
end
end
"""
@type scheme :: :http | :https
@type instance :: pid()
@type port_number :: :inet.port_number()
@type options :: [tls: keyword(), ipfamily: :inet | :inet6]
@type server_options :: keyword()
@callback start(instance(), port_number(), scheme(), options(), server_options()) :: {:ok, pid(), server_options()} | {:error, any()}
@callback stop(instance(), server_options()) :: :ok | {:error, any()}
@callback get_socket_pid(Plug.Conn.t()) :: pid()
@default_http_server Enum.find_value([
{Bandit, TestServer.HTTPServer.Bandit},
{Plug.Cowboy, TestServer.HTTPServer.Plug.Cowboy},
{:httpd, TestServer.HTTPServer.Httpd}
],
fn {dep, module} ->
if Code.ensure_loaded?(dep), do: {module, []}
end)
@doc false
@spec start(pid(), keyword()) :: {:ok, keyword()} | {:error, any()}
def start(instance, options) do
port = open_port(options)
scheme = parse_scheme(options)
{tls_options, x509_options} = maybe_generate_x509_suite(options, scheme)
ip_family = Keyword.get(options, :ipfamily, :inet)
test_server_options = [tls: tls_options, ipfamily: ip_family]
{mod, server_options} = Keyword.get(options, :http_server, Application.get_env(:test_server, :http_server, @default_http_server))
case mod.start(instance, port, scheme, test_server_options, server_options) do
{:ok, reference, server_options} ->
options =
options
|> Keyword.merge(x509_options)
|> Keyword.put(:scheme, scheme)
|> Keyword.put(:port, port)
|> Keyword.put(:http_server, {mod, server_options})
|> Keyword.put(:http_server_reference, reference)
{:ok, options}
{:error, error} ->
{:error, error}
end
end
defp open_port(options) do
{port, options} =
case Keyword.get(options, :port, 0) do
{port, options} -> {port, options}
port -> {port, []}
end
unless is_integer(port) and port >= 0 and port <= 65_535,
do: raise("Invalid port, got: #{inspect(port)}")
with {:ok, socket} <- :gen_tcp.listen(port, options),
{:ok, port} <- :inet.port(socket),
true <- :erlang.port_close(socket) do
port
else
{:error, error} ->
raise("Could not listen to port #{inspect(port)}, because: #{inspect(error)}")
end
end
defp parse_scheme(options) do
scheme = Keyword.get(options, :scheme, :http)
unless scheme in [:http, :https], do: raise("Invalid scheme, got: #{inspect(scheme)}")
scheme
end
defp maybe_generate_x509_suite(options, :https) do
tls_opts = Keyword.get(options, :tls, [])
case Keyword.has_key?(tls_opts, :key) || Keyword.has_key?(tls_opts, :keyfile) do
true ->
{tls_opts, []}
false ->
suite = X509.Test.Suite.new()
{[
key: {:RSAPrivateKey, X509.PrivateKey.to_der(suite.server_key)},
cert: X509.Certificate.to_der(suite.valid),
cacerts: suite.chain ++ suite.cacerts
], x509_suite: suite}
end
end
defp maybe_generate_x509_suite(_options, :http) do
{[], []}
end
@doc false
@spec stop(keyword()) :: :ok | {:error, any()}
def stop(options) do
{mod, server_options} = Keyword.fetch!(options, :http_server)
reference = Keyword.fetch!(options, :http_server_reference)
mod.stop(reference, server_options)
end
end