defmodule Mint.WebSocket do
@moduledoc """
HTTP/1 and HTTP/2 WebSocket support for the Mint functional HTTP client
Like Mint, `Mint.WebSocket` provides a functional, process-less interface
for operating a WebSocket connection. Prospective Mint.WebSocket users
may wish to first familiarize themselves with `Mint.HTTP`.
Mint.WebSocket is not fully spec-conformant on its own. Runtime behaviors
such as responding to pings with pongs must be implemented by the user of
Mint.WebSocket.
## Usage
A connection formed with `Mint.HTTP.connect/4` can be upgraded to a WebSocket
connection with `upgrade/5`.
```elixir
{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
{:ok, conn, ref} = Mint.WebSocket.upgrade(:ws, conn, "/", [])
```
`upgrade/5` sends an upgrade request to the remote server. The WebSocket
connection is then built by awaiting the HTTP response from the server.
```elixir
http_reply_message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
Mint.WebSocket.stream(conn, http_reply_message)
{:ok, conn, websocket} =
Mint.WebSocket.new(conn, ref, status, resp_headers)
```
Once the WebSocket connection has been established, use the `websocket`
data structure to encode and decode frames with `encode/2` and `decode/2`,
and send and stream messages with `stream_request_body/3` and `stream/2`.
For example, one may send a "hello world" text frame across a connection
like so:
```elixir
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)
```
Say that the remote is echoing messages. Use `stream/2` and `decode/2` to
decode a received WebSocket frame:
```elixir
echo_message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, echo_message)
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
```
## HTTP/2 Support
Mint.WebSocket supports WebSockets over HTTP/2 as defined in rfc8441.
rfc8441 is an extension to the HTTP/2 specification. At the time of
writing, very few HTTP/2 server libraries support or enable HTTP/2
WebSockets by default.
`upgrade/5` works on both HTTP/1 and HTTP/2 connections. In order to select
HTTP/2, the `:http2` protocol should be explicitly selected in
`Mint.HTTP.connect/4`.
```elixir
{:ok, conn} =
Mint.HTTP.connect(:http, "websocket.example", 80, protocols: [:http2])
:http2 = Mint.HTTP.protocol(conn)
{:ok, conn, ref} = Mint.WebSocket.upgrade(:ws, conn, "/", [])
```
If the server does not support the extended CONNECT method needed to bootstrap
WebSocket connections over HTTP/2, `upgrade/4` will return an error tuple
with the `:extended_connect_disabled` error reason.
```elixir
{:error, conn, %Mint.WebSocketError{reason: :extended_connect_disabled}}
```
Why use HTTP/2 for WebSocket connections in the first place? HTTP/2
can multiplex many requests over the same connection, which can
reduce the latency incurred by forming new connections for each request.
A WebSocket connection only occupies one stream of a HTTP/2 connection, so
even if an HTTP/2 connection has an open WebSocket communication, it can be
used to transport more requests.
## WebSocket Secure
Encryption of connections is handled by Mint functions. To start a WSS
connection, select `:https` as the scheme in `Mint.HTTP.connect/4`:
```elixir
{:ok, conn} = Mint.HTTP.connect(:https, "websocket.example", 443)
```
And pass the `:wss` scheme to `upgrade/5`. See the Mint documentation
on SSL for more information.
## Extensions
The WebSocket protocol allows for _extensions_. Extensions act as a
middleware for encoding and decoding frames. For example "permessage-deflate"
compresses and decompresses the body of data frames, which minifies the amount
of bytes which must be sent over the network.
See `Mint.WebSocket.Extension` for more information about extensions and
`Mint.WebSocket.PerMessageDeflate` for information about the
"permessage-deflate" extension.
"""
alias __MODULE__.{Utils, Extension, Frame}
alias Mint.{WebSocketError, WebSocket.UpgradeFailureError}
import Mint.HTTP, only: [get_private: 2, put_private: 3, protocol: 1]
@typedoc """
An immutable data structure representing a WebSocket connection
"""
@opaque t :: %__MODULE__{
extensions: [Extension.t()],
fragment: tuple(),
private: map(),
buffer: binary()
}
defstruct extensions: [],
fragment: nil,
private: %{},
buffer: <<>>
@type error :: Mint.Types.error() | WebSocketError.t() | UpgradeFailureError.t()
@typedoc """
Shorthand notations for control frames
* `:ping` - shorthand for `{:ping, ""}`
* `:pong` - shorthand for `{:pong, ""}`
* `:close` - shorthand for `{:close, nil, nil}`
These may be passed to `encode/2`. Frames decoded with `decode/2` are always
in `t:frame/0` format.
"""
@type shorthand_frame :: :ping | :pong | :close
@typedoc """
A WebSocket frame
* `{:binary, binary}` - a frame containing binary data. Binary frames
can be used to send arbitrary binary data such as a PDF.
* `{:text, text}` - a frame containing string data. Text frames must be
valid utf8. Elixir has wonderful support for utf8: `String.valid?/1`
can detect valid and invalid utf8.
* `{:ping, binary}` - a control frame which the server should respond to
with a pong. The binary data must be echoed in the pong response.
* `{:pong, binary}` - a control frame which forms a reply to a ping frame.
Pings and pongs may be used to check the a connection is alive or to
estimate latency.
* `{:close, code, reason}` - a control frame used to request that a connection
be closed or to acknowledgee a close frame send by the server.
These may be passed to `encode/2` or returned from `decode/2`.
## Close frames
In order to close a WebSocket connection gracefully, either the client or
server sends a close frame. Then the other endpoint responds with a
close with code `1_000` and then closes the TCP connection. This can be
accomplished in Mint.WebSocket like so:
```elixir
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, :close)
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)
close_response = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, close_response)
{:ok, websocket, [{:close, 1_000, ""}]} = Mint.WebSocket.decode(websocket, data)
Mint.HTTP.close(conn)
```
[rfc6455
section 7.4.1](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1)
documents codes which may be used in the `code` element.
"""
@type frame ::
{:text, String.t()}
| {:binary, binary()}
| {:ping, binary()}
| {:pong, binary()}
| {:close, code :: non_neg_integer() | nil, reason :: binary() | nil}
@doc """
Requests that a connection be upgraded to the WebSocket protocol
This function wraps `Mint.HTTP.request/5` to provide a single interface
for bootstrapping an upgrade for HTTP/1 and HTTP/2 connections.
For HTTP/1 connections, this function performs a GET request with
WebSocket-specific headers. For HTTP/2 connections, this function performs
an extended CONNECT request which opens a stream to be used for the WebSocket
connection.
The `scheme` argument should be either `:ws` or `:wss`, using `:ws` for
connections established by passing `:http` to `Mint.HTTP.connect/4` and
`:wss` corresponding to `:https`.
## Options
* `:extensions` - a list of extensions to negotiate. See the extensions
section below.
## Extensions
Extensions should be declared by passing the `:extensions` option in the
`opts` keyword list. Note that in the WebSocket protocol, extensions are
negotiated: the client proposes a list of extensions and the server may
accept any (or none) of them. See `Mint.WebSocket.Extension` for more
information about extension negotiation.
Extensions may be passed as a list of `Mint.WebSocket.Extension` structs
or with the following shorthand notations:
* `module` - shorthand for `{module, []}`
* `{module, params}` - shorthand for `{module, params, []}`
* `{module, params, opts}` - a shorthand which is expanded to a
`Mint.WebSocket.Extension` struct
## Examples
```elixir
{:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
{:ok, conn, ref} =
Mint.WebSocket.upgrade(:ws, conn, "/", [], extensions: [Mint.WebSocket.PerMessageDeflate])
# or provide params:
{:ok, conn, ref} =
Mint.WebSocket.upgrade(
:ws,
conn,
"/",
[],
extensions: [{Mint.WebSocket.PerMessageDeflate, [:client_max_window_bits]]}]
)
```
"""
@spec upgrade(
scheme :: :ws | :wss,
conn :: Mint.HTTP.t(),
path :: String.t(),
headers :: Mint.Types.headers(),
opts :: Keyword.t()
) :: {:ok, Mint.HTTP.t(), Mint.Types.request_ref()} | {:error, Mint.HTTP.t(), error()}
def upgrade(scheme, conn, path, headers, opts \\ []) when scheme in ~w[ws wss]a do
conn = put_private(conn, :scheme, scheme)
do_upgrade(scheme, Mint.HTTP.protocol(conn), conn, path, headers, opts)
end
defp do_upgrade(_scheme, :http1, conn, path, headers, opts) do
nonce = Utils.random_nonce()
extensions = get_extensions(opts)
conn =
conn
|> put_private(:sec_websocket_key, nonce)
|> put_private(:extensions, extensions)
headers = Utils.headers({:http1, nonce}, extensions) ++ headers
Mint.HTTP.request(conn, "GET", path, headers, nil)
end
@dialyzer {:no_opaque, do_upgrade: 6}
defp do_upgrade(scheme, :http2, conn, path, headers, opts) do
if Mint.HTTP2.get_server_setting(conn, :enable_connect_protocol) == true do
extensions = get_extensions(opts)
conn = put_private(conn, :extensions, extensions)
headers =
[
{":scheme", if(scheme == :ws, do: "http", else: "https")},
{":path", path},
{":protocol", "websocket"}
| headers
] ++ Utils.headers(:http2, extensions)
Mint.HTTP2.request(conn, "CONNECT", path, headers, :stream)
else
{:error, conn, %WebSocketError{reason: :extended_connect_disabled}}
end
end
@doc """
Creates a new WebSocket data structure given the server's reply to the
upgrade request
This function will setup any extensions accepted by the server using
the `c:Mint.WebSocket.Extension.init/2` callback.
## Options
* `:mode` - (default: `:active`) either `:active` or `:passive`. This
corresponds to the same option in `Mint.HTTP.connect/4`.
## Examples
```elixir
http_reply = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, headers}, {:done, ^ref}]} =
Mint.WebSocket.stream(conn, http_reply)
{:ok, conn, websocket} =
Mint.WebSocket.new(conn, ref, status, resp_headers)
```
"""
@spec new(
Mint.HTTP.t(),
reference(),
Mint.Types.status(),
Mint.Types.headers()
) ::
{:ok, Mint.HTTP.t(), t()} | {:error, Mint.HTTP.t(), error()}
def new(conn, request_ref, status, response_headers, opts \\ []) do
websockets = [request_ref | get_private(conn, :websockets) || []]
conn =
conn
|> put_private(:websockets, websockets)
|> put_private(:mode, Keyword.get(opts, :mode, :active))
do_new(protocol(conn), conn, status, response_headers)
end
defp do_new(:http1, conn, status, headers) when status != 101 do
error = %UpgradeFailureError{status_code: status, headers: headers}
{:error, conn, error}
end
defp do_new(:http1, conn, _status, response_headers) do
with :ok <- Utils.check_accept_nonce(get_private(conn, :sec_websocket_key), response_headers),
{:ok, extensions} <-
Extension.accept_extensions(get_private(conn, :extensions), response_headers) do
{:ok, conn, %__MODULE__{extensions: extensions}}
else
{:error, reason} -> {:error, conn, reason}
end
end
defp do_new(:http2, conn, status, response_headers)
when status in 200..299 do
with {:ok, extensions} <-
Extension.accept_extensions(get_private(conn, :extensions), response_headers) do
{:ok, conn, %__MODULE__{extensions: extensions}}
end
end
defp do_new(:http2, conn, status, headers) do
error = %UpgradeFailureError{status_code: status, headers: headers}
{:error, conn, error}
end
@doc """
A wrapper around `Mint.HTTP.stream/2` for streaming HTTP and WebSocket
messages
This function does not decode WebSocket frames. Instead, once a WebSocket
connection has been established, decode any `{:data, request_ref, data}`
frames with `decode/2`.
This function is a drop-in replacement for `Mint.HTTP.stream/2` which
enables streaming WebSocket data after the bootstrapping HTTP/1 connection
has concluded. It decodes both WebSocket and regular HTTP messages.
## Examples
message = receive(do: (message -> message))
{:ok, conn, [{:data, ^websocket_ref, data}]} =
Mint.WebSocket.stream(conn, message)
{:ok, websocket, [{:text, "hello world!"}]} =
Mint.WebSocket.decode(websocket, data)
"""
@spec stream(Mint.HTTP.t(), term()) ::
{:ok, Mint.HTTP.t(), [Mint.Types.response()]}
| {:error, Mint.HTTP.t(), Mint.Types.error(), [Mint.Types.response()]}
| :unknown
def stream(conn, message) do
with :http1 <- protocol(conn),
# HTTP/1 only allows one WebSocket per connection
[request_ref] <- get_private(conn, :websockets) do
stream_http1(conn, request_ref, message)
else
_ -> Mint.HTTP.stream(conn, message)
end
end
# we take manual control of the :gen_tcp and :ssl messages in HTTP/1 because
# we have taken over the transport
defp stream_http1(conn, request_ref, message) do
socket = Mint.HTTP.get_socket(conn)
tag = if get_private(conn, :scheme) == :ws, do: :tcp, else: :ssl
case message do
{^tag, ^socket, data} ->
reset_mode(conn, [{:data, request_ref, data}])
_ ->
Mint.HTTP.stream(conn, message)
end
end
defp reset_mode(conn, responses) do
module = if get_private(conn, :scheme) == :ws, do: :inet, else: :ssl
with :active <- get_private(conn, :mode),
{:error, reason} <- module.setopts(Mint.HTTP.get_socket(conn), active: :once) do
{:error, conn, %Mint.TransportError{reason: reason}, responses}
else
_ -> {:ok, conn, responses}
end
end
@doc """
Receives data from the socket
This function is used instead of `stream/2` when the connection is
in `:passive` mode. You must pass the `mode: :passive` option to
`new/5` in order to use `recv/3`.
This function wraps `Mint.HTTP.recv/3`. See the `Mint.HTTP.recv/3`
documentation for more information.
## Examples
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.recv(conn, 0, 5_000)
{:ok, websocket, [{:text, "hello world!"}]} =
Mint.WebSocket.decode(websocket, data)
"""
@spec recv(Mint.HTTP.t(), non_neg_integer(), timeout()) ::
{:ok, Mint.HTTP.t(), [Mint.Types.response()]}
| {:error, t(), Mint.Types.error(), [Mint.Types.response()]}
def recv(conn, byte_count, timeout) do
with :http1 <- protocol(conn),
[request_ref] <- get_private(conn, :websockets) do
recv_http1(conn, request_ref, byte_count, timeout)
else
_ -> Mint.HTTP.recv(conn, byte_count, timeout)
end
end
defp recv_http1(conn, request_ref, byte_count, timeout) do
module = if get_private(conn, :scheme) == :ws, do: :gen_tcp, else: :ssl
socket = Mint.HTTP.get_socket(conn)
case module.recv(socket, byte_count, timeout) do
{:ok, data} ->
{:ok, conn, [{:data, request_ref, data}]}
{:error, error} ->
{:error, conn, error, []}
end
end
@doc """
Streams chunks of data on the connection
`stream_request_body/3` should be used to send encoded data on an
established WebSocket connection that has already been upgraded with
`upgrade/5`.
This function is a wrapper around `Mint.HTTP.stream_request_body/3`. It
delegates to that function unless the `request_ref` belongs to an HTTP/1
WebSocket connection. When the request is an HTTP/1 WebSocket, this
function allows sending data on a request which Mint considers to be
closed, but is actually a valid WebSocket connection.
See the `Mint.HTTP.stream_request_body/3` documentation for more
information.
## Examples
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world!"})
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, websocket_ref, data)
"""
@spec stream_request_body(
Mint.HTTP.t(),
Mint.Types.request_ref(),
iodata() | :eof | {:eof, trailing_headers :: Mint.Types.headers()}
) :: {:ok, Mint.HTTP.t()} | {:error, Mint.HTTP.t(), error()}
def stream_request_body(conn, request_ref, data) do
with :http1 <- protocol(conn),
[^request_ref] <- get_private(conn, :websockets),
data when is_binary(data) or is_list(data) <- data do
stream_request_body_http1(conn, data)
else
_ -> Mint.HTTP.stream_request_body(conn, request_ref, data)
end
end
defp stream_request_body_http1(conn, data) do
transport = if get_private(conn, :scheme) == :ws, do: :gen_tcp, else: :ssl
case transport.send(Mint.HTTP.get_socket(conn), data) do
:ok -> {:ok, conn}
{:error, reason} -> {:error, conn, %Mint.TransportError{reason: reason}}
end
end
@doc """
Encodes a frame into a binary
The resulting binary may be sent with `stream_request_body/3`.
This function will invoke the `c:Mint.WebSocket.Extension.encode/2` callback
for any accepted extensions.
## Examples
```elixir
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, websocket_ref, data)
```
"""
@spec encode(t(), shorthand_frame() | frame()) :: {:ok, t(), binary()} | {:error, t(), any()}
defdelegate encode(websocket, frame), to: Frame
@doc """
Decodes a binary into a list of frames
The binary may received from the connection with `Mint.HTTP.stream/2`.
This function will invoke the `c:Mint.WebSocket.Extension.decode/2` callback
for any accepted extensions.
## Examples
```elixir
message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
```
"""
@spec decode(t(), data :: binary()) ::
{:ok, t(), [frame() | {:error, term()}]} | {:error, t(), any()}
defdelegate decode(websocket, data), to: Frame
defp get_extensions(opts) do
opts
|> Keyword.get(:extensions, [])
|> Enum.map(fn
module when is_atom(module) ->
%Extension{module: module, name: module.name()}
{module, params} ->
%Extension{module: module, name: module.name(), params: normalize_params(params)}
{module, params, opts} ->
%Extension{
module: module,
name: module.name(),
params: normalize_params(params),
opts: opts
}
%Extension{} = extension ->
update_in(extension.params, &normalize_params/1)
end)
end
defp normalize_params(params) do
params
|> Enum.map(fn
{_key, false} -> nil
{key, value} -> {to_string(key), to_string(value)}
key -> {to_string(key), "true"}
end)
|> Enum.reject(&is_nil/1)
end
end