Skip to content

Commit

Permalink
Websocket: Allow setting the max_frame_size option dynamically
Browse files Browse the repository at this point in the history
This can be used to limit the maximum frame size before
some authentication or other validation is completed.
  • Loading branch information
essen committed Jan 16, 2025
1 parent 818b448 commit 81de580
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 14 deletions.
9 changes: 6 additions & 3 deletions doc/src/manual/cowboy_websocket.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ commands() :: [Command]
Command :: {active, boolean()}
| {deflate, boolean()}
| {set_options, #{idle_timeout => timeout()}}
| {set_options, #{
idle_timeout => timeout(),
max_frame_size => non_neg_integer() | infinity}}
| {shutdown_reason, any()}
| Frame :: cow_ws:frame()
----
Expand All @@ -159,8 +161,8 @@ effect on connections that did not negotiate compression.

set_options::

Set Websocket options. Currently only the option `idle_timeout`
may be updated from a Websocket handler.
Set Websocket options. Currently only the options `idle_timeout`
and `max_frame_size` may be updated from a Websocket handler.

shutdown_reason::

Expand Down Expand Up @@ -285,6 +287,7 @@ normal circumstances if necessary.

== Changelog

* *2.13*: The `max_frame_size` option can now be set dynamically.
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: HTTP/1.1 Websocket no longer traps exits by default.
* *2.8*: The `active_n` option was added.
Expand Down
16 changes: 9 additions & 7 deletions src/cowboy_websocket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -615,14 +615,16 @@ commands([{active, Active}|Tail], State0=#state{active=Active0}, Data) when is_b
commands(Tail, State#state{active=Active}, Data);
commands([{deflate, Deflate}|Tail], State, Data) when is_boolean(Deflate) ->
commands(Tail, State#state{deflate=Deflate}, Data);
commands([{set_options, SetOpts}|Tail], State0=#state{opts=Opts}, Data) ->
State = case SetOpts of
#{idle_timeout := IdleTimeout} ->
commands([{set_options, SetOpts}|Tail], State0, Data) ->
State = maps:fold(fun
(idle_timeout, IdleTimeout, StateF=#state{opts=Opts}) ->
%% We reset the number of ticks when changing the idle_timeout option.
set_idle_timeout(State0#state{opts=Opts#{idle_timeout => IdleTimeout}}, 0);
_ ->
State0
end,
set_idle_timeout(StateF#state{opts=Opts#{idle_timeout => IdleTimeout}}, 0);
(max_frame_size, MaxFrameSize, StateF=#state{opts=Opts}) ->
StateF#state{opts=Opts#{max_frame_size => MaxFrameSize}};
(_, _, StateF) ->
StateF
end, State0, SetOpts),
commands(Tail, State, Data);
commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) ->
commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data);
Expand Down
19 changes: 15 additions & 4 deletions test/handlers/ws_set_options_commands_h.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ init(Req, RunOrHibernate) ->
{cowboy_websocket, Req, RunOrHibernate,
#{idle_timeout => infinity}}.

websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=run) ->
{[{set_options, #{idle_timeout => 500}}, Frame], State};
websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=hibernate) ->
{[{set_options, #{idle_timeout => 500}}, Frame], State, hibernate}.
%% Set the idle_timeout option dynamically.
websocket_handle({text, <<"idle_timeout_short">>}, State=run) ->
{[{set_options, #{idle_timeout => 500}}], State};
websocket_handle({text, <<"idle_timeout_short">>}, State=hibernate) ->
{[{set_options, #{idle_timeout => 500}}], State, hibernate};
%% Set the max_frame_size option dynamically.
websocket_handle({text, <<"max_frame_size_small">>}, State=run) ->
{[{set_options, #{max_frame_size => 1000}}], State};
websocket_handle({text, <<"max_frame_size_small">>}, State=hibernate) ->
{[{set_options, #{max_frame_size => 1000}}], State, hibernate};
%% We just echo binary frames.
websocket_handle(Frame={binary, _}, State=run) ->
{[Frame], State};
websocket_handle(Frame={binary, _}, State=hibernate) ->
{[Frame], State, hibernate}.

websocket_info(_Info, State) ->
{[], State}.
35 changes: 35 additions & 0 deletions test/ws_handler_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,41 @@ websocket_set_options_idle_timeout(Config) ->
error(timeout)
end.

websocket_set_options_max_frame_size(Config) ->
doc("The max_frame_size option can be modified using the "
"command {set_options, Opts} at runtime."),
ConnPid = gun_open(Config),
StreamRef = gun:ws_upgrade(ConnPid, "/set_options"),
receive
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
ok;
{gun_response, ConnPid, _, _, Status, Headers} ->
exit({ws_upgrade_failed, Status, Headers});
{gun_error, ConnPid, StreamRef, Reason} ->
exit({ws_upgrade_failed, Reason})
after 1000 ->
error(timeout)
end,
%% We first send a 1MB frame to confirm that yes, we can
%% send a frame that large. The default max_frame_size is infinity.
gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000000>>}),
{ws, {binary, <<0:8000000>>}} = gun:await(ConnPid, StreamRef),
%% Trigger the change in max_frame_size. From now on we will
%% only allow frames of up to 1000 bytes.
gun:ws_send(ConnPid, StreamRef, {text, <<"max_frame_size_small">>}),
%% Confirm that we can send frames of up to 1000 bytes.
gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000>>}),
{ws, {binary, <<0:8000>>}} = gun:await(ConnPid, StreamRef),
%% Confirm that sending frames larger than 1000 bytes
%% results in the closing of the connection.
gun:ws_send(ConnPid, StreamRef, {binary, <<0:8008>>}),
receive
{gun_down, ConnPid, _, _, _} ->
ok
after 2000 ->
error(timeout)
end.

websocket_shutdown_reason(Config) ->
doc("The command {shutdown_reason, any()} can be used to "
"change the shutdown reason of a Websocket connection."),
Expand Down

0 comments on commit 81de580

Please sign in to comment.