Current section
Files
Jump to
Current section
Files
src/ws_deflate.erl
%% @doc RFC 7692 — permessage-deflate extension helpers.
%%
%% Provides negotiation (parse client offer / build server response),
%% and the inflate / deflate primitives used on the hot path once the
%% extension is enabled. The session does not yet wire this in
%% automatically — embedders enabling permessage-deflate should drive
%% it from their upgrade helper and pass the resulting zlib streams
%% into `encode_payload/3' / `decode_payload/3'.
%%
%% The algorithm follows the one in hackney_ws_proto / cowlib: deflate
%% with `sync' flush, strip the trailing `0x00 0x00 0xff 0xff' block on
%% the wire, and re-append it before calling inflate.
-module(ws_deflate).
-export([negotiate_server/2]).
-export([client_offer/1]).
-export([parse_server_response/1]).
-export([init_inflate/2, init_deflate/2]).
-export([inflate/3, deflate/3]).
-type opts() :: #{
server_context_takeover => takeover | no_takeover,
client_context_takeover => takeover | no_takeover,
server_max_window_bits => 8..15,
client_max_window_bits => 8..15,
level => zlib:zlevel(),
mem_level => zlib:zmemlevel(),
strategy => zlib:zstrategy()
}.
-type params() :: [{binary(), binary()} | binary()].
-type negotiated() :: #{
server_context_takeover := takeover | no_takeover,
client_context_takeover := takeover | no_takeover,
server_max_window_bits := 8..15,
client_max_window_bits := 8..15
}.
-export_type([opts/0, params/0, negotiated/0]).
%% ---------------------------------------------------------------------
%% Server-side negotiation. Takes the parsed offer params from the
%% client's `Sec-WebSocket-Extensions' and server policy in Opts.
%% Returns either `ignore' (cannot agree) or
%% `{ok, ResponseHeader :: iolist(), negotiated()}`.
-spec negotiate_server(params(), opts()) ->
ignore | {ok, iolist(), negotiated()}.
negotiate_server(Params, Opts) ->
Dedup = lists:usort(Params),
case length(Dedup) =:= length(Params) of
false -> ignore;
true -> negotiate1(Dedup, Opts)
end.
negotiate1(Params, Opts) ->
ServerCTO = maps:get(server_context_takeover, Opts, takeover),
ClientCTO = maps:get(client_context_takeover, Opts, takeover),
ServerWB = maps:get(server_max_window_bits, Opts, 15),
ClientWB = maps:get(client_max_window_bits, Opts, 15),
Negotiated0 = #{
server_context_takeover => ServerCTO,
client_context_takeover => ClientCTO,
server_max_window_bits => ServerWB,
client_max_window_bits => ClientWB
},
Resp0 = case ServerCTO of takeover -> []; no_takeover ->
[<<"; server_no_context_takeover">>] end,
Resp1 = case ClientCTO of takeover -> Resp0; no_takeover ->
[<<"; client_no_context_takeover">> | Resp0] end,
case walk_params(Params, Negotiated0, Resp1, ServerWB, ClientWB) of
ignore -> ignore;
{Negotiated, Resp} ->
Resp2 = case maps:is_key(server_max_window_bits_set, Negotiated) of
true -> Resp;
false when ServerWB =:= 15 -> Resp;
false -> [<<"; server_max_window_bits=">>,
integer_to_binary(ServerWB) | Resp]
end,
{ok, [<<"permessage-deflate">> | Resp2],
maps:without([client_max_window_bits_set,
server_max_window_bits_set], Negotiated)}
end.
walk_params([], Neg, Resp, _SWB, _CWB) ->
{Neg, Resp};
walk_params([<<"client_max_window_bits">> | T], Neg, Resp, SWB, CWB) ->
walk_params(T, Neg#{client_max_window_bits_set => true},
[<<"; client_max_window_bits=">>,
integer_to_binary(maps:get(client_max_window_bits, Neg))
| Resp], SWB, CWB);
walk_params([{<<"client_max_window_bits">>, V} | T], Neg, Resp, SWB, CWB) ->
case parse_window_bits(V) of
error -> ignore;
Bits when Bits =< CWB ->
walk_params(T, Neg#{client_max_window_bits => Bits,
client_max_window_bits_set => true},
[<<"; client_max_window_bits=">>, V | Resp], SWB, CWB);
_ ->
walk_params(T, Neg#{client_max_window_bits_set => true},
[<<"; client_max_window_bits=">>,
integer_to_binary(CWB) | Resp], SWB, CWB)
end;
walk_params([{<<"server_max_window_bits">>, V} | T], Neg, Resp, SWB, CWB) ->
case parse_window_bits(V) of
error -> ignore;
Bits when Bits =< SWB ->
walk_params(T, Neg#{server_max_window_bits => Bits,
server_max_window_bits_set => true},
[<<"; server_max_window_bits=">>, V | Resp], SWB, CWB);
_ -> ignore
end;
walk_params([<<"client_no_context_takeover">> | T], Neg, Resp, SWB, CWB) ->
walk_params(T, Neg#{client_context_takeover => no_takeover}, Resp, SWB, CWB);
walk_params([<<"server_no_context_takeover">> | T], Neg, Resp, SWB, CWB) ->
walk_params(T, Neg#{server_context_takeover => no_takeover}, Resp, SWB, CWB);
walk_params(_, _, _, _, _) ->
ignore.
parse_window_bits(Bin) ->
try binary_to_integer(Bin) of
N when N >= 8, N =< 15 -> N;
_ -> error
catch _:_ -> error
end.
%% ---------------------------------------------------------------------
%% Client side.
-spec client_offer(opts()) -> iodata().
client_offer(Opts) ->
Base = [<<"permessage-deflate">>],
C1 = case maps:get(client_context_takeover, Opts, takeover) of
takeover -> Base;
no_takeover -> Base ++ [<<"; client_no_context_takeover">>]
end,
C2 = case maps:get(server_context_takeover, Opts, takeover) of
takeover -> C1;
no_takeover -> C1 ++ [<<"; server_no_context_takeover">>]
end,
C3 = case maps:find(client_max_window_bits, Opts) of
error -> C2 ++ [<<"; client_max_window_bits">>];
{ok, Bits} -> C2 ++ [<<"; client_max_window_bits=">>,
integer_to_binary(Bits)]
end,
case maps:find(server_max_window_bits, Opts) of
error -> C3;
{ok, SBits} -> C3 ++ [<<"; server_max_window_bits=">>,
integer_to_binary(SBits)]
end.
-spec parse_server_response(params()) ->
{ok, negotiated()} | {error, term()}.
parse_server_response(Params) ->
walk_response(Params, #{
server_context_takeover => takeover,
client_context_takeover => takeover,
server_max_window_bits => 15,
client_max_window_bits => 15
}).
walk_response([], N) -> {ok, N};
walk_response([{<<"client_max_window_bits">>, V} | T], N) ->
case parse_window_bits(V) of
error -> {error, bad_client_max_window_bits};
B -> walk_response(T, N#{client_max_window_bits => B})
end;
walk_response([<<"client_no_context_takeover">> | T], N) ->
walk_response(T, N#{client_context_takeover => no_takeover});
walk_response([{<<"server_max_window_bits">>, V} | T], N) ->
case parse_window_bits(V) of
error -> {error, bad_server_max_window_bits};
B -> walk_response(T, N#{server_max_window_bits => B})
end;
walk_response([<<"server_no_context_takeover">> | T], N) ->
walk_response(T, N#{server_context_takeover => no_takeover});
walk_response(_, _) ->
{error, unknown_param}.
%% ---------------------------------------------------------------------
%% zlib stream setup.
-spec init_inflate(negotiated(), client | server) -> zlib:zstream().
init_inflate(#{client_max_window_bits := CBits, server_max_window_bits := SBits}, Role) ->
Bits = case Role of client -> SBits; server -> CBits end,
Z = zlib:open(),
ok = zlib:inflateInit(Z, -Bits),
Z.
-spec init_deflate(negotiated(), client | server) -> zlib:zstream().
init_deflate(#{client_max_window_bits := CBits, server_max_window_bits := SBits}, Role) ->
Bits = case Role of client -> CBits; server -> SBits end,
Z = zlib:open(),
ok = zlib:deflateInit(Z, default, deflated, -Bits, 8, default),
Z.
%% ---------------------------------------------------------------------
%% Codec.
-spec inflate(zlib:zstream(), takeover | no_takeover, iodata()) -> binary().
inflate(Z, Takeover, Data) ->
%% Re-append the trailing empty block stripped on the wire.
Out = iolist_to_binary(zlib:inflate(Z, [Data, <<0, 0, 255, 255>>])),
case Takeover of
no_takeover -> ok = zlib:inflateReset(Z);
takeover -> ok
end,
Out.
-spec deflate(zlib:zstream(), takeover | no_takeover, iodata()) -> binary().
deflate(Z, Takeover, Data) ->
Deflated = iolist_to_binary(zlib:deflate(Z, Data, sync)),
case Takeover of
no_takeover -> ok = zlib:deflateReset(Z);
takeover -> ok
end,
Len = byte_size(Deflated) - 4,
case Deflated of
<<Body:Len/binary, 0:8, 0:8, 255:8, 255:8>> -> Body;
<<>> -> <<0>>;
_ -> Deflated
end.