update
This commit is contained in:
105
phoenix/deps/websock_adapter/lib/websock_adapter.ex
Normal file
105
phoenix/deps/websock_adapter/lib/websock_adapter.ex
Normal file
@@ -0,0 +1,105 @@
|
||||
defmodule WebSockAdapter do
|
||||
@moduledoc """
|
||||
Defines adapters to allow common Web Servers to serve applications via the `WebSock` API.
|
||||
Also provides a consistent upgrade facility to upgrade `Plug.Conn` requests to `WebSock`
|
||||
connections for supported servers.
|
||||
"""
|
||||
|
||||
@typedoc "The type of a supported connection option"
|
||||
@type connection_opt ::
|
||||
{:compress, boolean()}
|
||||
| {:timeout, timeout()}
|
||||
| {:max_frame_size, non_neg_integer()}
|
||||
| {:fullsweep_after, non_neg_integer()}
|
||||
| {:max_heap_size, :erlang.max_heap_size()}
|
||||
| {:validate_utf8, boolean()}
|
||||
| {:active_n, integer()}
|
||||
|
||||
@doc """
|
||||
Upgrades the provided `Plug.Conn` connection request to a `WebSock` connection using the
|
||||
provided `WebSock` compliant module as a handler.
|
||||
|
||||
This function returns the passed `conn` set to an `:upgraded` state. If `early_validate_upgrade`
|
||||
is set to true (as it is by default), the request is first examined to determine if it
|
||||
represents a valid WebSocket upgrade. If errors are discovered in the request, a
|
||||
`WebSockAdapter.UpgradeError` is raised containing information about the failure
|
||||
|
||||
The provided `state` value will be used as the argument for `c:WebSock.init/1` once the WebSocket
|
||||
connection has been successfully negotiated.
|
||||
|
||||
The `opts` keyword list argument allows a number of options to be set on the WebSocket
|
||||
connection. Not all options may be supported by the underlying HTTP server. Possible values are
|
||||
as follows:
|
||||
|
||||
* `early_validate_upgrade`: A boolean indicating whether or not WebSockAdapter should attempt to
|
||||
validate the WebSocket upgrade request before returning from this call. The underlying webserver
|
||||
may still perform its own validation during the actual upgrade process, but since this occurs
|
||||
after the `c:Plug.call/2` lifecycle it can be difficult to meaningfully handle failed upgrades.
|
||||
Having WebSockAdapter do its own checks as part of this call helps to alleviate this. Defaults
|
||||
to `true`
|
||||
* `timeout`: The number of milliseconds to wait after no client data is received before
|
||||
closing the connection. Defaults to `60_000`
|
||||
* `compress`: Whether or not to accept negotiation of a compression extension with the client.
|
||||
Defaults to `false`
|
||||
* `max_frame_size`: The maximum frame size to accept, in octets. If a frame size larger than this
|
||||
is received the connection will be closed. Defaults to `:infinity`
|
||||
* `fullsweep_after`: The maximum number of garbage collections before forcing a fullsweep of
|
||||
the WebSocket connection process. Setting this option requires OTP 24 or newer
|
||||
* `max_heap_size`: The maximum size of the websocket process heap in words, or a configuration
|
||||
map. See `:erlang.max_heap_size()` for more info
|
||||
* `validate_utf8`: Whether the server should verify that the payload of text and close frames is valid UTF-8.
|
||||
This is required by the protocol specification but in some cases it may be more interesting to disable it
|
||||
in order to save resources. Note that binary frames do not have this UTF-8 requirement and are what should be
|
||||
used under normal circumstances if necessary
|
||||
* `active_n`: (Cowboy only) The number of packets Cowboy will request from the socket at once. This can be used to tweak
|
||||
the performance of the server. Higher values reduce the number of times Cowboy need to request more
|
||||
packets from the port driver at the expense of potentially higher memory being used.
|
||||
This option does not apply to Websocket over HTTP/2
|
||||
* `deflate_options`: A keyword list of options to pass to the deflate library.
|
||||
See `Bandit` or `:cow_ws` documentation for more details
|
||||
"""
|
||||
@spec upgrade(Plug.Conn.t(), WebSock.impl(), WebSock.state(), [connection_opt()]) ::
|
||||
Plug.Conn.t()
|
||||
def upgrade(%{adapter: {adapter, _}} = conn, websock, state, opts) do
|
||||
# Do this first so we can identify unsupported adapters
|
||||
tuple = tuple_for(adapter, websock, state, opts)
|
||||
|
||||
if Keyword.get(opts, :early_validate_upgrade, true) do
|
||||
WebSockAdapter.UpgradeValidation.validate_upgrade!(conn)
|
||||
end
|
||||
|
||||
Plug.Conn.upgrade_adapter(conn, :websocket, tuple)
|
||||
end
|
||||
|
||||
defp tuple_for(Bandit.Adapter, websock, state, opts), do: {websock, state, opts}
|
||||
# Support for adapters as specified prior to Bandit 1.4
|
||||
defp tuple_for(Bandit.HTTP1.Adapter, websock, state, opts), do: {websock, state, opts}
|
||||
defp tuple_for(Bandit.HTTP2.Adapter, websock, state, opts), do: {websock, state, opts}
|
||||
|
||||
defp tuple_for(Plug.Cowboy.Conn, websock, state, opts) do
|
||||
cowboy_opts =
|
||||
opts
|
||||
|> Enum.flat_map(fn
|
||||
{:timeout, timeout} -> [idle_timeout: timeout]
|
||||
{:compress, _} = opt -> [opt]
|
||||
{:max_frame_size, _} = opt -> [opt]
|
||||
{:validate_utf8, _} = opt -> [opt]
|
||||
{:active_n, _} = opt -> [opt]
|
||||
{:deflate_options, deflate_options} -> [deflate_opts: deflate_options]
|
||||
_other -> []
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
process_flags =
|
||||
opts
|
||||
|> Keyword.take([:fullsweep_after, :max_heap_size])
|
||||
|> Map.new()
|
||||
|
||||
{WebSockAdapter.CowboyAdapter, {websock, process_flags, state}, cowboy_opts}
|
||||
end
|
||||
|
||||
defp tuple_for(Plug.Adapters.Test.Conn, websock, state, opts), do: {websock, state, opts}
|
||||
|
||||
defp tuple_for(adapter, _websock, _state, _opts),
|
||||
do: raise(ArgumentError, "Unknown adapter #{inspect(adapter)}")
|
||||
end
|
||||
@@ -0,0 +1,109 @@
|
||||
if Code.ensure_loaded?(:cowboy_websocket) do
|
||||
defmodule WebSockAdapter.CowboyAdapter do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour :cowboy_websocket
|
||||
|
||||
# Cowboy never actually calls this implementation due to the way that Plug.Cowboy signals
|
||||
# upgrades; it's just here to quell compiler warnings
|
||||
@impl true
|
||||
def init(req, state), do: {:cowboy_websocket, req, state}
|
||||
|
||||
@impl true
|
||||
def websocket_init({handler, process_flags, state}) do
|
||||
for {key, value} <- process_flags do
|
||||
:erlang.process_flag(key, value)
|
||||
end
|
||||
|
||||
handler.init(state)
|
||||
|> handle_reply(handler)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def websocket_handle({opcode, payload}, {handler, state}) when opcode in [:text, :binary] do
|
||||
handler.handle_in({payload, opcode: opcode}, state)
|
||||
|> handle_reply(handler)
|
||||
end
|
||||
|
||||
def websocket_handle({opcode, payload}, {handler, state}) when opcode in [:ping, :pong] do
|
||||
if function_exported?(handler, :handle_control, 2) do
|
||||
handler.handle_control({payload, opcode: opcode}, state)
|
||||
else
|
||||
{:ok, state}
|
||||
end
|
||||
|> handle_reply(handler)
|
||||
end
|
||||
|
||||
def websocket_handle(opcode, handler_state) when opcode in [:ping, :pong] do
|
||||
websocket_handle({opcode, nil}, handler_state)
|
||||
end
|
||||
|
||||
def websocket_handle(_other, handler_state) do
|
||||
{:ok, handler_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def websocket_info(message, {handler, state}) do
|
||||
handler.handle_info(message, state)
|
||||
|> handle_reply(handler)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate({:remote, code, _}, _req, {handler, state})
|
||||
when code in 1000..1003 or code in 1005..1011 or code == 1015 do
|
||||
if function_exported?(handler, :terminate, 2) do
|
||||
handler.terminate(:remote, state)
|
||||
end
|
||||
end
|
||||
|
||||
def terminate({:remote, :closed}, _req, {handler, state}) do
|
||||
if function_exported?(handler, :terminate, 2) do
|
||||
handler.terminate(:closed, state)
|
||||
end
|
||||
end
|
||||
|
||||
def terminate(:stop, _req, {handler, state}) do
|
||||
if function_exported?(handler, :terminate, 2) do
|
||||
handler.terminate(:normal, state)
|
||||
end
|
||||
end
|
||||
|
||||
def terminate(reason, _req, {handler, state}) do
|
||||
if function_exported?(handler, :terminate, 2) do
|
||||
handler.terminate(reason, state)
|
||||
end
|
||||
end
|
||||
|
||||
# If websocket_init (or the handler's init) raises an error, state may still
|
||||
# be the 3-element tuple that we get at initialization time. Since we have
|
||||
# not yet initialized the websock at this point, just terminate and let
|
||||
# Cowboy do its usual logging
|
||||
def terminate(_reason, _req, {_handler, _process_flags, _state}), do: :ok
|
||||
|
||||
defp handle_reply({:ok, state}, handler), do: {:ok, {handler, state}}
|
||||
defp handle_reply({:push, data, state}, handler), do: {:reply, data, {handler, state}}
|
||||
|
||||
defp handle_reply({:reply, _status, data, state}, handler),
|
||||
do: {:reply, data, {handler, state}}
|
||||
|
||||
defp handle_reply({:stop, {:shutdown, :restart}, state}, handler),
|
||||
do: {:reply, {:close, _restart_code = 1012, <<>>}, {handler, state}}
|
||||
|
||||
defp handle_reply({:stop, _reason, state}, handler), do: {:stop, {handler, state}}
|
||||
|
||||
defp handle_reply({:stop, _reason, close_detail, state}, handler),
|
||||
do: {:reply, close_frame_for_detail(close_detail), {handler, state}}
|
||||
|
||||
defp handle_reply({:stop, _reason, close_detail, messages, state}, handler),
|
||||
do: {:reply, messages ++ [close_frame_for_detail(close_detail)], {handler, state}}
|
||||
|
||||
defp close_frame_for_detail(code) when is_integer(code),
|
||||
do: {:close, code, <<>>}
|
||||
|
||||
defp close_frame_for_detail({code, nil}) when is_integer(code),
|
||||
do: {:close, code, <<>>}
|
||||
|
||||
defp close_frame_for_detail({code, detail}) when is_integer(code) and is_binary(detail),
|
||||
do: {:close, code, detail}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
defmodule WebSockAdapter.UpgradeError do
|
||||
defexception [:message]
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
defmodule WebSockAdapter.UpgradeValidation do
|
||||
@moduledoc """
|
||||
Provides validation of WebSocket upgrade requests as described in RFC6455§4.2 & RFC8441§5
|
||||
|
||||
The `validate_upgrade/1` function is called internally by `WebSockAdapter.upgrade/4`; there is
|
||||
no need to call it yourself before attempting an upgrade (though doing so is harmless)
|
||||
"""
|
||||
|
||||
# credo:disable-for-this-file Credo.Check.Refactor.RedundantWithClauseResult
|
||||
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
|
||||
|
||||
@doc """
|
||||
Validates that the request satisfies the requirements to issue a WebSocket upgrade response.
|
||||
|
||||
Validations are performed based on the clauses laid out in RFC6455§4.2 & RFC8441§5
|
||||
|
||||
This function does not actually perform an upgrade or change the connection in any way.
|
||||
Regardless of whether or not this function indicates a satisfactory connection, the
|
||||
underlying web server MAY still choose to not perform the upgrade (this scenario likely
|
||||
indicates a discrepancy between the validations done here and those done in the underlying web
|
||||
server & would merit further investigation)
|
||||
|
||||
Returns `:ok` if the connection satisfies the requirements for a WebSocket upgrade, and
|
||||
`{:error, reason}` if not
|
||||
"""
|
||||
@spec validate_upgrade(Plug.Conn.t()) :: :ok | {:error, String.t()}
|
||||
def validate_upgrade(conn) do
|
||||
case Plug.Conn.get_http_protocol(conn) do
|
||||
:"HTTP/1.1" -> validate_upgrade_http1(conn)
|
||||
:"HTTP/2" -> validate_upgrade_http2(conn)
|
||||
other -> {:error, "HTTP version #{other} unsupported"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Raising variant of `validate_upgrade/1`.
|
||||
|
||||
Returns `:ok` if the connection satisfies the requirements for a WebSocket upgrade, and raises
|
||||
a `WebSockAdapter.UpgradeError` error if validation fails.
|
||||
"""
|
||||
@spec validate_upgrade!(Plug.Conn.t()) :: :ok
|
||||
def validate_upgrade!(conn) do
|
||||
case validate_upgrade(conn) do
|
||||
:ok -> :ok
|
||||
{:error, reason} -> raise WebSockAdapter.UpgradeError, reason
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the conn per RFC6455§4.2.1
|
||||
defp validate_upgrade_http1(conn) do
|
||||
with :ok <- assert_method(conn, "GET"),
|
||||
:ok <- assert_header_nonempty(conn, "host"),
|
||||
:ok <- assert_header_contains(conn, "connection", "upgrade"),
|
||||
:ok <- assert_header_contains(conn, "upgrade", "websocket"),
|
||||
:ok <- assert_header_nonempty(conn, "sec-websocket-key"),
|
||||
:ok <- assert_header_equals(conn, "sec-websocket-version", "13") do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the conn per RFC8441§5. Note that pseudo headers are not exposed
|
||||
# through Plug so we cannot test for those clauses here
|
||||
defp validate_upgrade_http2(conn) do
|
||||
with :ok <- assert_method(conn, "CONNECT"),
|
||||
:ok <- assert_header_equals(conn, "sec-websocket-version", "13") do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_method(conn, verb) do
|
||||
case conn.method do
|
||||
^verb -> :ok
|
||||
other -> {:error, "HTTP method #{other} unsupported"}
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_nonempty(conn, header) do
|
||||
case Plug.Conn.get_req_header(conn, header) do
|
||||
[] -> {:error, "'#{header}' header is absent"}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_equals(conn, header, expected) do
|
||||
case Plug.Conn.get_req_header(conn, header) |> Enum.map(&String.downcase(&1, :ascii)) do
|
||||
[^expected] -> :ok
|
||||
value -> {:error, "'#{header}' header must equal '#{expected}', got #{inspect(value)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_header_contains(conn, header, needle) do
|
||||
haystack = Plug.Conn.get_req_header(conn, header)
|
||||
|
||||
haystack
|
||||
|> Enum.flat_map(&Plug.Conn.Utils.list/1)
|
||||
|> Enum.any?(&(String.downcase(&1, :ascii) == needle))
|
||||
|> case do
|
||||
true -> :ok
|
||||
false -> {:error, "'#{header}' header must contain '#{needle}', got #{inspect(haystack)}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user