Permalink
Browse files

HTML5 Web Sockets support.

  • Loading branch information...
1 parent d63642e commit 7fdc54ad5b063cd8395e07dd95a7f8b4a383ebf3 @davide davide committed Dec 18, 2009
Showing with 337 additions and 7 deletions.
  1. +2 −1 src/Makefile
  2. +36 −0 src/yaws_api.erl
  3. +20 −6 src/yaws_server.erl
  4. +122 −0 src/yaws_websockets.erl
  5. +157 −0 www/websockets_example.yaws
View
3 src/Makefile
@@ -42,7 +42,8 @@ MODULES=yaws \
yaws_sendfile yaws_sendfile_compat \
yaws_sup_restarts \
yaws_stats \
- yaws_multipart
+ yaws_multipart \
+ yaws_websockets
View
36 src/yaws_api.erl
@@ -36,6 +36,8 @@
stream_chunk_end/1]).
-export([stream_process_deliver/2, stream_process_deliver_chunk/2,
stream_process_deliver_final_chunk/2, stream_process_end/2]).
+-export([websocket_send/2, websocket_receive/1,
+ websocket_unframe_data/1, websocket_setopts/2]).
-export([new_cookie_session/1, new_cookie_session/2, new_cookie_session/3,
cookieval_to_opaque/1, request_url/1,
print_cookie_sessions/0,
@@ -866,6 +868,40 @@ stream_process_end(Sock, YawsPid) ->
gen_tcp:controlling_process(Sock, YawsPid),
YawsPid ! endofstreamcontent.
+
+websocket_send(Socket, IoList) ->
+ DataFrame = [0, IoList, 255],
+ case Socket of
+ {sslsocket,_,_} ->
+ ssl:send(Socket, DataFrame);
+ _ ->
+ gen_tcp:send(Socket, DataFrame)
+ end.
+
+websocket_receive(Socket) ->
+ R = case Socket of
+ {sslsocket,_,_} ->
+ ssl:recv(Socket, 0);
+ _ ->
+ gen_tcp:recv(Socket, 0)
+ end,
+ case R of
+ {ok, DataFrames} ->
+ ReceivedMsgs = yaws_websockets:unframe_all(DataFrames, []),
+ {ok, ReceivedMsgs};
+ _ -> R
+ end.
+
+websocket_unframe_data(DataFrameBin) ->
+ {ok, Msg, <<>>} = yaws_websockets:unframe_one(DataFrameBin),
+ Msg.
+
+websocket_setopts({sslsocket,_,_}=Socket, Opts) ->
+ ssl:setopts(Socket, Opts);
+websocket_setopts(Socket, Opts) ->
+ inet:setopts(Socket, Opts).
+
+
%% Return new cookie string
new_cookie_session(Opaque) ->
yaws_session_server:new_session(Opaque).
View
26 src/yaws_server.erl
@@ -922,12 +922,16 @@ acceptor0(GS, Top) ->
ok
end,
Res = (catch aloop(Client, GS, 0)),
- if
- GS#gs.ssl == nossl ->
- gen_tcp:close(Client);
- GS#gs.ssl == ssl ->
- ssl:close(Client)
- end,
+ case yaws:outh_get_doclose() of
+ false -> ok;
+ true ->
+ if
+ GS#gs.ssl == nossl ->
+ gen_tcp:close(Client);
+ GS#gs.ssl == ssl ->
+ ssl:close(Client)
+ end
+ end,
case Res of
{ok, Int} when is_integer(Int) ->
Top ! {self(), done_client, Int};
@@ -2347,6 +2351,12 @@ deliver_dyn_part(CliSock, % essential params
Priv = deliver_accumulated(Arg, CliSock,
no, undefined, stream),
wait_for_streamcontent_pid(Priv, CliSock, Pid);
+ {websocket, OwnerPid, SocketMode} ->
+ %% The handshake passes control over the socket to OwnerPid
+ %% and terminates the Yaws worker!
+ yaws_websockets:handshake(Arg, OwnerPid, SocketMode)
+ %% this point is never reached
+ ;
_ ->
DeliverCont(Arg)
end.
@@ -2767,6 +2777,10 @@ handle_out_reply({streamcontent_from_pid, MimeType, Pid},
yaws:outh_set_content_type(MimeType),
{streamcontent_from_pid, MimeType, Pid};
+handle_out_reply({websocket, _OwnerPid, _SocketMode}=Reply,
+ _LineNo,_YawsFile, _UT, _ARG) ->
+ Reply;
+
handle_out_reply({header, H}, _LineNo, _YawsFile, _UT, _ARG) ->
yaws:accumulate_header(H);
View
122 src/yaws_websockets.erl
@@ -0,0 +1,122 @@
+%%%----------------------------------------------------------------------
+%%% File : yaws_websockets.erl
+%%% Author : Davide Marquês <nesrait@gmail.com>
+%%% Purpose :
+%%% Created : 18 Dec 2009 by Davide Marquês <nesrait@gmail.com>
+%%% Modified:
+%%%----------------------------------------------------------------------
+
+-module(yaws_websockets).
+-author('nesrait@gmail.com').
+
+-include("../include/yaws.hrl").
+-include("../include/yaws_api.hrl").
+-include("yaws_debug.hrl").
+
+-include_lib("kernel/include/file.hrl").
+-export([handshake/3, unframe_one/1, unframe_all/2]).
+
+handshake(Arg, ContentPid, SocketMode) ->
+ CliSock = Arg#arg.clisock,
+ case get_origin_header(Arg#arg.headers) of
+ undefined ->
+ %% Yaws will take care of closing the socket
+ ContentPid ! discard;
+ Origin ->
+ Host = (Arg#arg.headers)#headers.host,
+ {abs_path, Path} = (Arg#arg.req)#http_request.path,
+ %% TODO: Support for wss://
+ WebSocketLocation = "ws://" ++ Host ++ Path,
+ Handshake =
+ ["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
+ "Upgrade: WebSocket\r\n",
+ "Connection: Upgrade\r\n",
+ "WebSocket-Origin: ", Origin, "\r\n",
+ "WebSocket-Location: ", WebSocketLocation, "\r\n",
+ "\r\n"],
+ SC = get(sc),
+ case SC#sconf.ssl of
+ undefined ->
+ gen_tcp:send(CliSock, Handshake),
+ inet:setopts(CliSock, [{packet, raw}, {active, SocketMode}]),
+ TakeOverResult =
+ gen_tcp:controlling_process(CliSock, ContentPid);
+ _ ->
+ ssl:send(CliSock, Handshake),
+ ssl:setopts(CliSock, [{packet, raw}, {active, SocketMode}]),
+ TakeOverResult =
+ ssl:controlling_process(CliSock, ContentPid)
+ end,
+ case TakeOverResult of
+ ok ->
+ %% Make sure that Yaws doesn't close the socket!
+ put(outh, (get(outh))#outh{doclose = false}),
+ ContentPid ! {ok, CliSock};
+ {error, Reason} ->
+ ContentPid ! discard,
+ exit({websocket, Reason})
+ end
+ end,
+ exit(normal).
+
+
+%% This should take care of all the Data Framing scenarios specified in
+%% http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-66#page-26
+unframe_one(DataFrames) ->
+ <<Type, _/bitstring>> = DataFrames,
+ case Type of
+ T when (T =< 127) ->
+ %% {ok, FF_Ended_Frame} = re:compile("^.(.*)\\xFF(.*?)", [ungreedy]),
+ FF_Ended_Frame = {re_pattern,2,0,
+ <<69,82,67,80,71,0,0,0,16,2,0,0,5,0,0,0,2,0,0,0,0,0,255,2,40,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,93,0,27,25,12,94,0,7,0,1,57,
+ 12,84,0,7,27,255,94,0,7,0,2,56,12,84,0,7,84,0,27,0>>},
+ {match, [Data, NextFrame]} =
+ re:run(DataFrames, FF_Ended_Frame,
+ [{capture, all_but_first, binary}]),
+ {ok, Data, NextFrame};
+
+ _ -> %% Type band 16#80 =:= 16#80
+ {Length, LenBytes} = unpack_length(DataFrames, 0, 0),
+ <<_, _:LenBytes/bytes, Data:Length/bytes,
+ NextFrame/bitstring>> = DataFrames,
+ {ok, Data, NextFrame}
+ end.
+
+unframe_all(<<>>, Acc) ->
+ lists:reverse(Acc);
+unframe_all(DataFramesBin, Acc) ->
+ {ok, Msg, Rem} = unframe_one(DataFramesBin),
+ unframe_all(Rem, [Msg|Acc]).
+
+
+%% Internal functions
+get_origin_header(#headers{other=L}) ->
+ lists:foldl(fun({http_header,_,K0,_,V}, undefined) ->
+ K = case is_atom(K0) of
+ true ->
+ atom_to_list(K0);
+ false ->
+ K0
+ end,
+ case string:to_lower(K) of
+ "origin" ->
+ V;
+ _ ->
+ undefined
+ end;
+ (_, Acc) ->
+ Acc
+ end, undefined, L).
+
+unpack_length(Binary, LenBytes, Length) ->
+ <<_, _:LenBytes/bytes, B, _/bitstring>> = Binary,
+ B_v = B band 16#7F,
+ NewLength = (Length * 128) + B_v,
+ case B band 16#80 of
+ 16#80 ->
+ unpack_length(Binary, LenBytes + 1, NewLength);
+ 0 ->
+ {NewLength, LenBytes + 1}
+ end.
+
View
157 www/websockets_example.yaws
@@ -0,0 +1,157 @@
+
+<erl>
+
+out(A) ->
+ case get_upgrade_header(A#arg.headers) of
+ undefined ->
+ serve_html_page(A);
+ "WebSocket" ->
+ WebSocketOwner = spawn(fun() -> websocket_owner() end),
+ {websocket, WebSocketOwner, passive}
+ end.
+
+
+websocket_owner() ->
+ receive
+ {ok, WebSocket} ->
+ %% This is how we read messages (plural!!) from websockets on passive mode
+ case yaws_api:websocket_receive(WebSocket) of
+ {error,closed} ->
+ io:format("The websocket got disconnected right from the start. "
+ "This wasn't supposed to happen!!~n");
+ {ok, Messages} ->
+ case Messages of
+ [<<"client-connected">>] ->
+ yaws_api:websocket_setopts(WebSocket, [{active, true}]),
+ echo_server(WebSocket);
+ Other ->
+ io:format("websocket_owner got: ~p. Terminating~n", [Other])
+ end
+ end;
+ _ -> ok
+ end.
+
+
+echo_server(WebSocket) ->
+ receive
+ {tcp, WebSocket, DataFrame} ->
+ Data = yaws_api:websocket_unframe_data(DataFrame),
+ io:format("Got data from Websocket: ~p~n", [Data]),
+ yaws_api:websocket_send(WebSocket, Data),
+ echo_server(WebSocket);
+ {tcp_closed, WebSocket} ->
+ io:format("Websocket closed. Terminating echo_server...~n");
+ Any ->
+ io:format("echo_server received msg:~p~n", [Any]),
+ echo_server(WebSocket)
+ end.
+
+get_upgrade_header(#headers{other=L}) ->
+ lists:foldl(fun({http_header,_,K0,_,V}, undefined) ->
+ K = case is_atom(K0) of
+ true ->
+ atom_to_list(K0);
+ false ->
+ K0
+ end,
+ case string:to_lower(K) of
+ "upgrade" ->
+ V;
+ _ ->
+ undefined
+ end;
+ (_, Acc) ->
+ Acc
+ end, undefined, L).
+
+serve_html_page(A) ->
+ Host = (A#arg.headers)#headers.host,
+ {abs_path, Path} = (A#arg.req)#http_request.path,
+ WebSocketLocation = Host ++ Path,
+ io:format("WebSocketLocation: ~p ~n", [WebSocketLocation]),
+ Body = html_body(WebSocketLocation),
+ {content, "text/html", Body}.
+
+%% this html was copied from the basic example in
+%% http://github.com/davebryson/erlang_websocket/
+html_body(WebSocketLocation) ->
+"<html>
+<head>
+ <title>Basic WebSocket Example</title>
+ <script type=\"text/javascript\">
+ if (!window.WebSocket)
+ alert(\"WebSocket not supported by this browser\");
+
+ // Get an Element
+ function $() { return document.getElementById(arguments[0]); }
+ // Get the value of an Element
+ function $F() { return document.getElementById(arguments[0]).value; }
+
+ var client = {
+ connect: function(){
+ this._ws=new WebSocket(\"ws://" ++ WebSocketLocation ++ "\");
+ this._ws.onopen=this._onopen;
+ this._ws.onmessage=this._onmessage;
+ this._ws.onclose=this._onclose;
+ },
+ _onopen: function(){
+ $('connect').className='hidden';
+ $('connected').className='';
+ $('phrase').focus();
+ client._send('client-connected');
+ },
+ _send: function(message){
+ if (this._ws)
+ this._ws.send(message);
+ },
+ chat: function(text) {
+ if (text != null && text.length>0 )
+ client._send(text);
+ },
+ _onmessage: function(m) {
+ if (m.data){
+ var text = m.data;
+ var msg=$('msgs');
+ var spanText = document.createElement('span');
+ spanText.className='text';
+ spanText.innerHTML=text;
+ var lineBreak = document.createElement('br');
+ msg.appendChild(spanText);
+ msg.appendChild(lineBreak);
+ msg.scrollTop = msg.scrollHeight - msg.clientHeight;
+ }
+ },
+ _onclose: function(m) {
+ this._ws=null;
+ $('connect').className='';
+ $('connected').className='hidden';
+ $('msg').innerHTML='';
+ }
+ };
+ </script>
+ <style type='text/css'>
+ div.hidden { display: none; }
+ </style>
+
+</head>
+<body>
+ <h1>Basic Echo Example</h1>
+ <div id=\"msgs\"></div>
+ <div id=\"connect\">
+ <input id='cA' class='button' type='submit' name='connect' value='Connect'/>
+ </div>
+ <br/>
+ <div id=\"connected\" class=\"hidden\">
+ Say Something:&nbsp;<input id='phrase' type='text'/>
+ <input id='sendB' class='button' type='submit' name='connect' value='Send'/>
+ </div>
+
+ <script type='text/javascript'>
+ $('cA').onclick = function(event) { client.connect(); return false; };
+ $('sendB').onclick = function(event) { client.chat($F('phrase')); $('phrase').value=''; return false; };
+ </script>
+ </body>
+</html>".
+
+</erl>
+

0 comments on commit 7fdc54a

Please sign in to comment.