Skip to content

Commit

Permalink
HTML5 Web Sockets support.
Browse files Browse the repository at this point in the history
  • Loading branch information
davide committed Dec 18, 2009
1 parent d63642e commit 7fdc54a
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 7 deletions.
3 changes: 2 additions & 1 deletion src/Makefile
Expand Up @@ -42,7 +42,8 @@ MODULES=yaws \
yaws_sendfile yaws_sendfile_compat \
yaws_sup_restarts \
yaws_stats \
yaws_multipart
yaws_multipart \
yaws_websockets



Expand Down
36 changes: 36 additions & 0 deletions src/yaws_api.erl
Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand Down
26 changes: 20 additions & 6 deletions src/yaws_server.erl
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand Down
122 changes: 122 additions & 0 deletions 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.

157 changes: 157 additions & 0 deletions 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.