Skip to content

Commit 09d0a24

Browse files
committed
Websocket: Fix max_inflate_size calculation
1 parent 17b0e59 commit 09d0a24

3 files changed

Lines changed: 67 additions & 8 deletions

File tree

src/cowboy_websocket.erl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ websocket_handshake(State=#state{opts=Opts},
312312
mask_key :: cow_ws:mask_key(),
313313
rsv :: cow_ws:rsv(),
314314
close_code = undefined :: undefined | cow_ws:close_code(),
315-
unmasked = <<>> :: binary(),
315+
payload = <<>> :: binary(),
316316
unmasked_len = 0 :: non_neg_integer(),
317317
buffer = <<>> :: binary()
318318
}).
@@ -540,37 +540,37 @@ parse_header(State=#state{opts=Opts, frag_state=FragState, extensions=Extensions
540540
parse_payload(State=#state{opts=Opts, frag_state=FragState, utf8_state=Incomplete, extensions=Extensions},
541541
HandlerState, ParseState=#ps_payload{
542542
type=Type, len=Len, mask_key=MaskKey, rsv=Rsv,
543-
unmasked=Unmasked, unmasked_len=UnmaskedLen}, Data) ->
543+
payload=Payload0, unmasked_len=UnmaskedLen}, Data) ->
544544
MaxFrameSize = case maps:get(max_frame_size, Opts, ?MAX_FRAME_SIZE_DEFAULT) of
545545
infinity -> infinity;
546-
MaxFrameSize0 -> MaxFrameSize0 - UnmaskedLen
546+
MaxFrameSize0 -> MaxFrameSize0 - byte_size(Payload0)
547547
end,
548548
case cow_ws:parse_payload(Data, MaskKey, Incomplete, UnmaskedLen,
549549
Type, Len, FragState, Extensions#{max_inflate_size => MaxFrameSize}, Rsv) of
550550
{ok, CloseCode, Payload, Utf8State, Rest} ->
551551
dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState,
552-
ParseState#ps_payload{unmasked= <<Unmasked/binary, Payload/binary>>,
552+
ParseState#ps_payload{payload= <<Payload0/binary, Payload/binary>>,
553553
close_code=CloseCode}, Rest);
554554
{ok, Payload, Utf8State, Rest} ->
555555
dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState,
556-
ParseState#ps_payload{unmasked= <<Unmasked/binary, Payload/binary>>},
556+
ParseState#ps_payload{payload= <<Payload0/binary, Payload/binary>>},
557557
Rest);
558558
{more, CloseCode, Payload, Utf8State} ->
559559
before_loop(State#state{utf8_state=Utf8State}, HandlerState,
560560
ParseState#ps_payload{len=Len - byte_size(Data), close_code=CloseCode,
561-
unmasked= <<Unmasked/binary, Payload/binary>>,
561+
payload= <<Payload0/binary, Payload/binary>>,
562562
unmasked_len=UnmaskedLen + byte_size(Data)});
563563
{more, Payload, Utf8State} ->
564564
before_loop(State#state{utf8_state=Utf8State}, HandlerState,
565565
ParseState#ps_payload{len=Len - byte_size(Data),
566-
unmasked= <<Unmasked/binary, Payload/binary>>,
566+
payload= <<Payload0/binary, Payload/binary>>,
567567
unmasked_len=UnmaskedLen + byte_size(Data)});
568568
Error = {error, _Reason} ->
569569
websocket_close(State, HandlerState, Error)
570570
end.
571571

572572
dispatch_frame(State=#state{opts=Opts, frag_state=FragState, frag_buffer=SoFar}, HandlerState,
573-
#ps_payload{type=Type0, unmasked=Payload0, close_code=CloseCode0}, RemainingData) ->
573+
#ps_payload{type=Type0, payload=Payload0, close_code=CloseCode0}, RemainingData) ->
574574
MaxFrameSize = maps:get(max_frame_size, Opts, ?MAX_FRAME_SIZE_DEFAULT),
575575
case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of
576576
%% @todo Allow receiving fragments.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-module(ws_max_frame_size_deflate).
2+
3+
-export([init/2]).
4+
-export([websocket_handle/2]).
5+
-export([websocket_info/2]).
6+
7+
init(Req, State) ->
8+
{cowboy_websocket, Req, State, #{max_frame_size => 100, compress => true}}.
9+
10+
websocket_handle({text, Data}, State) ->
11+
{[{text, Data}], State};
12+
websocket_handle({binary, Data}, State) ->
13+
{[{binary, Data}], State};
14+
websocket_handle(_Frame, State) ->
15+
{[], State}.
16+
17+
websocket_info(_Info, State) ->
18+
{[], State}.

test/ws_SUITE.erl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ init_dispatch() ->
6767
{"/ws_timeout_hibernate", ws_timeout_hibernate, []},
6868
{"/ws_timeout_cancel", ws_timeout_cancel, []},
6969
{"/ws_max_frame_size", ws_max_frame_size, []},
70+
{"/ws_max_frame_size_deflate", ws_max_frame_size_deflate, []},
7071
{"/ws_deflate_opts", ws_deflate_opts_h, []},
7172
{"/ws_dont_validate_utf8", ws_dont_validate_utf8_h, []},
7273
{"/ws_ping", ws_ping_h, []}
@@ -222,6 +223,46 @@ ws_deflate_max_frame_size_close(Config) ->
222223
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
223224
ok.
224225

226+
ws_deflate_max_frame_size_chunked_close(Config) ->
227+
doc("Server closes connection when decompressed frame size exceeds "
228+
"max_frame_size option, even when the compressed payload is "
229+
"delivered over multiple separate TCP segments."),
230+
%% max_frame_size is set to 100 bytes in ws_max_frame_size_deflate.
231+
{ok, Socket, Headers} = do_handshake("/ws_max_frame_size_deflate",
232+
"Sec-WebSocket-Extensions: permessage-deflate\r\n", Config),
233+
{_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers),
234+
Mask = 16#11223344,
235+
Z = zlib:open(),
236+
zlib:deflateInit(Z, best_compression, deflated, -15, 8, default),
237+
%% Three independently sync-flushed chunks, each decompressing to
238+
%% exactly 50 bytes. After the first two (100 bytes, exactly at
239+
%% max_frame_size) the server must not close yet; after the third
240+
%% (150 bytes, over max_frame_size) it must close with code 1009,
241+
%% regardless of the fact that each individual chunk's own wire
242+
%% size (~8-10 bytes) never looks oversized by itself.
243+
[Chunk1, Chunk2, Chunk3] = [iolist_to_binary(zlib:deflate(Z, <<0:400>>, sync))
244+
|| _ <- lists:seq(1, 3)],
245+
Chunk1Size = byte_size(Chunk1),
246+
Chunk2Size = byte_size(Chunk2),
247+
CompressedData0 = iolist_to_binary([Chunk1, Chunk2, Chunk3]),
248+
CompressedData = binary:part(CompressedData0, 0, byte_size(CompressedData0) - 4),
249+
MaskedData = do_mask(CompressedData, Mask, <<>>),
250+
Len = byte_size(MaskedData),
251+
true = Len < 100,
252+
<< MaskedChunk1:Chunk1Size/binary, MaskedChunk2:Chunk2Size/binary,
253+
MaskedChunk3/binary >> = MaskedData,
254+
ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, Len:7, Mask:32 >>),
255+
ok = gen_tcp:send(Socket, MaskedChunk1),
256+
ok = gen_tcp:send(Socket, MaskedChunk2),
257+
%% 100 bytes decompressed so far: exactly at the limit, connection
258+
%% must still be open.
259+
{error, timeout} = gen_tcp:recv(Socket, 0, 200),
260+
ok = gen_tcp:send(Socket, MaskedChunk3),
261+
%% 150 bytes decompressed: over the limit, connection must close now.
262+
{ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000),
263+
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
264+
ok.
265+
225266
ws_deflate_opts_client_context_takeover(Config) ->
226267
doc("Handler is configured with client context takeover enabled."),
227268
{ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_context_takeover",

0 commit comments

Comments
 (0)