Current section
Files
Jump to
Current section
Files
lib/test_server/http_server/httpd.ex
if Code.ensure_loaded?(:httpd) do
defmodule TestServer.HTTPServer.Httpd do
@moduledoc false
@behaviour TestServer.HTTPServer
@impl TestServer.HTTPServer
def start(instance, port, scheme, options, httpd_options) do
httpd_options =
httpd_options
|> Keyword.put(:port, port)
|> Keyword.put(:modules, [__MODULE__])
|> Keyword.put_new(:server_name, 'Httpd Test Server')
|> Keyword.put_new(:document_root, '/tmp')
|> Keyword.put_new(:server_root, '/tmp')
|> Keyword.put_new(:handler_plug, {TestServer.Plug, {__MODULE__, [], instance}})
|> Keyword.put_new(:ipfamily, options[:ipfamily])
|> put_tls_options(scheme, options[:tls])
case :inets.start(:httpd, httpd_options) do
{:ok, pid} -> {:ok, pid, httpd_options}
{:error, error} -> {:error, error}
end
end
defp put_tls_options(httpd_options, :http, _tls_options), do: httpd_options
defp put_tls_options(httpd_options, :https, tls_options) do
tls_options = Keyword.put_new(tls_options, :log_level, :warning)
Keyword.put(httpd_options, :socket_type, {:ssl, tls_options})
end
@impl TestServer.HTTPServer
def stop(pid, _httpd_options) do
:inets.stop(:httpd, pid)
end
@impl TestServer.HTTPServer
def get_socket_pid(conn), do: conn.owner
# :httpd record handler
require Record
Record.defrecord :httpd, Record.extract(:mod, from_lib: "inets/include/httpd.hrl") ++ [handler_plug: :undefined, websocket: :undefined]
def unquote(:do)(data) do
{plug, opts} = :httpd_util.lookup(httpd(data, :config_db), :handler_plug)
data
|> conn()
|> plug.call(opts)
|> handle_websocket()
end
defp handle_websocket(%Plug.Conn{adapter: {_adapter, {:websocket, _opts, _data}}}) do
{:proceed, response: {422, 'WebSocket is not supported with httpd!'}}
end
defp handle_websocket(conn) do
%Plug.Conn{adapter: {_, response}} = maybe_send(conn)
{:proceed, response: response}
end
defp maybe_send(%Plug.Conn{state: :unset}), do: raise(Plug.Conn.NotSentError)
defp maybe_send(%Plug.Conn{state: :set} = conn), do: Plug.Conn.send_resp(conn)
defp maybe_send(%Plug.Conn{} = conn), do: conn
# Plug.Conn adapter
@behaviour Plug.Conn.Adapter
alias Plug.Conn
defp conn(data) do
{path, qs} = request_path(data)
{port, host} = host(data)
{_port, remote_ip} = peer(data)
{:ok, remote_ip} = :inet.parse_address(remote_ip)
headers = Enum.map(parsed_header(data), &{to_string(elem(&1, 0)), to_string(elem(&1, 1))})
%Conn{
adapter: {__MODULE__, data},
host: to_string(host),
method: to_string(method(data)),
owner: self(),
path_info: split_path(to_string(path)),
port: port,
remote_ip: remote_ip,
query_string: qs,
req_headers: headers,
request_path: path,
scheme: nil
}
end
defp method(data), do: httpd(data, :method)
defp request_path(data) do
data
|> httpd(:request_uri)
|> to_string()
|> String.split("?")
|> case do
[request_path] -> {request_path, ""}
[request_path, qs] -> {request_path, qs}
end
end
defp host(data) do
data
|> httpd(:parsed_header)
|> Enum.find(&elem(&1, 0) == 'host')
|> Kernel.||({'host', ''})
|> elem(1)
|> to_string()
|> :binary.split(":")
|> case do
[host, port] ->
{Integer.parse(port), host}
[host] ->
{nil, host}
end
end
defp peer(data) do
{:init_data, peer, _, _} = httpd(data, :init_data)
peer
end
defp parsed_header(data), do: httpd(data, :parsed_header)
defp split_path(path) do
segments = :binary.split(path, "/", [:global])
for segment <- segments, segment != "", do: segment
end
@impl Plug.Conn.Adapter
def send_resp(_data, status, headers, body) do
body = String.to_charlist(body)
headers =
headers
|> Kernel.++([{"content-length", to_string(length(body))}])
|> Enum.map(&{String.to_charlist(elem(&1, 0)), String.to_charlist(elem(&1, 1))})
|> Kernel.++([{:code, status}])
{:ok, nil, {:response, headers, body}}
end
@impl Plug.Conn.Adapter
def upgrade(data, :websocket, opts) do
{:ok, {:websocket, opts, data}}
end
def upgrade(_data, _upgrade, _opts), do: {:error, :not_supported}
@impl Plug.Conn.Adapter
def get_http_protocol(data), do: httpd(data, :http_version)
@impl Plug.Conn.Adapter
def read_req_body(data, _opts), do: {:ok, to_string(httpd(data, :entity_body)), data}
# Callbacks yet to be implemented
@impl Plug.Conn.Adapter
def send_file(_, _, _, _, _, _), do: {:ok, nil, nil}
@impl Plug.Conn.Adapter
def send_chunked(_, _, _), do: {:ok, nil, nil}
@impl Plug.Conn.Adapter
def chunk(_, _), do: {:error, :not_implemented}
@impl Plug.Conn.Adapter
def push(_, _, _), do: {:error, :not_implemented}
@impl Plug.Conn.Adapter
def inform(_, _, _), do: {:error, :not_implemented}
@impl Plug.Conn.Adapter
def get_peer_data(_), do: %{address: {127, 0, 0, 1}, port: 0, ssl_cert: nil}
end
end