Skip to content

Commit

Permalink
Added WebSockets support. Use the {% comet %} tag to enable it.. the …
Browse files Browse the repository at this point in the history
…name of this scomp will be changed...
  • Loading branch information
mworrell committed Jan 25, 2010
1 parent 339b1d7 commit 3170a2b
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 41 deletions.
44 changes: 43 additions & 1 deletion deps/webmachine/src/webmachine_decision_core.erl
Expand Up @@ -182,7 +182,32 @@ decision(v3b8, Rs, Rd) ->
end;
%% "Forbidden?"
decision(v3b7, Rs, Rd) ->
decision_test(resource_call(forbidden, Rs, Rd), true, 403, v3b6);
decision_test(resource_call(forbidden, Rs, Rd), true, 403, v3b6_upgrade);
%% "Upgrade?"
decision(v3b6_upgrade, Rs, Rd) ->
case get_header_val("upgrade", Rd) of
undefined ->
decision(v3b6, Rs, Rd);
UpgradeHdr ->
case get_header_val("connection", Rd) of
undefined ->
decision(v3b6, Rs, Rd);
Connection ->
case string:strip(string:to_lower(Connection)) of
"upgrade" ->
{Choosen, Rs1, Rd1} = choose_upgrade(UpgradeHdr, Rs, Rd),
case Choosen of
none ->
decision(v3b6, Rs1, Rd1);
{_Protocol, UpgradeFunc} ->
%% TODO: log the upgrade action
{upgrade, UpgradeFunc, Rs1, Rd1}
end;
_ ->
decision(v3b6, Rs, Rd)
end
end
end;
%% "Okay Content-* Headers?"
decision(v3b6, Rs, Rd) ->
decision_test(resource_call(valid_content_headers, Rs, Rd), true, v3b5, 501);
Expand Down Expand Up @@ -630,6 +655,23 @@ choose_charset(AccCharHdr, Rs, Rd) ->
end
end.

choose_upgrade(UpgradeHdr, Rs, Rd) ->
{UpgradesProvided, Rs1, Rd1} = resource_call(upgrades_provided, Rs, Rd),
Provided1 = [ {string:to_lower(Prot), Prot, PFun} || {Prot, PFun} <- UpgradesProvided],
Requested = [ string:to_lower(string:strip(Up)) || Up <- string:tokens(UpgradeHdr, ",") ],
{choose_upgrade1(Requested, Provided1), Rs1, Rd1}.

choose_upgrade1([], _) ->
none;
choose_upgrade1([Req|Requested], Provided) ->
case lists:keysearch(Req, 1, Provided) of
false ->
choose_upgrade1(Requested, Provided);
{value, {_, Protocol, UpgradeFun}} ->
{Protocol, UpgradeFun}
end.


variances(Rs, Rd) ->
{ContentTypesProvided, Rs1, Rd1} = resource_call(content_types_provided, Rs, Rd),
Accept = case length(ContentTypesProvided) of
Expand Down
52 changes: 38 additions & 14 deletions deps/webmachine/src/webmachine_mochiweb.erl
Expand Up @@ -19,7 +19,7 @@
-author('Justin Sheehy <justin@basho.com>').
-author('Andy Gross <andy@basho.com>').
-export([start/1, stop/0, loop/1]).
-export([t/0, t2/0]).
-export([t/0, t2/0, t3/0]).

-include("webmachine_logger.hrl").
-include_lib("wm_reqdata.hrl").
Expand Down Expand Up @@ -82,18 +82,19 @@ loop(MochiReq) ->
{ok, Resource} = BootstrapResource:wrap(Mod, ModOpts),
{ok,RD1} = webmachine_request:load_dispatch_data(Bindings,HostTokens,Port,PathTokens,AppRoot,StringPath,ReqDispatch),
{ok,RD2} = webmachine_request:set_metadata('resource_module', Mod, RD1),
try
{_, RsFin, RdFin} = webmachine_decision_core:handle_request(Resource, RD2),
EndTime = now(),
{_, RdResp} = webmachine_request:send_response(RdFin),

%% Log this request
RMod = webmachine_request:get_metadata('resource_module', RdResp),
LogData0 = webmachine_request:log_data(RdResp),
spawn(fun() -> webmachine_decision_core:do_log(LogData0#wm_log_data{resource_module=RMod, end_time=EndTime}) end),

%% Halt the request, cleanup
RsFin:stop()
Result = try
case webmachine_decision_core:handle_request(Resource, RD2) of
{_, RsFin, RdFin} ->
EndTime = now(),
{_, RdResp} = webmachine_request:send_response(RdFin),
LogData0 = webmachine_request:log_data(RdResp),
spawn(fun() -> webmachine_decision_core:do_log(LogData0#wm_log_data{resource_module=Mod, end_time=EndTime}) end),
RsFin:stop(),
ok;
{upgrade, UpgradeFun, RsFin, RdFin} ->
RsFin:stop(),
{upgrade, UpgradeFun, RdFin}
end
catch
error:_ ->
?DBG({error, erlang:get_stacktrace()}),
Expand All @@ -103,7 +104,14 @@ loop(MochiReq) ->
spawn(LogModule, log_access, [webmachine_request:log_data(RD3)]);
_ -> nop
end
end;
end,

%% Optional tail continuation to a function that takes over the request.
%% Used for protocol upgrades.
case Result of
ok -> ok;
{upgrade, Fun, RdUpgrade} -> Mod:Fun(RdUpgrade)
end;
handled ->
nop
end.
Expand Down Expand Up @@ -184,3 +192,19 @@ t2() ->
nil,nil}}}},
loop(MochiReq).


t3() ->
Headers = [
{"Upgrade", "WebSocket"},
{"Connection", "Upgrade"},
{"Host", "example.com"},
{"Origin", "http://example.com"},
{"WebSocket-Protocol", "sample"}
],
MochiReq = {mochiweb_request,undefined,'GET',
"/websocket",
{1,1},
mochiweb_headers:make(Headers)
},
loop(MochiReq).

2 changes: 2 additions & 0 deletions deps/webmachine/src/webmachine_resource.erl
Expand Up @@ -32,6 +32,8 @@ default(is_authorized) ->
true;
default(forbidden) ->
false;
default(upgrades_provided) ->
[];
default(allow_missing_post) ->
false;
default(malformed_request) ->
Expand Down
3 changes: 3 additions & 0 deletions modules/mod_base/dispatch/dispatch
Expand Up @@ -6,6 +6,9 @@
%% Comet connection, used with long polls from the browser.
{comet, ["comet"], resource_comet, []},

%% WebSocket connection.
{websocket, ["websocket"], resource_websocket, []},

%% Postback of events from the browser to the server, dispatched from the postback resource.
{postback, ["postback"], resource_postback, []},

Expand Down
56 changes: 46 additions & 10 deletions modules/mod_base/lib/js/apps/zotonic-1.0.js
Expand Up @@ -5,7 +5,7 @@
@Author: Tim Benniks <tim@timbenniks.nl>
@Author: Marc Worrell <marc@worrell.nl>
Copyright 2009 Tim Benniks, Marc Worrell
Copyright 2009-2010 Tim Benniks, Marc Worrell
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@ Based on nitrogen.js which is copyright 2008-2009 Rusty Klophaus
---------------------------------------------------------- */

var z_ws = false;
var z_comet_is_running = false;
var z_starting_postback = false;
var z_spinner_show_ct = 0;
Expand Down Expand Up @@ -146,7 +147,14 @@ function z_do_postback(triggerID, postback, extraParams)
"&z_pageid=" + urlencode(z_pageid) +
"&" + $.param(extraParams);

z_ajax(params);
if (z_ws)
{
z_ws.send(params);
}
else
{
z_ajax(params);
}
}

function z_ajax(params)
Expand Down Expand Up @@ -186,13 +194,41 @@ function z_ajax(params)
/* Comet long poll
---------------------------------------------------------- */

function z_comet_start()
function z_comet_start(hostname)
{
if (!z_comet_is_running)
{
setTimeout("z_comet();", 2000);
z_comet_is_running = true;
}
if (!z_ws && !z_comet_is_running)
{
if ("WebSocket" in window) {
z_ws = new WebSocket("ws://"+hostname+"/websocket");

z_ws.onopen = function() {
// Web Socket is connected. You can send data by send() method.
};

z_ws.onmessage = function (evt)
{
try
{
eval(evt.data);
z_init_postback_forms();
}
catch (e)
{
$.misc.error("Error evaluating ajax return value: " + data);
$.misc.warn(e);
}
};

z_ws.onclose = function()
{
};
}
else
{
setTimeout("z_comet();", 2000);
z_comet_is_running = true;
}
}
}

function z_comet()
Expand All @@ -211,8 +247,8 @@ function z_comet()
}
catch (e)
{
alert("Error evaluating Comet return value: " + data);
alert(e);
$.misc.error("Error evaluating ajax return value: " + data);
$.misc.warn(e);
}
setTimeout("z_comet();", 1000);
},
Expand Down
2 changes: 1 addition & 1 deletion modules/mod_base/resources/resource_postback.erl
Expand Up @@ -59,7 +59,7 @@ content_types_provided(ReqData, Context) ->
process_post(ReqData, Context) ->
Context1 = ?WM_REQ(ReqData, Context),
Postback = z_context:get_q("postback", Context1),
{EventType, TriggerId, TargetId, Tag, Module} = z_utils:depickle(Postback, Context),
{EventType, TriggerId, TargetId, Tag, Module} = z_utils:depickle(Postback, Context1),

TriggerId1 = case TriggerId of
undefined -> z_context:get_q("z_trigger_id", Context1);
Expand Down
135 changes: 135 additions & 0 deletions modules/mod_base/resources/resource_websocket.erl
@@ -0,0 +1,135 @@
%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2010 Marc Worrell
%% @doc WebSocket connections

%% Copyright 2010 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(resource_websocket).
-author("Marc Worrell <marc@worrell.nl>").

-export([
init/1,
upgrades_provided/2,
websocket_start/1,
loop/3,
send_loop/2
]).

-include_lib("webmachine_resource.hrl").
-include_lib("include/zotonic.hrl").

init([]) -> {ok, []}.

upgrades_provided(ReqData, State) ->
{[
{"WebSocket", websocket_start}
], ReqData, State}.


%% @doc Initiate the websocket connection upgrade
websocket_start(ReqData) ->
Context = z_context:new(ReqData),
Context1 = z_context:ensure_all(Context),
Socket = webmachine_request:socket(ReqData),
Hostname = m_site:get(hostname, Context1),
WebSocketPath = z_dispatcher:url_for(websocket, Context1),
Data = ["HTTP/1.1 101 Web Socket Protocol Handshake", 13, 10,
"Upgrade: WebSocket", 13, 10,
"Connection: Upgrade", 13, 10,
"WebSocket-Origin: http://", Hostname, 13, 10,
"WebSocket-Location: ws://", Hostname, WebSocketPath, 13, 10,
13, 10],
ok = send(Socket, Data),
spawn_link(fun() -> start_send_loop(Socket, Context1) end),
loop(none, Socket, Context1).


%% @doc Start receiving messages from the websocket
loop(Buff, Socket, Context) ->
case gen_tcp:recv(Socket, 0) of
{ok, Received} ->
handle_data(Buff, Received, Socket, Context);
{error, Reason} ->
{error, Reason}
end.

%% @doc Upack any data frames, send them to the handling functions.
handle_data(none, <<0,T/binary>>, Socket, Context) ->
handle_data(<<>>, T, Socket, Context);
handle_data(none, <<>>, Socket, Context) ->
resource_websocket:loop(none, Socket, Context);
handle_data(Msg, <<255,T/binary>>, Socket, Context) ->
handle_message(Msg, Context),
handle_data(none, T, Socket, Context);
handle_data(Msg, <<H,T/binary>>, Socket, Context) ->
handle_data(<<Msg/binary, H>>, T, Socket, Context);
handle_data(Msg, <<>>, Socket, Context) ->
resource_websocket:loop(Msg, Socket, Context).


%% Handle a message from the browser, should contain an url encoded request. Sends result script back to browser.
handle_message(Msg, Context) ->
Qs = mochiweb_util:parse_qs(Msg),
Context1 = z_context:set('q', Qs, Context),
Postback = z_context:get_q("postback", Context1),
{EventType, TriggerId, TargetId, Tag, Module} = z_utils:depickle(Postback, Context1),

TriggerId1 = case TriggerId of
undefined -> proplists:get_q("z_trigger_id", Context1);
_ -> TriggerId
end,

ContextRsc = z_context:set_resource_module(Module, Context1),
EventContext = case EventType of
"submit" ->
case z_validation:validate_query_args(ContextRsc) of
{ok, ContextEval} ->
Module:event({submit, Tag, TriggerId1, TargetId}, ContextEval);
{error, ContextEval} ->
ContextEval
end;
_ ->
Module:event({postback, Tag, TriggerId1, TargetId}, ContextRsc)
end,
Script = iolist_to_binary(z_script:get_script(EventContext)),
z_session_page:add_script(Script, EventContext).


%% @doc Start the loop passing data (scripts) from the page to the browser
start_send_loop(Socket, Context) ->
z_session_page:websocket_attach(self(), Context),
send_loop(Socket, Context).

send_loop(Socket, Context) ->
receive
{send_data, Data} ->
case send(Socket, [0, Data, 255]) of
ok -> resource_websocket:send_loop(Socket, Context);
closed -> closed
end;
_ ->
resource_websocket:send_loop(Socket, Context)
end.


%% @doc Send data to the user agent
send(undefined, _Data) ->
ok;
send(Socket, Data) ->
case gen_tcp:send(Socket, iolist_to_binary(Data)) of
ok -> ok;
{error, closed} -> closed;
_ -> exit(normal)
end.
3 changes: 2 additions & 1 deletion modules/mod_base/scomps/scomp_base_comet.erl
Expand Up @@ -28,4 +28,5 @@ varies(_Params, _Context) -> undefined.
terminate(_State, _Context) -> ok.

render(_Params, _Vars, Context, _State) ->
{ok, z_script:add_script(<<"z_comet_start();">>, Context)}.
Hostname = m_site:get(hostname, Context),
{ok, z_script:add_script(["z_comet_start('",Hostname,"');"], Context)}.

0 comments on commit 3170a2b

Please sign in to comment.