aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2013-01-10 21:58:38 +0100
committerLoïc Hoguin <[email protected]>2013-01-10 21:58:38 +0100
commit10e3692fa62ebc57cfe4f8677382119a40b55e53 (patch)
treeedd761586b2ac1cf08e3720a5fb79518ec51d033
parent9bfef7c715821ef25ca1511693a5eaaa4e4b394b (diff)
downloadcowboy-10e3692fa62ebc57cfe4f8677382119a40b55e53.tar.gz
cowboy-10e3692fa62ebc57cfe4f8677382119a40b55e53.tar.bz2
cowboy-10e3692fa62ebc57cfe4f8677382119a40b55e53.zip
Remove support for Websocket hixie76 draft
It was only used by Safari 5.0.1 and possibly 5.1. Their market share is dropping as we speak. It was also insecure (disabled in Firefox for that reason). This will allow us to make much more efficient and cleaner code for the rest of the Websocket versions we support (drafts 7 to 17 + RFC), which are pretty much all versions seen in the wild excluding the one we're removing here.
-rw-r--r--src/cowboy_websocket.erl162
-rw-r--r--test/ws_SUITE.erl81
2 files changed, 49 insertions, 194 deletions
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index 51fed54..bffffd8 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -12,7 +12,10 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-%% @doc WebSocket protocol implementation.
+%% @doc Websocket protocol implementation.
+%%
+%% Cowboy supports versions 7 through 17 of the Websocket drafts.
+%% It also supports RFC6455, the proposed standard for Websocket.
-module(cowboy_websocket).
%% API.
@@ -41,22 +44,19 @@
env :: cowboy_middleware:env(),
socket = undefined :: inet:socket(),
transport = undefined :: module(),
- version :: 0 | 7 | 8 | 13,
handler :: module(),
handler_opts :: any(),
- challenge = undefined :: undefined | binary() | {binary(), binary()},
+ key = undefined :: undefined | binary(),
timeout = infinity :: timeout(),
timeout_ref = undefined :: undefined | reference(),
messages = undefined :: undefined | {atom(), atom(), atom()},
hibernate = false :: boolean(),
- eop :: undefined | tuple(), %% hixie-76 specific.
- origin = undefined :: undefined | binary(), %% hixie-76 specific.
frag_state = undefined :: frag_state()
}).
-%% @doc Upgrade a HTTP request to the WebSocket protocol.
+%% @doc Upgrade an HTTP request to the Websocket protocol.
%%
-%% You do not need to call this function manually. To upgrade to the WebSocket
+%% You do not need to call this function manually. To upgrade to the Websocket
%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
%% in your <em>cowboy_http_handler:init/3</em> handler function.
-spec upgrade(Req, Env, module(), any())
@@ -84,36 +84,13 @@ websocket_upgrade(State, Req) ->
{ok, [<<"websocket">>], Req3}
= cowboy_req:parse_header(<<"upgrade">>, Req2),
{Version, Req4} = cowboy_req:header(<<"sec-websocket-version">>, Req3),
- websocket_upgrade(Version, State, Req4).
-
-%% @todo Handle the Sec-Websocket-Protocol header.
-%% @todo Reply a proper error, don't die, if a required header is undefined.
--spec websocket_upgrade(undefined | <<_:8>>, #state{}, Req)
- -> {ok, #state{}, Req} when Req::cowboy_req:req().
-%% No version given. Assuming hixie-76 draft.
-%%
-%% We need to wait to send a reply back before trying to read the
-%% third part of the challenge key, because proxies will wait for
-%% a reply before sending it. Therefore we calculate the challenge
-%% key only in websocket_handshake/3.
-websocket_upgrade(undefined, State, Req) ->
- {Origin, Req2} = cowboy_req:header(<<"origin">>, Req),
- {Key1, Req3} = cowboy_req:header(<<"sec-websocket-key1">>, Req2),
- {Key2, Req4} = cowboy_req:header(<<"sec-websocket-key2">>, Req3),
- false = lists:member(undefined, [Origin, Key1, Key2]),
- EOP = binary:compile_pattern(<< 255 >>),
- {ok, State#state{version=0, origin=Origin, challenge={Key1, Key2},
- eop=EOP}, cowboy_req:set_meta(websocket_version, 0, Req4)};
-%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts.
-websocket_upgrade(Version, State, Req)
- when Version =:= <<"7">>; Version =:= <<"8">>;
- Version =:= <<"13">> ->
- {Key, Req2} = cowboy_req:header(<<"sec-websocket-key">>, Req),
- false = Key =:= undefined,
- Challenge = hybi_challenge(Key),
IntVersion = list_to_integer(binary_to_list(Version)),
- {ok, State#state{version=IntVersion, challenge=Challenge},
- cowboy_req:set_meta(websocket_version, IntVersion, Req2)}.
+ true = (IntVersion =:= 7) orelse (IntVersion =:= 8)
+ orelse (IntVersion =:= 13),
+ {Key, Req5} = cowboy_req:header(<<"sec-websocket-key">>, Req4),
+ false = Key =:= undefined,
+ {ok, State#state{key=Key},
+ cowboy_req:set_meta(websocket_version, IntVersion, Req5)}.
-spec handler_init(#state{}, Req)
-> {ok, Req, cowboy_middleware:env()} | {error, 400, Req}
@@ -160,39 +137,10 @@ upgrade_error(Req, Env) ->
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-websocket_handshake(State=#state{socket=Socket, transport=Transport,
- version=0, origin=Origin, challenge={Key1, Key2}},
- Req, HandlerState) ->
- {<< "http", Location/binary >>, Req1} = cowboy_req:url(Req),
- {ok, Req2} = cowboy_req:upgrade_reply(
- <<"101 WebSocket Protocol Handshake">>,
- [{<<"upgrade">>, <<"WebSocket">>},
- {<<"sec-websocket-location">>, << "ws", Location/binary >>},
- {<<"sec-websocket-origin">>, Origin}],
- Req1),
- %% Flush the resp_sent message before moving on.
- receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- %% We replied with a proper response. Proxies should be happy enough,
- %% we can now read the 8 last bytes of the challenge keys and send
- %% the challenge response directly to the socket.
- %%
- %% We use a trick here to read exactly 8 bytes of the body regardless
- %% of what's in the buffer.
- {ok, Req3} = cowboy_req:init_stream(
- fun cowboy_http:te_identity/2, {0, 8},
- fun cowboy_http:ce_identity/1, Req2),
- case cowboy_req:body(Req3) of
- {ok, Key3, Req4} ->
- Challenge = hixie76_challenge(Key1, Key2, Key3),
- Transport:send(Socket, Challenge),
- handler_before_loop(State#state{messages=Transport:messages()},
- Req4, HandlerState, <<>>);
- _Any ->
- %% If an error happened reading the body, stop there.
- handler_terminate(State, Req3, HandlerState, {error, closed})
- end;
-websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
+websocket_handshake(State=#state{transport=Transport, key=Key},
Req, HandlerState) ->
+ Challenge = base64:encode(crypto:sha(
+ << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)),
{ok, Req2} = cowboy_req:upgrade_reply(
101,
[{<<"upgrade">>, <<"websocket">>},
@@ -201,8 +149,8 @@ websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
%% Flush the resp_sent message before moving on.
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
State2 = handler_loop_timeout(State),
- handler_before_loop(State2#state{messages=Transport:messages()},
- Req2, HandlerState, <<>>).
+ handler_before_loop(State2#state{key=undefined,
+ messages=Transport:messages()}, Req2, HandlerState, <<>>).
-spec handler_before_loop(#state{}, Req, any(), binary())
-> {ok, Req, cowboy_middleware:env()}
@@ -261,26 +209,8 @@ handler_loop(State=#state{
%% No more data.
websocket_data(State, Req, HandlerState, <<>>) ->
handler_before_loop(State, Req, HandlerState, <<>>);
-%% hixie-76 close frame.
-websocket_data(State=#state{version=0}, Req, HandlerState,
- << 255, 0, _Rest/binary >>) ->
- websocket_close(State, Req, HandlerState, {normal, closed});
-%% hixie-76 data frame. We only support the frame type 0, same as the specs.
-websocket_data(State=#state{version=0, eop=EOP}, Req, HandlerState,
- Data = << 0, _/binary >>) ->
- case binary:match(Data, EOP) of
- {Pos, 1} ->
- Pos2 = Pos - 1,
- << 0, Payload:Pos2/binary, 255, Rest/bits >> = Data,
- handler_call(State, Req, HandlerState,
- Rest, websocket_handle, {text, Payload}, fun websocket_data/4);
- nomatch ->
- %% @todo We probably should allow limiting frame length.
- handler_before_loop(State, Req, HandlerState, Data)
- end;
-%% incomplete hybi data frame.
-websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
- when Version =/= 0, byte_size(Data) =:= 1 ->
+%% Incomplete.
+websocket_data(State, Req, HandlerState, Data) when byte_size(Data) =:= 1 ->
handler_before_loop(State, Req, HandlerState, Data);
%% 7 bit payload length prefix exists
websocket_data(State, Req, HandlerState,
@@ -361,8 +291,8 @@ websocket_data(State, Req, HandlerState, _Fin, _Rsv, Opcode, _Mask, PayloadLen,
_Rest, _Data) when Opcode >= 8, PayloadLen > 125 ->
websocket_close(State, Req, HandlerState, {error, badframe});
%% unfragmented message. unmask and dispatch the message.
-websocket_data(State=#state{version=Version}, Req, HandlerState, _Fin=1, _Rsv=0,
- Opcode, Mask, PayloadLen, Rest, Data) when Version =/= 0 ->
+websocket_data(State, Req, HandlerState, _Fin=1, _Rsv=0,
+ Opcode, Mask, PayloadLen, Rest, Data) ->
websocket_before_unmask(
State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen);
%% Something was wrong with the frame. Close the connection.
@@ -370,7 +300,7 @@ websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode, _Mask,
_PayloadLen, _Rest, _Data) ->
websocket_close(State, Req, HandlerState, {error, badframe}).
-%% hybi routing depending on whether unmasking is needed.
+%% Routing depending on whether unmasking is needed.
-spec websocket_before_unmask(#state{}, Req, any(), binary(),
binary(), opcode(), 0 | 1, non_neg_integer() | undefined)
-> {ok, Req, cowboy_middleware:env()}
@@ -390,7 +320,7 @@ websocket_before_unmask(State, Req, HandlerState, Data,
Opcode, Payload, MaskKey)
end.
-%% hybi unmasking.
+%% Unmasking.
-spec websocket_unmask(#state{}, Req, any(), binary(),
opcode(), binary(), mask_key())
-> {ok, Req, cowboy_middleware:env()}
@@ -434,7 +364,7 @@ websocket_unmask(State, Req, HandlerState, RemainingData,
websocket_dispatch(State, Req, HandlerState, RemainingData,
Opcode, Acc).
-%% hybi dispatching.
+%% Dispatching.
-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
@@ -470,7 +400,7 @@ websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, _Payload) ->
%% Ping control frame. Send a pong back and forward the ping to the handler.
websocket_dispatch(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, RemainingData, 9, Payload) ->
- Len = hybi_payload_length(byte_size(Payload)),
+ Len = payload_length_to_binary(byte_size(Payload)),
Transport:send(Socket, << 1:1, 0:3, 10:4, 0:1, Len/bits, Payload/binary >>),
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {ping, Payload}, fun websocket_data/4);
@@ -563,13 +493,6 @@ websocket_opcode(pong) -> 10.
-spec websocket_send(frame(), #state{})
-> ok | shutdown | {error, atom()}.
-%% hixie-76 text frame.
-websocket_send({text, Payload}, #state{
- socket=Socket, transport=Transport, version=0}) ->
- Transport:send(Socket, [0, Payload, 255]);
-%% Ignore all unknown frame types for compatibility with hixie 76.
-websocket_send(_Any, #state{version=0}) ->
- ok;
websocket_send(Type, #state{socket=Socket, transport=Transport})
when Type =:= close ->
Opcode = websocket_opcode(Type),
@@ -589,7 +512,7 @@ websocket_send({Type = close, StatusCode, Payload}, #state{
Len = 2 + iolist_size(Payload),
%% Control packets must not be > 125 in length.
true = Len =< 125,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]),
shutdown;
@@ -602,7 +525,7 @@ websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) ->
true ->
true
end,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]).
@@ -620,10 +543,6 @@ websocket_send_many([Frame|Tail], State) ->
-spec websocket_close(#state{}, Req, any(), {atom(), atom()})
-> {ok, Req, cowboy_middleware:env()}
when Req::cowboy_req:req().
-websocket_close(State=#state{socket=Socket, transport=Transport, version=0},
- Req, HandlerState, Reason) ->
- Transport:send(Socket, << 255, 0 >>),
- handler_terminate(State, Req, HandlerState, Reason);
websocket_close(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, Reason) ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
@@ -648,30 +567,9 @@ handler_terminate(#state{env=Env, handler=Handler, handler_opts=HandlerOpts},
end,
{ok, Req, [{result, closed}|Env]}.
-%% hixie-76 specific.
-
--spec hixie76_challenge(binary(), binary(), binary()) -> binary().
-hixie76_challenge(Key1, Key2, Key3) ->
- IntKey1 = hixie76_key_to_integer(Key1),
- IntKey2 = hixie76_key_to_integer(Key2),
- erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
-
--spec hixie76_key_to_integer(binary()) -> integer().
-hixie76_key_to_integer(Key) ->
- Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]),
- Spaces = length([C || << C >> <= Key, C =:= 32]),
- Number div Spaces.
-
-%% hybi specific.
-
--spec hybi_challenge(binary()) -> binary().
-hybi_challenge(Key) ->
- Bin = << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>,
- base64:encode(crypto:sha(Bin)).
-
--spec hybi_payload_length(0..16#7fffffffffffffff)
+-spec payload_length_to_binary(0..16#7fffffffffffffff)
-> << _:7 >> | << _:23 >> | << _:71 >>.
-hybi_payload_length(N) ->
+payload_length_to_binary(N) ->
case N of
N when N =< 125 -> << N:7 >>;
N when N =< 16#ffff -> << 126:7, N:16 >>;
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
index ed084d6..5702130 100644
--- a/test/ws_SUITE.erl
+++ b/test/ws_SUITE.erl
@@ -122,10 +122,7 @@ init_dispatch() ->
%% ws and wss.
-%% This test makes sure the code works even if we wait for a reply
-%% before sending the third challenge key in the GET body.
-%%
-%% This ensures that Cowboy will work fine with proxies on hixie.
+%% We do not support hixie76 anymore.
ws0(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
@@ -140,34 +137,8 @@ ws0(Config) ->
"Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n"
"\r\n"),
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest}
- = erlang:decode_packet(http, Handshake, []),
- [Headers, <<>>] = websocket_headers(
- erlang:decode_packet(httph, Rest, []), []),
- {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
- {'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers),
- {"sec-websocket-location", "ws://localhost/websocket"}
- = lists:keyfind("sec-websocket-location", 1, Headers),
- {"sec-websocket-origin", "http://localhost"}
- = lists:keyfind("sec-websocket-origin", 1, Headers),
- ok = gen_tcp:send(Socket, <<15,245,8,18,2,204,133,33>>),
- {ok, Body} = gen_tcp:recv(Socket, 0, 6000),
- <<169,244,191,103,146,33,149,59,74,104,67,5,99,118,171,236>> = Body,
- ok = gen_tcp:send(Socket, << 0, "client_msg", 255 >>),
- {ok, << 0, "client_msg", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_init", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- %% We try to send another HTTP request to make sure
- %% the server closed the request.
- ok = gen_tcp:send(Socket, [
- << 255, 0 >>, %% Close websocket command.
- "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" %% Server should ignore it.
- ]),
- {ok, << 255, 0 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
+ {ok, {http_response, {1, 1}, 400, _}, _}
+ = erlang:decode_packet(http, Handshake, []).
ws8(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
@@ -479,7 +450,6 @@ ws_text_fragments(Config) ->
<< 16#9f >>, << 16#4d >>, << 16#51 >>, << 16#58 >>]),
{ok, << 1:1, 0:3, 1:4, 0:1, 15:7, "HelloHelloHello" >>}
= gen_tcp:recv(Socket, 0, 6000),
-
ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
@@ -547,41 +517,28 @@ ws_timeout_reset(Config) ->
"GET /ws_timeout_cancel HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: Upgrade\r\n"
- "Upgrade: WebSocket\r\n"
- "Origin: http://localhost\r\n"
- "Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i\r\n"
- "Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-Websocket-Version: 13\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"\r\n"]),
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest}
+ {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest}
= erlang:decode_packet(http, Handshake, []),
[Headers, <<>>] = websocket_headers(
erlang:decode_packet(httph, Rest, []), []),
{'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
- {'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers),
- {"sec-websocket-location", "ws://localhost/ws_timeout_cancel"}
- = lists:keyfind("sec-websocket-location", 1, Headers),
- {"sec-websocket-origin", "http://localhost"}
- = lists:keyfind("sec-websocket-origin", 1, Headers),
- ok = gen_tcp:send(Socket, <<15,245,8,18,2,204,133,33>>),
- {ok, Body} = gen_tcp:recv(Socket, 0, 6000),
- <<169,244,191,103,146,33,149,59,74,104,67,5,99,118,171,236>> = Body,
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 255, 0 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
+ {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ [begin
+ ok = gen_tcp:send(Socket, << 16#81, 16#85, 16#37, 16#fa, 16#21, 16#3d,
+ 16#7f, 16#9f, 16#4d, 16#51, 16#58 >>),
+ {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>}
+ = gen_tcp:recv(Socket, 0, 6000),
+ ok = timer:sleep(500)
+ end || _ <- [1, 2, 3, 4]],
+ {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.