Permalink
Browse files

added support for session access from websockets

  • Loading branch information...
ostinelli committed Dec 4, 2011
1 parent 682b8a3 commit dce39c9b6c389066906f865467f2291d6033827e
View
@@ -4,7 +4,7 @@ SRC_DIR:=src
all: compile
-compile:
+compile: clean
@rebar compile
clean:
@@ -14,7 +14,7 @@ clean:
tests: compile
@rebar ct
-debug:
+debug: clean
@if test -f $(REBAR_CONFIG); then mv $(REBAR_CONFIG) $(REBAR_CONFIG).bak; fi;
@echo {erl_opts, [{d, log_debug}]}. > $(REBAR_CONFIG)
@rebar debug_info=true compile
@@ -0,0 +1,110 @@
+% ==========================================================================================================
+% MISULTIN - Websocket Sessions Example.
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
+% All rights reserved.
+%
+% BSD License
+%
+% Redistribution and use in source and binary forms, with or without modification, are permitted provided
+% that the following conditions are met:
+%
+% * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+% following disclaimer.
+% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+% the following disclaimer in the documentation and/or other materials provided with the distribution.
+% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote
+% products derived from this software without specific prior written permission.
+%
+% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+% POSSIBILITY OF SUCH DAMAGE.
+% ==========================================================================================================
+-module(misultin_websocket_sessions_example).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{port, Port}, {loop, fun(Req) -> handle_http(Req, Port) end}, {ws_loop, fun(Ws) -> handle_websocket(Ws) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+% callback on request received
+handle_http(Req, Port) ->
+ % get session info
+ {SessionId, _SessionState} = Req:session(),
+ % save user's peer_addr and a resetted counter as session's state. a more complex state can easily be saved here, such as proplist()
+ Req:save_session_state(SessionId, {Req:get(peer_addr), 1}),
+ % output
+ Req:ok([{"Content-Type", "text/html"}],
+ ["
+ <html>
+ <head>
+ <script type=\"text/javascript\">
+ function addStatus(text){
+ var date = new Date();
+ document.getElementById('status').innerHTML = document.getElementById('status').innerHTML + date + \": \" + text + \"<br>\";
+ }
+ function ready(){
+ var ws;
+ if (\"WebSocket\" in window) {
+ ws = new WebSocket(\"ws://localhost:", erlang:integer_to_list(Port) ,"/service\");
+ } else if (\"MozWebSocket\" in window) {
+ ws = new MozWebSocket(\"ws://localhost:", erlang:integer_to_list(Port) ,"/service\");
+ }
+ if (ws) {
+ // browser supports websockets
+ ws.onopen = function() {
+ // websocket is connected
+ addStatus(\"websocket connected!\");
+ // send hello data to server.
+ ws.send(\"hello server!\");
+ addStatus(\"sent message to server: 'hello server'!\");
+ };
+ ws.onmessage = function (evt) {
+ var receivedMsg = evt.data;
+ addStatus(\"server sent the following: '\" + receivedMsg + \"'\");
+ };
+ ws.onclose = function() {
+ // websocket was closed
+ addStatus(\"websocket was closed\");
+ };
+ } else {
+ // browser does not support websockets
+ addStatus(\"sorry, your browser does not support websockets.\");
+ }
+ }
+ </script>
+ </head>
+ <body onload=\"ready();\">
+ <div id=\"status\"></div>
+ </body>
+ </html>"]).
+
+% callback on received websockets data
+handle_websocket(Ws) ->
+ % get session info
+ {SessionId, {UserPeerAddr, Count}} = Ws:session(),
+ receive
+ {browser, Data} ->
+ Ws:send(["received '", Data, "'"]),
+ handle_websocket(Ws);
+ _Ignore ->
+ handle_websocket(Ws)
+ after 5000 ->
+ % increase pushed counter and save new sessin state
+ Ws:save_session_state(SessionId, {UserPeerAddr, Count + 1}),
+ % build push message
+ Pushmessage = lists:flatten(io_lib:format("pushed ~p time(s) for user with session IP: ~p", [Count, UserPeerAddr])),
+ Ws:send(Pushmessage),
+ handle_websocket(Ws)
+ end.
View
@@ -250,7 +250,7 @@ headers(#c{recv_timeout = RecvTimeout, ws_loop = WsLoop} = C, #req{socket = Sock
end;
{true, Vsn} ->
?LOG_DEBUG("websocket request received", []),
- misultin_websocket:connect(C#c.server_ref, Req#req{headers = Headers, ws_force_ssl = C#c.ws_force_ssl}, #ws{vsn = Vsn, socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, path = Path, ws_autoexit = C#c.ws_autoexit}, WsLoop)
+ misultin_websocket:connect(C#c.server_ref, C#c.sessions_ref, Req#req{headers = Headers, ws_force_ssl = C#c.ws_force_ssl}, #ws{vsn = Vsn, socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, path = Path, ws_autoexit = C#c.ws_autoexit}, WsLoop)
end;
{SocketMode, Sock, _Other} ->
?LOG_WARNING("tcp error treating headers: ~p, send bad request error back", [_Other]),
@@ -614,26 +614,28 @@ socket_loop(#c{compress = Compress} = C, #req{socket = Sock, socket_mode = Socke
socket_loop(C, Req, LoopPid, ReqOptions, AppHeaders, HttpCodeSent, SizeSent);
{CallerPid, {session_cmd, SessionCmd}} ->
?LOG_DEBUG("received a session command: ~p", [SessionCmd]),
- case SessionCmd of
- {session, Cookies} ->
- % start new session process or retrieve exiting one's id
- case misultin_sessions:session(C#c.sessions_ref, Cookies, Req) of
- {error, Reason} ->
- ?LOG_DEBUG("error getting/creating session: ~p", [Reason]),
- misultin_utility:respond(CallerPid, {error, Reason});
- {SessionId, _SessionVars} = SessionInfo ->
- ?LOG_DEBUG("got session info: ~p", [SessionInfo]),
- % respond with session id
- misultin_utility:respond(CallerPid, SessionInfo),
- % add session id cookie header
- socket_loop(C, Req, LoopPid, ReqOptions, [misultin_sessions:set_session_cookie(SessionId)|AppHeaders], HttpCodeSent, SizeSent)
- end;
- {save_session_state, SessionId, SessionState} ->
- % save session state
- Response = misultin_sessions:save_session_state(C#c.sessions_ref, SessionId, SessionState, Req),
- misultin_utility:respond(CallerPid, Response),
- % loop
- socket_loop(C, Req, LoopPid, ReqOptions, AppHeaders, HttpCodeSent, SizeSent)
+ case misultin_utility:get_peer(Req#req.headers, Req#req.peer_addr) of
+ {error, Reason} ->
+ ?LOG_ERROR("error getting remote peer_addr: ~p, cannot get/create session", [Reason]),
+ misultin_utility:respond(CallerPid, {error, {peer_addr, Reason}}),
+ socket_loop(C, Req, LoopPid, ReqOptions, AppHeaders, HttpCodeSent, SizeSent);
+ {ok, PeerAddr} ->
+ case SessionCmd of
+ {session, Cookies} ->
+ % start new session process or retrieve exiting one's id
+ {SessionId, _SessionVars} = SessionInfo = misultin_sessions:session(C#c.sessions_ref, Cookies, PeerAddr),
+ ?LOG_DEBUG("got session info: ~p", [SessionInfo]),
+ % respond with session id
+ misultin_utility:respond(CallerPid, SessionInfo),
+ % add session id cookie header
+ socket_loop(C, Req, LoopPid, ReqOptions, [misultin_sessions:set_session_cookie(SessionId)|AppHeaders], HttpCodeSent, SizeSent);
+ {save_session_state, SessionId, SessionState} ->
+ % save session state
+ Response = misultin_sessions:save_session_state(C#c.sessions_ref, SessionId, SessionState, PeerAddr),
+ misultin_utility:respond(CallerPid, Response),
+ % loop
+ socket_loop(C, Req, LoopPid, ReqOptions, AppHeaders, HttpCodeSent, SizeSent)
+ end
end;
{CallerPid, body_recv} ->
?LOG_DEBUG("received a request to manually read the body",[]),
View
@@ -329,7 +329,6 @@ resource(Options, ReqT) when is_list(Options) ->
% ============================ /\ API ======================================================================
-
% ============================ \/ INTERNAL FUNCTIONS =======================================================
% Clean URI.
View
@@ -36,7 +36,7 @@
% API
-export([start_link/1]).
--export([set_session_cookie/1, session/3, save_session_state/4]).
+-export([set_session_cookie/1, session/3, session/4, save_session_state/4]).
% macros
-define(TABLE_SESSIONS, misultin_table_sessions).
@@ -68,17 +68,20 @@ start_link(Options) when is_tuple(Options) ->
set_session_cookie(SessionId) ->
misultin_cookies:set_cookie(?SESSION_HEADER_NAME, SessionId, [{max_age, 365*24*3600}]).
-% retrieve session info and start a session if necessary.
--spec session(ServerRef::pid(), Cookies::gen_proplist(), Req::#req{}) -> {SessionId::string(), SessionState::term()} | {error, Reason::term()}.
-session(ServerRef, Cookies, Req) ->
+% retrieve session info or start a session if necessary.
+-spec session(ServerRef::pid(), Cookies::gen_proplist(), PeerAddr::inet:ip_address()) -> {SessionId::string(), SessionState::term()}.
+-spec session(ServerRef::pid(), Cookies::gen_proplist(), PeerAddr::inet:ip_address(), CreateIfNonExistant::boolean()) -> {SessionId::string(), SessionState::term()} | {error, Reason::term()}.
+session(ServerRef, Cookies, PeerAddr) ->
+ session(ServerRef, Cookies, PeerAddr, true).
+session(ServerRef, Cookies, PeerAddr, CreateIfNonExistant) ->
% extract session id
SessionId = misultin_utility:get_key_value(?SESSION_HEADER_NAME, Cookies),
- gen_server:call(ServerRef, {session, SessionId, Req}).
+ gen_server:call(ServerRef, {session, SessionId, PeerAddr, CreateIfNonExistant}).
% save a session state
--spec save_session_state(ServerRef::pid(), SessionId::string(), SessionState::term(), Req::#req{}) -> ok | {error, Reason::term()}.
-save_session_state(ServerRef, SessionId, SessionState, Req) ->
- gen_server:call(ServerRef, {save_session_state, SessionId, SessionState, Req}).
+-spec save_session_state(ServerRef::pid(), SessionId::string(), SessionState::term(), PeerAddr::inet:ip_address()) -> ok | {error, Reason::term()}.
+save_session_state(ServerRef, SessionId, SessionState, PeerAddr) ->
+ gen_server:call(ServerRef, {save_session_state, SessionId, SessionState, PeerAddr}).
% ============================ /\ API ======================================================================
@@ -108,43 +111,57 @@ init({MainSupRef, SessionsExpireSec}) ->
% ----------------------------------------------------------------------------------------------------------
% start a session
-handle_call({session, undefined, Req}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
- ?LOG_DEBUG("generate a new session",[]),
- NewSessionId = i_start_session(TableSessions, TableDateRef, Req),
- {reply, {NewSessionId, ?DEFAULT_SESSION_STATE}, State};
-handle_call({session, SessionId, Req}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
+handle_call({session, undefined, PeerAddr, CreateIfNonExistant}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
+ case CreateIfNonExistant of
+ true ->
+ ?LOG_DEBUG("generate a new session",[]),
+ NewSessionId = i_start_session(TableSessions, TableDateRef, PeerAddr),
+ {reply, {NewSessionId, ?DEFAULT_SESSION_STATE}, State};
+ _ ->
+ ?LOG_DEBUG("do not generate a new session, report error",[]),
+ {reply, {error, non_existent_session}, State}
+ end;
+
+handle_call({session, SessionId, PeerAddr, CreateIfNonExistant}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
?LOG_DEBUG("starting or retrieving session ~p", [SessionId]),
case ets:lookup(TableSessions, SessionId) of
- [] ->
+ [] when CreateIfNonExistant =:= true ->
?LOG_DEBUG("session with id ~p could not be found, start new one", [SessionId]),
- NewSessionId = i_start_session(TableSessions, TableDateRef, Req),
+ NewSessionId = i_start_session(TableSessions, TableDateRef, PeerAddr),
{reply, {NewSessionId, ?DEFAULT_SESSION_STATE}, State};
+ [] ->
+ ?LOG_DEBUG("session with id ~p could not be found, report error", [SessionId]),
+ {reply, {error, non_existent_session}, State};
[{SessionId, VerifyInfo, _TSLastAccess, SessionState}] ->
?LOG_DEBUG("session id ~p found in table with verify info: ~p, start validity check", [SessionId, VerifyInfo]),
- case is_session_valid(VerifyInfo, Req) of
+ case is_session_valid(VerifyInfo, PeerAddr) of
true ->
?LOG_DEBUG("session id ~p is valid, return id and state", [SessionId]),
% update session access data
ets:insert(TableSessions, {SessionId, VerifyInfo, misultin_server:get_timestamp(TableDateRef), SessionState}),
{reply, {SessionId, SessionState}, State};
- false ->
+ false when CreateIfNonExistant =:= true ->
?LOG_DEBUG("session is not valid, killing session and generate new",[]),
i_remove_session(TableSessions, SessionId),
- NewSessionId = i_start_session(TableSessions, TableDateRef, Req),
- {reply, {NewSessionId, ?DEFAULT_SESSION_STATE}, State}
+ NewSessionId = i_start_session(TableSessions, TableDateRef, PeerAddr),
+ {reply, {NewSessionId, ?DEFAULT_SESSION_STATE}, State};
+ false ->
+ ?LOG_DEBUG("session is not valid, killing session",[]),
+ i_remove_session(TableSessions, SessionId),
+ {reply, {error, non_existent_session}, State}
end
end;
% save session state
-handle_call({save_session_state, SessionId, SessionState, Req}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
+handle_call({save_session_state, SessionId, SessionState, PeerAddr}, _From, #state{table_sessions = TableSessions, table_date_ref = TableDateRef} = State) ->
?LOG_DEBUG("saving session ~p state ~p", [SessionId, SessionState]),
case ets:lookup(TableSessions, SessionId) of
[] ->
?LOG_DEBUG("session with id ~p could not be found, could not save", [SessionId]),
{reply, {error, invalid_session_id}, State};
[{SessionId, VerifyInfo, _TSLastAccess, _OldSessionState}] ->
?LOG_DEBUG("session id ~p found in table with verify info: ~p, start validity check", [SessionId, VerifyInfo]),
- case is_session_valid(VerifyInfo, Req) of
+ case is_session_valid(VerifyInfo, PeerAddr) of
true ->
?LOG_DEBUG("session id ~p is valid, set new state ~p", [SessionId, SessionState]),
ets:insert(TableSessions, {SessionId, VerifyInfo, misultin_server:get_timestamp(TableDateRef), SessionState}),
@@ -236,10 +253,10 @@ code_change(_OldVsn, State, _Extra) ->
% ============================ \/ INTERNAL FUNCTIONS =======================================================
% start a new session
--spec i_start_session(TableSessions::ets:tid(), TableDateRef::ets:tid(), Req::#req{}) -> SessionId::string() | {error, Reason::term()}.
-i_start_session(TableSessions, TableDateRef, Req) ->
+-spec i_start_session(TableSessions::ets:tid(), TableDateRef::ets:tid(), PeerAddr::inet:ip_address()) -> SessionId::string() | {error, Reason::term()}.
+i_start_session(TableSessions, TableDateRef, PeerAddr) ->
% build verify info
- case get_ip_domain(Req#req.peer_addr) of
+ case get_ip_domain(PeerAddr) of
{error, _Error} ->
?LOG_DEBUG("could not start session: ~p", [_Error]),
{error, could_not_start_session};
@@ -273,9 +290,9 @@ get_ip_domain({A, B, C, D, E, F, G, _}) -> {A, B, C, D, E, F, G}; % ipv6
get_ip_domain(_) -> {error, undefined_ip}.
% is session valid
--spec is_session_valid(VerifyInfo::term(), Req::#req{}) -> boolean().
-is_session_valid({VerifyDomain}, Req) ->
- get_ip_domain(Req#req.peer_addr) =:= VerifyDomain.
+-spec is_session_valid(VerifyInfo::term(), PeerAddr::inet:ip_address()) -> boolean().
+is_session_valid({VerifyDomain}, PeerAddr) ->
+ get_ip_domain(PeerAddr) =:= VerifyDomain.
% expire sessions
-spec expire_sessions(TableSessions::ets:tid(), TableDateRef::ets:tid(), SessionsExpireSec::non_neg_integer()) -> true.
Oops, something went wrong.

0 comments on commit dce39c9

Please sign in to comment.