Browse Source

Add WebSocket drafts 7, 8, 9 and 10 implementation

The implementation is only partial for now but should work for
all browsers implementing it.
Loïc Hoguin 13 years ago
parent
commit
2374aa7e07
4 changed files with 324 additions and 86 deletions
  1. 11 3
      README.md
  2. 261 75
      src/cowboy_http_websocket.erl
  3. 47 5
      test/http_SUITE.erl
  4. 5 3
      test/websocket_handler.erl

+ 11 - 3
README.md

@@ -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) ->
-    {reply, << "That's what she said! ", Msg/binary >>, Req, State}.
+websocket_handle({text, Msg}, 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
 ---------------------------------
 ---------------------------------
 
 

+ 261 - 75
src/cowboy_http_websocket.erl

@@ -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>Google Chrome 6+</li>
+%%  <li>Firefox 4-5 (disabled by default)</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, eop=EOP}, Req) of
+	case catch websocket_upgrade(#state{handler=Handler, opts=Opts}, 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),
-	{<<"WebSocket">>, Req3} = cowboy_http_req:header('Upgrade', Req2),
-	{Origin, Req4} = cowboy_http_req:header(<<"Origin">>, Req3),
-	{Key1, Req5} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req4),
-	{Key2, Req6} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req5),
-	false = lists:member(undefined, [Origin, Key1, Key2]),
-	{ok, Key3, Req7} = cowboy_http_req:body(8, Req6),
-	Challenge = challenge(Key1, Key2, Key3),
-	{ok, State#state{origin=Origin, challenge=Challenge}, Req7}.
-
--spec challenge(binary(), binary(), binary()) -> binary().
-challenge(Key1, Key2, Key3) ->
-	IntKey1 = key_to_integer(Key1),
-	IntKey2 = key_to_integer(Key2),
-	erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
+	case cowboy_http_req:header('Connection', Req) of
+		{<<"Upgrade">>, Req2} -> ok;
+		{<<"keep-alive, Upgrade">>, Req2} -> ok %% @todo Temp. For Firefox 6.
+	end,
+	{Version, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Version">>, Req2),
+	websocket_upgrade(Version, State, Req3).
 
 
--spec key_to_integer(binary()) -> integer().
-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.
+%% @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]),
+	{ok, Key3, Req6} = cowboy_http_req:body(8, Req5),
+	Challenge = hixie76_challenge(Key1, Key2, Key3),
+	EOP = binary:compile_pattern(<< 255 >>),
+	{ok, State#state{version=0, origin=Origin, challenge=Challenge,
+		eop=EOP}, Req6};
+%% Versions 7 and 8. Implementation follows the hybi 7 through 10 drafts.
+%% @todo We don't need Origin?
+websocket_upgrade(<< Version >>, State, Req)
+		when Version =:= $7; Version =:= $8 ->
+	{<<"websocket">>, Req2} = cowboy_http_req:header('Upgrade', Req),
+	{Origin, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Origin">>, Req2),
+	{Key, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Key">>, Req3),
+	false = lists:member(undefined, [Origin, Key]),
+	Challenge = hybi_challenge(Key),
+	{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},
-		Req=#http_req{transport=Transport, raw_host=Host, port=Port,
-		raw_path=Path}, HandlerState) ->
-	Location = websocket_location(Transport:name(), Host, Port, Path),
+websocket_handshake(State=#state{version=0, origin=Origin,
+		challenge=Challenge}, Req=#http_req{transport=Transport,
+		raw_host=Host, port=Port, raw_path=Path}, HandlerState) ->
+	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-Origin">>, Origin}],
+		 {<<"Sec-Websocket-Location">>, Location},
+		 {<<"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()},
 	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()},
 		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 >>) ->
-	websocket_close(State, Req, HandlerState, {normal, closed});
+%% No more data.
 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) ->
-	websocket_frame(State, Req, HandlerState, Data, binary:first(Data)).
-
-%% We do not support any frame type other than 0 yet. Just like the specs.
--spec websocket_frame(#state{}, #http_req{}, any(), binary(), byte()) -> ok.
-websocket_frame(State=#state{eop=EOP}, Req, HandlerState, Data, 0) ->
+%% hixie-76 close frame.
+websocket_data(State=#state{version=0}, Req, HandlerState,
+		<< 255, 0, _Rest/bits >>) ->
+	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, _/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} ->
-			websocket_send(Data, Req2),
+		{reply, Payload, Req2, HandlerState2} ->
+			websocket_send(Payload, State, Req2),
 			NextState(State, Req2, HandlerState2, RemainingData);
 			NextState(State, Req2, HandlerState2, RemainingData);
-		{reply, Data, Req2, HandlerState2, hibernate} ->
-			websocket_send(Data, Req2),
+		{reply, Payload, Req2, HandlerState2, hibernate} ->
+			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.
-websocket_send(Data, #http_req{socket=Socket, transport=Transport}) ->
-	Transport:send(Socket, << 0, Data/binary, 255 >>).
+-spec websocket_send(binary(), #state{}, #http_req{}) -> ok | ignore.
+%% hixie-76 text frame.
+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},
-		HandlerState, Reason) ->
+websocket_close(State=#state{version=0}, Req=#http_req{socket=Socket,
+		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.

+ 47 - 5
test/http_SUITE.erl

@@ -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.
--export([http_200/1, http_404/1, websocket/1]). %% http and https.
+	nc_rand/1, pipeline/1, raw/1, ws0/1, ws8/1]). %% http.
+-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) ->

+ 5 - 3
test/websocket_handler.erl

@@ -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) ->
-	{reply, Data, Req, State}.
+websocket_handle({text, 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}.