0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add WebSocket drafts 7, 8, 9 and 10 implementation

The implementation is only partial for now but should work for
all browsers implementing it.
This commit is contained in:
Loïc Hoguin 2011-08-23 16:24:02 +02:00
parent 24bf2c54d0
commit 2374aa7e07
4 changed files with 324 additions and 86 deletions

View file

@ -195,12 +195,14 @@ websocket_init(TransportName, Req, _Opts) ->
erlang:start_timer(1000, self(), <<"Hello!">>), erlang:start_timer(1000, self(), <<"Hello!">>),
{ok, Req, undefined_state}. {ok, Req, undefined_state}.
websocket_handle(Msg, Req, State) -> websocket_handle({text, Msg}, Req, State) ->
{reply, << "That's what she said! ", Msg/binary >>, Req, State}. {reply, {text, << "That's what she said! ", Msg/binary >>}, Req, State};
websocket_handle(_Data, Req, State) ->
{ok, Req, State}.
websocket_info({timeout, _Ref, Msg}, Req, State) -> websocket_info({timeout, _Ref, Msg}, Req, State) ->
erlang:start_timer(1000, self(), <<"How' you doin'?">>), erlang:start_timer(1000, self(), <<"How' you doin'?">>),
{reply, Msg, Req, State}; {reply, {text, Msg}, Req, State};
websocket_info(_Info, Req, State) -> websocket_info(_Info, Req, State) ->
{ok, Req, State}. {ok, Req, State}.
@ -212,6 +214,12 @@ Of course you can have an HTTP handler doing both HTTP and Websocket
handling, but for the sake of this example we're ignoring the HTTP handling, but for the sake of this example we're ignoring the HTTP
part entirely. part entirely.
As the Websocket protocol is still a draft the API is subject to change
regularly when support to the most recent drafts gets added. Features may
be added, changed or removed before the protocol gets finalized. Cowboy
tries to implement all drafts transparently and give a single interface to
handle them all, however.
Using Cowboy with other protocols Using Cowboy with other protocols
--------------------------------- ---------------------------------

View file

@ -12,15 +12,29 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% @doc WebSocket protocol draft hixie-76 implementation. %% @doc WebSocket protocol implementation.
%% %%
%% Known to work with the following browsers: %% Supports the protocol version 0 (hixie-76), version 7 (hybi-7)
%% and version 8 (hybi-8, hybi-9 and hybi-10).
%%
%% Version 0 is supported by the following browsers:
%% <ul> %% <ul>
%% <li>Mozilla Firefox 4.0 (disabled by default)</li> %% <li>Firefox 4-5 (disabled by default)</li>
%% <li>Google Chrome 6+</li> %% <li>Chrome 6-13</li>
%% <li>Safari 5.0.1+</li> %% <li>Safari 5.0.1+</li>
%% <li>Opera 11.00+ (disabled by default)</li> %% <li>Opera 11.00+ (disabled by default)</li>
%% </ul> %% </ul>
%%
%% Version 7 is supported by the following browser:
%% <ul>
%% <li>Firefox 6</li>
%% </ul>
%%
%% Version 8 is supported by the following browsers:
%% <ul>
%% <li>Firefox 7</li>
%% <li>Chrome 14+</li>
%% </ul>
-module(cowboy_http_websocket). -module(cowboy_http_websocket).
-export([upgrade/4]). %% API. -export([upgrade/4]). %% API.
@ -29,15 +43,19 @@
-include("include/http.hrl"). -include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
-type mask_key() :: 0..16#ffffffff.
-record(state, { -record(state, {
version :: 0 | 7 | 8,
handler :: module(), handler :: module(),
opts :: any(), opts :: any(),
origin = undefined :: undefined | binary(),
challenge = undefined :: undefined | binary(), challenge = undefined :: undefined | binary(),
timeout = infinity :: timeout(), timeout = infinity :: timeout(),
messages = undefined :: undefined | {atom(), atom(), atom()}, messages = undefined :: undefined | {atom(), atom(), atom()},
eop :: tuple(), hibernate = false :: boolean(),
hibernate = false :: boolean() eop :: undefined | tuple(), %% hixie-76 specific.
origin = undefined :: undefined | binary() %% hixie-76 specific.
}). }).
%% @doc Upgrade a HTTP request to the WebSocket protocol. %% @doc Upgrade a HTTP request to the WebSocket protocol.
@ -48,35 +66,49 @@
-spec upgrade(pid(), module(), any(), #http_req{}) -> ok. -spec upgrade(pid(), module(), any(), #http_req{}) -> ok.
upgrade(ListenerPid, Handler, Opts, Req) -> upgrade(ListenerPid, Handler, Opts, Req) ->
cowboy_listener:move_connection(ListenerPid, websocket, self()), cowboy_listener:move_connection(ListenerPid, websocket, self()),
EOP = binary:compile_pattern(<< 255 >>), case catch websocket_upgrade(#state{handler=Handler, opts=Opts}, Req) of
case catch websocket_upgrade(#state{handler=Handler, opts=Opts, eop=EOP}, Req) of
{ok, State, Req2} -> handler_init(State, Req2); {ok, State, Req2} -> handler_init(State, Req2);
{'EXIT', _Reason} -> upgrade_error(Req) {'EXIT', _Reason} -> upgrade_error(Req)
end. end.
%% @todo We need a function to properly parse headers according to their ABNF,
%% instead of having ugly code like this case here.
-spec websocket_upgrade(#state{}, #http_req{}) -> {ok, #state{}, #http_req{}}. -spec websocket_upgrade(#state{}, #http_req{}) -> {ok, #state{}, #http_req{}}.
websocket_upgrade(State, Req) -> websocket_upgrade(State, Req) ->
{<<"Upgrade">>, Req2} = cowboy_http_req:header('Connection', Req), case cowboy_http_req:header('Connection', Req) of
{<<"WebSocket">>, Req3} = cowboy_http_req:header('Upgrade', Req2), {<<"Upgrade">>, Req2} -> ok;
{Origin, Req4} = cowboy_http_req:header(<<"Origin">>, Req3), {<<"keep-alive, Upgrade">>, Req2} -> ok %% @todo Temp. For Firefox 6.
{Key1, Req5} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req4), end,
{Key2, Req6} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req5), {Version, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Version">>, Req2),
websocket_upgrade(Version, State, Req3).
%% @todo Handle the Sec-Websocket-Protocol header.
-spec websocket_upgrade(undefined | <<_:8>>, #state{}, #http_req{})
-> {ok, #state{}, #http_req{}}.
%% No version given. Assuming hixie-76 draft.
%% @todo Check Origin?
websocket_upgrade(undefined, State, Req) ->
{<<"WebSocket">>, Req2} = cowboy_http_req:header('Upgrade', Req),
{Origin, Req3} = cowboy_http_req:header(<<"Origin">>, Req2),
{Key1, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req3),
{Key2, Req5} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req4),
false = lists:member(undefined, [Origin, Key1, Key2]), false = lists:member(undefined, [Origin, Key1, Key2]),
{ok, Key3, Req7} = cowboy_http_req:body(8, Req6), {ok, Key3, Req6} = cowboy_http_req:body(8, Req5),
Challenge = challenge(Key1, Key2, Key3), Challenge = hixie76_challenge(Key1, Key2, Key3),
{ok, State#state{origin=Origin, challenge=Challenge}, Req7}. EOP = binary:compile_pattern(<< 255 >>),
{ok, State#state{version=0, origin=Origin, challenge=Challenge,
-spec challenge(binary(), binary(), binary()) -> binary(). eop=EOP}, Req6};
challenge(Key1, Key2, Key3) -> %% Versions 7 and 8. Implementation follows the hybi 7 through 10 drafts.
IntKey1 = key_to_integer(Key1), %% @todo We don't need Origin?
IntKey2 = key_to_integer(Key2), websocket_upgrade(<< Version >>, State, Req)
erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>). when Version =:= $7; Version =:= $8 ->
{<<"websocket">>, Req2} = cowboy_http_req:header('Upgrade', Req),
-spec key_to_integer(binary()) -> integer(). {Origin, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Origin">>, Req2),
key_to_integer(Key) -> {Key, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Key">>, Req3),
Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]), false = lists:member(undefined, [Origin, Key]),
Spaces = length([C || << C >> <= Key, C =:= 32]), Challenge = hybi_challenge(Key),
Number div Spaces. {ok, State#state{version=Version - $0, origin=Origin,
challenge=Challenge}, Req4}.
-spec handler_init(#state{}, #http_req{}) -> ok. -spec handler_init(#state{}, #http_req{}) -> ok.
handler_init(State=#state{handler=Handler, opts=Opts}, handler_init(State=#state{handler=Handler, opts=Opts},
@ -103,35 +135,30 @@ upgrade_error(Req=#http_req{socket=Socket, transport=Transport}) ->
Transport:close(Socket). Transport:close(Socket).
-spec websocket_handshake(#state{}, #http_req{}, any()) -> ok. -spec websocket_handshake(#state{}, #http_req{}, any()) -> ok.
websocket_handshake(State=#state{origin=Origin, challenge=Challenge}, websocket_handshake(State=#state{version=0, origin=Origin,
Req=#http_req{transport=Transport, raw_host=Host, port=Port, challenge=Challenge}, Req=#http_req{transport=Transport,
raw_path=Path}, HandlerState) -> raw_host=Host, port=Port, raw_path=Path}, HandlerState) ->
Location = websocket_location(Transport:name(), Host, Port, Path), Location = hixie76_location(Transport:name(), Host, Port, Path),
{ok, Req2} = cowboy_http_req:reply( {ok, Req2} = cowboy_http_req:reply(
<<"101 WebSocket Protocol Handshake">>, <<"101 WebSocket Protocol Handshake">>,
[{<<"Connection">>, <<"Upgrade">>}, [{<<"Connection">>, <<"Upgrade">>},
{<<"Upgrade">>, <<"WebSocket">>}, {<<"Upgrade">>, <<"WebSocket">>},
{<<"Sec-WebSocket-Location">>, Location}, {<<"Sec-Websocket-Location">>, Location},
{<<"Sec-WebSocket-Origin">>, Origin}], {<<"Sec-Websocket-Origin">>, Origin}],
Challenge, Req#http_req{resp_state=waiting}), Challenge, Req#http_req{resp_state=waiting}),
handler_before_loop(State#state{messages=Transport:messages()},
Req2, HandlerState, <<>>);
websocket_handshake(State=#state{challenge=Challenge},
Req=#http_req{transport=Transport}, HandlerState) ->
{ok, Req2} = cowboy_http_req:reply(
<<"101 Switching Protocols">>,
[{<<"Connection">>, <<"Upgrade">>},
{<<"Upgrade">>, <<"websocket">>},
{<<"Sec-Websocket-Accept">>, Challenge}],
[], Req#http_req{resp_state=waiting}),
handler_before_loop(State#state{messages=Transport:messages()}, handler_before_loop(State#state{messages=Transport:messages()},
Req2, HandlerState, <<>>). Req2, HandlerState, <<>>).
-spec websocket_location(atom(), binary(), inet:ip_port(), binary())
-> binary().
websocket_location(Protocol, Host, Port, Path) ->
<< (websocket_location_protocol(Protocol))/binary, "://", Host/binary,
(websocket_location_port(ssl, Port))/binary, Path/binary >>.
-spec websocket_location_protocol(atom()) -> binary().
websocket_location_protocol(ssl) -> <<"wss">>;
websocket_location_protocol(_) -> <<"ws">>.
-spec websocket_location_port(atom(), inet:ip_port()) -> binary().
websocket_location_port(ssl, 443) -> <<"">>;
websocket_location_port(_, 80) -> <<"">>;
websocket_location_port(_, Port) -> <<":", (list_to_binary(integer_to_list(Port)))/binary>>.
-spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> ok. -spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> ok.
handler_before_loop(State=#state{hibernate=true}, handler_before_loop(State=#state{hibernate=true},
Req=#http_req{socket=Socket, transport=Transport}, Req=#http_req{socket=Socket, transport=Transport},
@ -164,29 +191,118 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout=Timeout},
end. end.
-spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok. -spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok.
websocket_data(State, Req, HandlerState, << 255, 0, _Rest/bits >>) -> %% No more data.
websocket_close(State, Req, HandlerState, {normal, closed});
websocket_data(State, Req, HandlerState, <<>>) -> websocket_data(State, Req, HandlerState, <<>>) ->
handler_before_loop(State, Req, HandlerState, <<>>); handler_before_loop(State, Req, HandlerState, <<>>);
websocket_data(State, Req, HandlerState, Data) -> %% hixie-76 close frame.
websocket_frame(State, Req, HandlerState, Data, binary:first(Data)). websocket_data(State=#state{version=0}, Req, HandlerState,
<< 255, 0, _Rest/bits >>) ->
%% We do not support any frame type other than 0 yet. Just like the specs. websocket_close(State, Req, HandlerState, {normal, closed});
-spec websocket_frame(#state{}, #http_req{}, any(), binary(), byte()) -> ok. %% hixie-76 data frame. We only support the frame type 0, same as the specs.
websocket_frame(State=#state{eop=EOP}, Req, HandlerState, Data, 0) -> websocket_data(State=#state{version=0, eop=EOP}, Req, HandlerState,
Data = << 0, _/bits >>) ->
case binary:match(Data, EOP) of case binary:match(Data, EOP) of
{Pos, 1} -> {Pos, 1} ->
Pos2 = Pos - 1, Pos2 = Pos - 1,
<< 0, Frame:Pos2/binary, 255, Rest/bits >> = Data, << 0, Payload:Pos2/binary, 255, Rest/bits >> = Data,
handler_call(State, Req, HandlerState, handler_call(State, Req, HandlerState,
Rest, websocket_handle, Frame, fun websocket_data/4); Rest, websocket_handle, {text, Payload}, fun websocket_data/4);
nomatch -> nomatch ->
%% @todo We probably should allow limiting frame length. %% @todo We probably should allow limiting frame length.
handler_before_loop(State, Req, HandlerState, Data) handler_before_loop(State, Req, HandlerState, Data)
end; end;
websocket_frame(State, Req, HandlerState, _Data, _FrameType) -> %% hybi data frame.
%% @todo Handle Fin.
websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
when Version =/= 0 ->
<< 1:1, 0:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >> = Data,
{PayloadLen2, Rest2} = case PayloadLen of
126 -> << L:16, R/bits >> = Rest, {L, R};
127 -> << 0:1, L:63, R/bits >> = Rest, {L, R};
PayloadLen -> {PayloadLen, Rest}
end,
case {Mask, PayloadLen2} of
{0, 0} ->
websocket_dispatch(State, Req, HandlerState, Rest2, Opcode, <<>>);
{1, N} when N + 4 < byte_size(Rest2) ->
%% @todo We probably should allow limiting frame length.
handler_before_loop(State, Req, HandlerState, Data);
{1, _N} ->
<< MaskKey:32, Payload:PayloadLen2/binary, Rest3/bits >> = Rest2,
websocket_unmask(State, Req, HandlerState, Rest3,
Opcode, Payload, MaskKey)
end;
%% Something was wrong with the frame. Close the connection.
websocket_data(State, Req, HandlerState, _Bad) ->
websocket_close(State, Req, HandlerState, {error, badframe}). websocket_close(State, Req, HandlerState, {error, badframe}).
%% hybi unmasking.
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
opcode(), binary(), mask_key()) -> ok.
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey) ->
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey, <<>>).
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
opcode(), binary(), mask_key(), binary()) -> ok.
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Rest, MaskKey, << Acc/binary, T:32 >>);
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:24 >>, MaskKey, Acc) ->
<< MaskKey2:24, _:8 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
websocket_dispatch(State, Req, HandlerState, RemainingData,
Opcode, << Acc/binary, T:24 >>);
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:16 >>, MaskKey, Acc) ->
<< MaskKey2:16, _:16 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
websocket_dispatch(State, Req, HandlerState, RemainingData,
Opcode, << Acc/binary, T:16 >>);
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:8 >>, MaskKey, Acc) ->
<< MaskKey2:8, _:24 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
websocket_dispatch(State, Req, HandlerState, RemainingData,
Opcode, << Acc/binary, T:8 >>);
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, <<>>, _MaskKey, Acc) ->
websocket_dispatch(State, Req, HandlerState, RemainingData,
Opcode, Acc).
%% hybi dispatching.
-spec websocket_dispatch(#state{}, #http_req{}, any(), binary(),
opcode(), binary()) -> ok.
%% @todo Fragmentation.
%~ websocket_dispatch(State, Req, HandlerState, RemainingData, 0, Payload) ->
%% Text frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {text, Payload}, fun websocket_data/4);
%% Binary frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {binary, Payload}, fun websocket_data/4);
%% Close control frame.
%% @todo Handle the optional Payload.
websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, _Payload) ->
websocket_close(State, Req, HandlerState, {normal, closed});
%% Ping control frame. Send a pong back and forward the ping to the handler.
websocket_dispatch(State, Req=#http_req{socket=Socket, transport=Transport},
HandlerState, RemainingData, 9, Payload) ->
Len = hybi_payload_length(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);
%% Pong control frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {pong, Payload}, fun websocket_data/4).
-spec handler_call(#state{}, #http_req{}, any(), binary(), -spec handler_call(#state{}, #http_req{}, any(), binary(),
atom(), any(), fun()) -> ok. atom(), any(), fun()) -> ok.
handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState, handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
@ -197,11 +313,11 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
{ok, Req2, HandlerState2, hibernate} -> {ok, Req2, HandlerState2, hibernate} ->
NextState(State#state{hibernate=true}, NextState(State#state{hibernate=true},
Req2, HandlerState2, RemainingData); Req2, HandlerState2, RemainingData);
{reply, Data, Req2, HandlerState2} -> {reply, Payload, Req2, HandlerState2} ->
websocket_send(Data, Req2), websocket_send(Payload, State, Req2),
NextState(State, Req2, HandlerState2, RemainingData); NextState(State, Req2, HandlerState2, RemainingData);
{reply, Data, Req2, HandlerState2, hibernate} -> {reply, Payload, Req2, HandlerState2, hibernate} ->
websocket_send(Data, Req2), websocket_send(Payload, State, Req2),
NextState(State#state{hibernate=true}, NextState(State#state{hibernate=true},
Req2, HandlerState2, RemainingData); Req2, HandlerState2, RemainingData);
{shutdown, Req2, HandlerState2} -> {shutdown, Req2, HandlerState2} ->
@ -217,15 +333,37 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
websocket_close(State, Req, HandlerState, {error, handler}) websocket_close(State, Req, HandlerState, {error, handler})
end. end.
-spec websocket_send(binary(), #http_req{}) -> ok. -spec websocket_send(binary(), #state{}, #http_req{}) -> ok | ignore.
websocket_send(Data, #http_req{socket=Socket, transport=Transport}) -> %% hixie-76 text frame.
Transport:send(Socket, << 0, Data/binary, 255 >>). websocket_send({text, Payload}, #state{version=0},
#http_req{socket=Socket, transport=Transport}) ->
Transport:send(Socket, << 0, Payload/binary, 255 >>);
%% Ignore all unknown frame types for compatibility with hixie 76.
websocket_send(_Any, #state{version=0}, _Req) ->
ignore;
websocket_send({Type, Payload}, _State,
#http_req{socket=Socket, transport=Transport}) ->
Opcode = case Type of
text -> 1;
binary -> 2;
ping -> 9;
pong -> 10
end,
Len = hybi_payload_length(byte_size(Payload)),
Transport:send(Socket, << 1:1, 0:3, Opcode:4,
0:1, Len/bits, Payload/binary >>).
-spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> ok. -spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> ok.
websocket_close(State, Req=#http_req{socket=Socket, transport=Transport}, websocket_close(State=#state{version=0}, Req=#http_req{socket=Socket,
HandlerState, Reason) -> transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 255, 0 >>), Transport:send(Socket, << 255, 0 >>),
Transport:close(Socket), Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason);
%% @todo Send a Payload? Using Reason is usually good but we're quite careless.
websocket_close(State, Req=#http_req{socket=Socket,
transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason). handler_terminate(State, Req, HandlerState, Reason).
-spec handler_terminate(#state{}, #http_req{}, -spec handler_terminate(#state{}, #http_req{},
@ -244,19 +382,67 @@ handler_terminate(#state{handler=Handler, opts=Opts},
HandlerState, Req, erlang:get_stacktrace()]) HandlerState, Req, erlang:get_stacktrace()])
end. end.
%% 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.
-spec hixie76_location(atom(), binary(), inet:ip_port(), binary())
-> binary().
hixie76_location(Protocol, Host, Port, Path) ->
<< (hixie76_location_protocol(Protocol))/binary, "://", Host/binary,
(hixie76_location_port(ssl, Port))/binary, Path/binary >>.
-spec hixie76_location_protocol(atom()) -> binary().
hixie76_location_protocol(ssl) -> <<"wss">>;
hixie76_location_protocol(_) -> <<"ws">>.
-spec hixie76_location_port(atom(), inet:ip_port()) -> binary().
hixie76_location_port(ssl, 443) ->
<<"">>;
hixie76_location_port(_, 80) ->
<<"">>;
hixie76_location_port(_, Port) ->
<<":", (list_to_binary(integer_to_list(Port)))/binary>>.
%% 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)
-> << _:7 >> | << _:23 >> | << _:71 >>.
hybi_payload_length(N) ->
case N of
N when N =< 125 -> << N:7 >>;
N when N =< 16#ffff -> << 126:7, N:16 >>;
N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >>
end.
%% Tests. %% Tests.
-ifdef(TEST). -ifdef(TEST).
websocket_location_test() -> hixie76_location_test() ->
?assertEqual(<<"ws://localhost/path">>, ?assertEqual(<<"ws://localhost/path">>,
websocket_location(other, <<"localhost">>, 80, <<"/path">>)), hixie76_location(other, <<"localhost">>, 80, <<"/path">>)),
?assertEqual(<<"ws://localhost:8080/path">>, ?assertEqual(<<"ws://localhost:8080/path">>,
websocket_location(other, <<"localhost">>, 8080, <<"/path">>)), hixie76_location(other, <<"localhost">>, 8080, <<"/path">>)),
?assertEqual(<<"wss://localhost/path">>, ?assertEqual(<<"wss://localhost/path">>,
websocket_location(ssl, <<"localhost">>, 443, <<"/path">>)), hixie76_location(ssl, <<"localhost">>, 443, <<"/path">>)),
?assertEqual(<<"wss://localhost:8443/path">>, ?assertEqual(<<"wss://localhost:8443/path">>,
websocket_location(ssl, <<"localhost">>, 8443, <<"/path">>)), hixie76_location(ssl, <<"localhost">>, 8443, <<"/path">>)),
ok. ok.
-endif. -endif.

View file

@ -19,8 +19,8 @@
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1, -export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
init_per_group/2, end_per_group/2]). %% ct. init_per_group/2, end_per_group/2]). %% ct.
-export([chunked_response/1, headers_dupe/1, headers_huge/1, -export([chunked_response/1, headers_dupe/1, headers_huge/1,
nc_rand/1, pipeline/1, raw/1]). %% http. nc_rand/1, pipeline/1, raw/1, ws0/1, ws8/1]). %% http.
-export([http_200/1, http_404/1, websocket/1]). %% http and https. -export([http_200/1, http_404/1]). %% http and https.
%% ct. %% ct.
@ -30,7 +30,7 @@ all() ->
groups() -> groups() ->
BaseTests = [http_200, http_404], BaseTests = [http_200, http_404],
[{http, [], [chunked_response, headers_dupe, headers_huge, [{http, [], [chunked_response, headers_dupe, headers_huge,
nc_rand, pipeline, raw, websocket] ++ BaseTests}, nc_rand, pipeline, raw, ws0, ws8] ++ BaseTests},
{https, [], BaseTests}]. {https, [], BaseTests}].
init_per_suite(Config) -> init_per_suite(Config) ->
@ -193,7 +193,7 @@ raw(Config) ->
[{Packet, StatusCode} = raw_req(Packet, Config) [{Packet, StatusCode} = raw_req(Packet, Config)
|| {Packet, StatusCode} <- Tests]. || {Packet, StatusCode} <- Tests].
websocket(Config) -> ws0(Config) ->
{port, Port} = lists:keyfind(port, 1, Config), {port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port, {ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]), [binary, {active, false}, {packet, raw}]),
@ -209,7 +209,8 @@ websocket(Config) ->
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
{ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest} {ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest}
= erlang:decode_packet(http, Handshake, []), = erlang:decode_packet(http, Handshake, []),
[Headers, Body] = websocket_headers(erlang:decode_packet(httph, Rest, []), []), [Headers, Body] = websocket_headers(
erlang:decode_packet(httph, Rest, []), []),
{'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers), {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
{'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers), {'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers),
{"sec-websocket-location", "ws://localhost/websocket"} {"sec-websocket-location", "ws://localhost/websocket"}
@ -228,6 +229,47 @@ websocket(Config) ->
{error, closed} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok. ok.
ws8(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]),
ok = gen_tcp:send(Socket, [
"GET /websocket HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: Upgrade\r\n"
"Upgrade: websocket\r\n"
"Sec-WebSocket-Origin: http://localhost\r\n"
"Sec-WebSocket-Version: 8\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"\r\n"]),
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
{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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
= lists:keyfind("sec-websocket-accept", 1, Headers),
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, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping
{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong
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),
ok.
websocket_headers({ok, http_eoh, Rest}, Acc) -> websocket_headers({ok, http_eoh, Rest}, Acc) ->
[Acc, Rest]; [Acc, Rest];
websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) -> websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) ->

View file

@ -20,12 +20,14 @@ websocket_init(_TransportName, Req, _Opts) ->
erlang:start_timer(1000, self(), <<"websocket_init">>), erlang:start_timer(1000, self(), <<"websocket_init">>),
{ok, Req, undefined}. {ok, Req, undefined}.
websocket_handle(Data, Req, State) -> websocket_handle({text, Data}, Req, State) ->
{reply, Data, Req, State}. {reply, {text, Data}, Req, State};
websocket_handle(_Frame, Req, State) ->
{ok, Req, State}.
websocket_info({timeout, _Ref, Msg}, Req, State) -> websocket_info({timeout, _Ref, Msg}, Req, State) ->
erlang:start_timer(1000, self(), <<"websocket_handle">>), erlang:start_timer(1000, self(), <<"websocket_handle">>),
{reply, Msg, Req, State}; {reply, {text, Msg}, Req, State};
websocket_info(_Info, Req, State) -> websocket_info(_Info, Req, State) ->
{ok, Req, State}. {ok, Req, State}.