Current section
Files
Jump to
Current section
Files
lib/buckaroo.ex
defmodule Buckaroo do
@moduledoc ~S"""
Simple `:cowboy` (v2) webserver with support for websockets.
"""
alias Plug.Adapters.Cowboy
@doc ~S"""
Setup a simple webserver handling HTTP requests including websockets.
"""
@spec child_spec(Keyword.t()) :: Supervisor.child_spec()
def child_spec(opts \\ []) do
router =
{opts[:plug] || raise("Need to set plug: ... to the plug router."), opts[:opts] || []}
socket = if s = opts[:socket], do: {s, opts[:socket_opts] || opts[:opts] || []}
options =
[port: 3000, dispatch: [{:_, [{:_, __MODULE__, {socket || router, router}}]}]]
|> Keyword.merge(Keyword.drop(opts, ~w(socket plug opts)a))
|> Keyword.update!(:port, &if(is_binary(&1), do: String.to_integer(&1), else: &1))
{Cowboy, scheme: :http, plug: elem(router, 0), options: options}
end
### Handler ###
@connection Plug.Cowboy.Conn
@already_sent {:plug_conn, :sent}
@behaviour :cowboy_websocket
@impl :cowboy_websocket
def init(req, {{socket, socket_opts}, {plug, plug_opts}}) do
{conn, plug, opts} =
if :cowboy_websocket.is_upgrade_request(req) do
{%{@connection.conn(req) | method: "WEBSOCKET"}, socket, socket_opts}
else
{@connection.conn(req), plug, plug_opts}
end
try do
conn = plug.call(conn, opts)
case Map.get(conn.private, :websocket) do
nil ->
%{adapter: {@connection, req}} = maybe_send(conn, plug)
{:ok, req, {plug, opts}}
{socket, opts} ->
{:cowboy_websocket, req, {socket, {conn, opts}}}
socket ->
{:cowboy_websocket, req, {socket, {conn, []}}}
end
catch
:error, value ->
stack = System.stacktrace()
exception = Exception.normalize(:error, value, stack)
exit({{exception, stack}, {plug, :call, [conn, opts]}})
:throw, value ->
stack = System.stacktrace()
exit({{{:nocatch, value}, stack}, {plug, :call, [conn, opts]}})
:exit, value ->
exit({value, {plug, :call, [conn, opts]}})
after
receive do
@already_sent -> :ok
after
0 -> :ok
end
end
end
defp maybe_send(%Plug.Conn{state: :unset}, _plug), do: raise(Plug.Conn.NotSentError)
defp maybe_send(%Plug.Conn{state: :set} = conn, _plug), do: Plug.Conn.send_resp(conn)
defp maybe_send(%Plug.Conn{} = conn, _plug), do: conn
defp maybe_send(other, plug) do
raise "Cowboy2 adapter expected #{inspect(plug)} to return Plug.Conn but got: " <>
inspect(other)
end
### Handling Socket ###
@impl :cowboy_websocket
def websocket_init({socket, {conn, state}}), do: conn |> socket.init(state) |> result(socket)
@impl :cowboy_websocket
def websocket_handle(frame, {socket, state}),
do: frame |> socket.handle(state) |> result(socket)
@impl :cowboy_websocket
def websocket_info(info, {socket, state}),
do: info |> socket.info(state) |> result(socket)
@impl :cowboy_websocket
def terminate(reason, req, {socket, state}) do
if :erlang.function_exported(socket, :terminate, 3),
do: socket.terminate(reason, req, state),
else: :ok
end
@spec result(tuple, module) :: tuple
defp result({:stop, state}, socket), do: {:stop, {socket, state}}
defp result({:ok, state}, socket), do: {:ok, {socket, state}}
defp result({:ok, state, :hibernate}, socket), do: {:ok, {socket, state}, :hibernate}
defp result({:reply, frame, state}, socket), do: {:reply, frame, {socket, state}}
defp result({:reply, frame, state, :hibernate}, socket),
do: {:reply, frame, {socket, state}, :hibernate}
end