This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,68 @@
## 0.5.9 (4 Nov 2025)
### Enhancements
* Add support for deflate_options configuration (#21, thanks @studzien!)
## 0.5.8 (12 Nov 2024)
### Enhancements
* Improve handling of crashes during WebSock.init/1 calls (#20)
## 0.5.7 (9 Aug 2024)
### Enhancements
* Support use within Plug.Adapters.Test.Conn based tests
## 0.5.6 (25 Mar 2024)
### Enhancements
* Support Bandit 1.4+
* Minor mix packaging improvements
## 0.5.5 (30 Oct 2023)
### Enhancements
* Add a `:max_heap_size` option (#15, thanks @v0idpwn!)
* Validate client upgrade request at the time of upgrade (#14)
## 0.5.4 (15 Aug 2023)
### Enhancements
* Add ability to send preamble frames when closing a connection (#13)
* Improve test coverage (#12)
## 0.5.3 (15 Jun 2023)
### Enhancements
* Support draining signals as used by Phoenix (#10)
## 0.5.2 (15 Jun 2023)
### Changes
* Allow the sending of some extra options to Cowboy (#9)
### Fixes
* Allow terminate/2 to be optional for Cowboy adapter (#8)
* Allow nil detail reasons when closing connections in Cowboy
## 0.5.1 (24 Apr 2023)
### Changes
* Loosen optional dependency versioning on Bandit
* Add examples to documentation, correct minor typos
## 0.5.0 (13 Mar 2023)
### Enhancements
* Add support for `:stop` return tuple to specify explicit close code & detail message

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Mat Trudel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,102 @@
# WebSockAdapter
[![Build Status](https://github.com/phoenixframework/websock_adapter/workflows/Elixir%20CI/badge.svg)](https://github.com/phoenixframework/websock_adapter/actions)
[![Docs](https://img.shields.io/badge/api-docs-green.svg?style=flat)](https://hexdocs.pm/websock_adapter)
[![Hex.pm](https://img.shields.io/hexpm/v/websock_adapter.svg?style=flat&color=blue)](https://hex.pm/packages/websock_adapter)
WebSockAdapter is a library of adapters from common Web Servers to the
`WebSock` specification. WebSockAdapter currently supports
[Bandit](https://github.com/mtrudel/bandit) and
[Cowboy](https://github.com/ninenines/cowboy).
For details on the `WebSock` specification, consult the
[WebSock](https://hexdocs.pm/websock) documentation.
## Usage
WebSockAdapter makes it easy to upgrade Plug connections to WebSock connections.
Here's a simple example:
```elixir
defmodule EchoServer do
def init(args) do
{:ok, []}
end
def handle_in({"ping", [opcode: :text]}, state) do
{:reply, :ok, {:text, "pong"}, state}
end
end
defmodule MyPlug do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
get "/" do
# Provide the user with some useful instructions to copy & paste into their inspector
send_resp(conn, 200, """
Use the JavaScript console to interact using websockets
sock = new WebSocket("ws://localhost:4000/websocket")
sock.addEventListener("message", console.log)
sock.addEventListener("open", () => sock.send("ping"))
""")
end
get "/websocket" do
conn
|> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000)
|> halt()
end
match _ do
send_resp(conn, 404, "not found")
end
end
```
This simple example illustrates many of the useful features of WebSock / WebSockAdapters:
* Implementing a WebSocket server is a single module, and looks & acts much like
a GenServer does
* It's easy to pass state from the `WebSockAdapter.upgrade/3`
call & have it show up in your WebSock callbacks
* Upgrades are handled as a plain Plug call. You are able to route requests to
your upgrade endpoint using all of the power of the Plug API
If you're looking for more detail, Benjamin Milde has a [great blog
post](https://kobrakai.de/kolumne/bare-websockets) that goes a bit deeper than
the simple example above.
## Upgrade Validation
Since `0.5.5`, WebSockAdapter validates requests made via
`WebSockAdapter.upgrade/3` (this was and continues to also be done by the
underlying web server, but since the server's validation occurs after the
`Plug.call/2` lifecycle completes it's difficult to meaningfully handle such
errors). This validation examines the request for conformance to the clauses
laid out in RFC6455§4.2, as well as RFC8441§5 for HTTP/2 connections. Requests
which do not satisfy the requirements laid out in those specifications will
result in a `WebSockAdapter.UpgradeError` being raised, containing
details of the reason for the failure
## Installation
The websock_adapter package can be installed by adding `websock_adapter` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:websock_adapter, "~> 0.5"}
]
end
```
Documentation can be found at <https://hexdocs.pm/websock_adapter>.
## License
MIT

View File

@@ -0,0 +1,36 @@
{<<"links">>,
[{<<"GitHub">>,<<"https://github.com/phoenixframework/websock_adapter">>}]}.
{<<"name">>,<<"websock_adapter">>}.
{<<"version">>,<<"0.5.9">>}.
{<<"description">>,<<"A set of WebSock adapters for common web servers">>}.
{<<"elixir">>,<<"~> 1.9">>}.
{<<"app">>,<<"websock_adapter">>}.
{<<"files">>,
[<<"lib">>,<<"lib/websock_adapter.ex">>,<<"lib/websock_adapter">>,
<<"lib/websock_adapter/upgrade_error.ex">>,
<<"lib/websock_adapter/upgrade_validation.ex">>,
<<"lib/websock_adapter/cowboy_adapter.ex">>,<<"mix.exs">>,<<"README.md">>,
<<"LICENSE">>,<<"CHANGELOG.md">>]}.
{<<"licenses">>,[<<"MIT">>]}.
{<<"requirements">>,
[[{<<"name">>,<<"websock">>},
{<<"app">>,<<"websock">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 0.5">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"plug">>},
{<<"app">>,<<"plug">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.14">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"bandit">>},
{<<"app">>,<<"bandit">>},
{<<"optional">>,true},
{<<"requirement">>,<<">= 0.6.0">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"plug_cowboy">>},
{<<"app">>,<<"plug_cowboy">>},
{<<"optional">>,true},
{<<"requirement">>,<<"~> 2.6">>},
{<<"repository">>,<<"hexpm">>}]]}.
{<<"build_tools">>,[<<"mix">>]}.

View 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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
defmodule WebSockAdapter.UpgradeError do
defexception [:message]
end

View File

@@ -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

View File

@@ -0,0 +1,47 @@
defmodule WebSockAdapter.MixProject do
use Mix.Project
def project do
[
app: :websock_adapter,
version: "0.5.9",
elixir: "~> 1.9",
start_permanent: Mix.env() == :prod,
deps: deps(),
dialyzer: dialyzer(),
name: "WebSockAdapter",
description: "A set of WebSock adapters for common web servers",
source_url: "https://github.com/phoenixframework/websock_adapter",
package: [
files: ["lib", "mix.exs", "README*", "LICENSE*", "CHANGELOG*"],
maintainers: ["Mat Trudel"],
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/phoenixframework/websock_adapter"}
]
]
end
def application do
[]
end
defp deps do
[
{:websock, "~> 0.5"},
{:plug, "~> 1.14"},
{:bandit, ">= 0.6.0", optional: true},
{:plug_cowboy, "~> 2.6", optional: true},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false},
{:credo, "~> 1.0", only: [:dev, :test], runtime: false}
]
end
defp dialyzer do
[
plt_add_apps: [:cowboy],
plt_core_path: "priv/plts",
plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
]
end
end