Skip to content

Commit 7ba5dec

Browse files
implement extensions and permessage-deflate (#12)
* include all tests in the autobahn suite * add extension parsing utilities to utils * delegate to extensions when encoding/decoding frames * refactor extensions to list, attach them in upgrade/4 * enable compression in the h2 cowboy server * pass extensions to autobahn client, hanlde :done msg * setup permessage-deflate ask for autobahn suite * add permessage-deflate argument to web_socket_test * basics of an extension behaviour and struct * draft implementation of permessage-deflate * add note about only data frames for permessage-deflate * fix defaulting behavior in Frame.encode_close/2 * fix param parsing in per_message_deflate * use a single recursive function for extension.encode and decode * use throw/catch for encoding this makes encoding line up with decoding when it comes to control-flow I think throw/catch is generally discouraged, but it simplifies "pipelining" the encoding/decoding process because we don't need to write any functions that simply carry error tuples to the next function, which in my opinion is even worse readability throw/catch allows us to write our encode/decode functions on the happy path and throw (ha) our hands up in the air when the spec is violated
1 parent 16d0bee commit 7ba5dec

10 files changed

Lines changed: 665 additions & 109 deletions

File tree

autobahn/config/fuzzingserver.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
"url": "ws://0.0.0.0:9001",
33
"outdir": "./reports",
44
"cases": ["*"],
5-
"exclude-cases": [
6-
"12.*",
7-
"13.*"
8-
],
5+
"exclude-cases": [],
96
"exclude-agent-cases": {}
107
}

lib/mint/web_socket.ex

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,17 @@ defmodule Mint.WebSocket do
44
"""
55

66
require __MODULE__.Frame, as: Frame
7-
alias __MODULE__.Utils
7+
alias __MODULE__.{Utils, Extension}
88
alias Mint.WebSocketError
99

1010
@type t :: %__MODULE__{}
11-
defstruct extensions: MapSet.new(),
11+
defstruct extensions: [],
1212
fragments: [],
1313
private: %{},
1414
buffer: <<>>
1515

1616
@type error :: Mint.Types.error() | WebSocketError.t()
1717

18-
defguardp is_frame(frame)
19-
when frame in [:ping, :pong, :close] or
20-
(is_tuple(frame) and elem(frame, 0) in [:text, :binary, :ping, :pong] and
21-
is_binary(elem(frame, 1))) or
22-
(is_tuple(frame) and elem(frame, 0) == :close and is_integer(elem(frame, 1)) and
23-
is_binary(elem(frame, 2)))
24-
2518
@type frame ::
2619
{:text, binary()}
2720
| {:binary, binary()}
@@ -52,10 +45,16 @@ defmodule Mint.WebSocket do
5245
) :: {:ok, Mint.HTTP.t(), Mint.Types.request_ref()} | {:error, Mint.HTTP.t(), error()}
5346
def upgrade(conn, path, headers, opts \\ [])
5447

55-
def upgrade(%Mint.HTTP1{} = conn, path, headers, _opts) do
48+
def upgrade(%Mint.HTTP1{} = conn, path, headers, opts) do
5649
nonce = Utils.random_nonce()
57-
headers = Utils.headers({:http1, nonce}) ++ headers
58-
conn = put_in(conn.private[:sec_websocket_key], nonce)
50+
extensions = get_extensions(opts)
51+
52+
conn =
53+
conn
54+
|> put_in([Access.key(:private), :sec_websocket_key], nonce)
55+
|> put_in([Access.key(:private), :extensions], extensions)
56+
57+
headers = Utils.headers({:http1, nonce}, extensions) ++ headers
5958

6059
Mint.HTTP.request(conn, "GET", path, headers, nil)
6160
end
@@ -64,15 +63,18 @@ defmodule Mint.WebSocket do
6463
%Mint.HTTP2{server_settings: %{enable_connect_protocol: true}} = conn,
6564
path,
6665
headers,
67-
_opts
66+
opts
6867
) do
68+
extensions = get_extensions(opts)
69+
conn = put_in(conn.private[:extensions], extensions)
70+
6971
headers =
7072
[
7173
{":scheme", conn.scheme},
7274
{":path", path},
7375
{":protocol", "websocket"}
7476
| headers
75-
] ++ Utils.headers(:http2)
77+
] ++ Utils.headers(:http2, extensions)
7678

7779
Mint.HTTP.request(conn, "CONNECT", path, headers, :stream)
7880
end
@@ -89,31 +91,31 @@ defmodule Mint.WebSocket do
8991
end
9092

9193
def new(%Mint.HTTP1{} = conn, request_ref, _status, response_headers) do
92-
with :ok <- Utils.check_accept_nonce(conn.private[:sec_websocket_key], response_headers) do
94+
with :ok <- Utils.check_accept_nonce(conn.private[:sec_websocket_key], response_headers),
95+
{:ok, extensions} <-
96+
Extension.accept_extensions(conn.private.extensions, response_headers) do
9397
conn = re_open_request(conn, request_ref)
9498

95-
{:ok, conn, %__MODULE__{}}
99+
{:ok, conn, %__MODULE__{extensions: extensions}}
96100
else
97101
{:error, reason} -> {:error, conn, reason}
98102
end
99103
end
100104

101-
def new(%Mint.HTTP2{} = conn, _request_ref, status, _response_headers)
105+
def new(%Mint.HTTP2{} = conn, _request_ref, status, response_headers)
102106
when status in 200..299 do
103-
{:ok, conn, %__MODULE__{}}
107+
with {:ok, extensions} <-
108+
Extension.accept_extensions(conn.private.extensions, response_headers) do
109+
{:ok, conn, %__MODULE__{extensions: extensions}}
110+
end
104111
end
105112

106113
def new(%Mint.HTTP2{} = conn, _request_ref, _status, _response_headers) do
107114
{:error, conn, %WebSocketError{reason: :connection_not_upgraded}}
108115
end
109116

110117
@spec encode(t(), frame()) :: {:ok, t(), binary()} | {:error, t(), any()}
111-
def encode(%__MODULE__{} = websocket, frame) when is_frame(frame) do
112-
case frame |> Frame.translate() |> Frame.encode() do
113-
{:ok, encoded} -> {:ok, websocket, encoded}
114-
{:error, reason} -> {:error, websocket, reason}
115-
end
116-
end
118+
defdelegate encode(websocket, data), to: Frame
117119

118120
@spec decode(t(), data :: binary()) :: {:ok, t(), [frame()]} | {:error, t(), any()}
119121
defdelegate decode(websocket, data), to: Frame
@@ -147,4 +149,37 @@ defmodule Mint.WebSocket do
147149
body: nil
148150
}
149151
end
152+
153+
defp get_extensions(opts) do
154+
opts
155+
|> Keyword.get(:extensions, [])
156+
|> Enum.map(fn
157+
module when is_atom(module) ->
158+
%Extension{module: module, name: module.name()}
159+
160+
{module, params} ->
161+
%Extension{module: module, name: module.name(), params: normalize_params(params)}
162+
163+
{module, params, opts} ->
164+
%Extension{
165+
module: module,
166+
name: module.name(),
167+
params: normalize_params(params),
168+
opts: opts
169+
}
170+
171+
%Extension{} = extension ->
172+
update_in(extension.params, &normalize_params/1)
173+
end)
174+
end
175+
176+
defp normalize_params(params) do
177+
params
178+
|> Enum.map(fn
179+
{_key, false} -> nil
180+
{key, value} -> {to_string(key), to_string(value)}
181+
key -> {to_string(key), "true"}
182+
end)
183+
|> Enum.reject(&is_nil/1)
184+
end
150185
end

0 commit comments

Comments
 (0)