Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

355 lines (291 sloc) 11.633 kB
%%%----------------------------------------------------------------------
%%% File : yaws_session_server.erl
%%% Author : Claes Wikstrom <klacke@hyber.org>
%%% Purpose : maintain state for cookie sessions
%%% Created : 17 Sep 2002 by Claes Wikstrom <klacke@hyber.org>
%%%----------------------------------------------------------------------
-module(yaws_session_server).
-author('klacke@hyber.org').
-behaviour(gen_server).
%% External exports
-export([start_link/0, start/0, stop/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-include("../include/yaws_api.hrl").
-include("../include/yaws.hrl").
-export([new_session/1,new_session/2,new_session/3,new_session/4,
cookieval_to_opaque/1,
print_sessions/0,
replace_session/2,
delete_session/1]).
%% Default ETS backend callbacks
-export ([init_backend/1, stop_backend/0,
list/0, lookup/1, insert/1, delete/1,
traverse/1, cleanup/0]).
%% Utility functions for callbacks
-export ([has_timedout/2, report_timedout_sess/1, cookie/1]).
-define(TTL, (30 * 60)). % 30 minutes
-record(ysession,
{cookie, %% the cookie assigned to the session
to, %% greg secs untill timeout death
ttl, %% default time to live
starttime, %% When calendar:local_time() did sess start
cleanup, %% PID to notify of session end
opaque %% any data the user supplies
}).
-record(state,
{backend %% storage engine module
}).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start_link() ->
Backend = get_yaws_session_server_backend(),
gen_server:start_link({local, yaws_session_server},
yaws_session_server, Backend, []).
start() ->
Backend = get_yaws_session_server_backend(),
gen_server:start({local, yaws_session_server},
yaws_session_server, Backend, []).
stop() ->
gen_server:call(?MODULE, stop, infinity).
%% We are bending over here in our pursuit of finding a
%% proper ysession_server backend.
get_yaws_session_server_backend() ->
#gconf{ysession_mod = DefaultBackend} = #gconf{},
case yaws_server:getconf() of
{ok, #gconf{ysession_mod = Backend}, _} -> Backend;
_ ->
case application:get_env(yaws, embedded) of
{ok, true} ->
case application:get_env(yaws, embedded_conf) of
{ok, L} when is_list(L) ->
case lists:keysearch(gc, 1, L) of
{value, {_, #gconf{ysession_mod = Backend}}} ->
Backend;
_ ->
DefaultBackend
end;
_ ->
DefaultBackend
end;
_ ->
DefaultBackend
end
end.
%% will return a new cookie as a string
new_session(Opaque) ->
Call = {new_session, Opaque, ?TTL, undefined, undefined},
gen_server:call(?MODULE, Call, infinity).
new_session(Opaque, TTL) ->
Call = {new_session, Opaque, TTL, undefined, undefined},
gen_server:call(?MODULE, Call, infinity).
new_session(Opaque, TTL, Cleanup) ->
Call = {new_session, Opaque, TTL, Cleanup, undefined},
gen_server:call(?MODULE, Call, infinity).
new_session(Opaque, TTL, Cleanup, Cookie) ->
Call = {new_session, Opaque, TTL, Cleanup, Cookie},
gen_server:call(?MODULE, Call, infinity).
cookieval_to_opaque(Cookie) ->
gen_server:call(?MODULE, {cookieval_to_opaque, Cookie}, infinity).
print_sessions() ->
gen_server:cast(?MODULE, print_sessions).
replace_session(Cookie, NewOpaque) ->
gen_server:call(?MODULE, {replace_session, Cookie, NewOpaque}, infinity).
delete_session(CookieVal) ->
gen_server:call(?MODULE, {delete_session, CookieVal}, infinity).
%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------
%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%%----------------------------------------------------------------------
init(Backend) ->
Backend:init_backend(record_info(fields, ysession)),
start_long_timer(),
{ok, #state{backend = Backend}, to()}.
%%----------------------------------------------------------------------
%% Func: handle_call/3
%% Returns: {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} | (terminate/2 is called)
%% {stop, Reason, State} (terminate/2 is called)
%%----------------------------------------------------------------------
handle_call({new_session, Opaque, undefined, Cleanup, Cookie}, From, State) ->
handle_call({new_session, Opaque, ?TTL, Cleanup, Cookie}, From, State);
handle_call({new_session, Opaque, TTL, Cleanup, undefined}, From, State) ->
N = bin2int(crypto:rand_bytes(16)),
Cookie = atom_to_list(node()) ++ [$-|integer_to_list(N)],
handle_call({new_session, Opaque, TTL, Cleanup, Cookie}, From, State);
handle_call({new_session, Opaque, TTL, Cleanup, Cookie}, _From, State) ->
Now = gnow(),
TS = calendar:local_time(),
NS = #ysession{cookie = Cookie,
starttime = TS,
opaque = Opaque,
to = Now + TTL,
ttl = TTL,
cleanup = Cleanup},
Backend = State#state.backend,
true = Backend:insert(NS),
{reply, Cookie, State, to()};
handle_call({cookieval_to_opaque, Cookie}, _From, State) ->
Backend = State#state.backend,
Result =
case Backend:lookup(Cookie) of
[Y] ->
Y2 = Y#ysession{to = gnow() + Y#ysession.ttl},
Backend:insert(Y2),
{ok, Y#ysession.opaque};
[] ->
{error, no_session}
end,
{reply, Result, State, to()};
handle_call({replace_session, Cookie, NewOpaque}, _From, State) ->
Backend = State#state.backend,
Result =
case Backend:lookup(Cookie) of
[Y] ->
Y2 = Y#ysession{to = gnow() + Y#ysession.ttl,
opaque = NewOpaque},
Backend:insert(Y2);
[] ->
error
end,
{reply, Result, State, to()};
handle_call({delete_session, CookieVal}, _From, State) ->
Backend = State#state.backend,
Result =
case Backend:lookup(CookieVal) of
[Y] ->
Backend:delete(CookieVal),
report_deleted_sess(Y);
[] ->
true
end,
{reply, Result, State, to()};
handle_call(stop, From, State) ->
gen_server:reply(From, ok),
{stop, normal, State}.
%%----------------------------------------------------------------------
%% Func: handle_cast/2
%% Returns: {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State} (terminate/2 is called)
%%----------------------------------------------------------------------
handle_cast(print_session, #state{backend = Backend} = State) ->
Ss = Backend:list(Backend),
io:format("** ~p sessions active ~n~n", [length(Ss)]),
N = gnow(),
lists:foreach(fun(S) ->
io:format("Cookie ~p ~n", [S#ysession.cookie]),
io:format("Start ~p ~n", [S#ysession.starttime]),
io:format("TTL ~p secs~n", [S#ysession.to - N]),
io:format("Opaque ~p ~n~n~n", [S#ysession.opaque]),
ok
end, Ss),
{noreply, State, to()};
handle_cast(_Msg, State) ->
{noreply, State, to()}.
%%----------------------------------------------------------------------
%% Func: handle_info/2
%% Returns: {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State} (terminate/2 is called)
%%----------------------------------------------------------------------
handle_info(timeout, #state{backend = Backend} = State) ->
Backend:traverse(gnow()),
{noreply, State, to()};
handle_info(long_timeout, #state{backend = Backend} = State) ->
Backend:traverse(gnow()),
start_long_timer(),
{noreply, State, to()}.
%%----------------------------------------------------------------------
%% Func: terminate/2
%% Purpose: Shutdown the server
%% Returns: any (ignored by gen_server)
%%----------------------------------------------------------------------
terminate(_Reason, #state{backend = Backend}) ->
Backend:stop_backend(),
ok.
%%----------------------------------------------------------------------
%% Func: code_change/3
%% Purpose: Handle upgrade
%% Returns: new State data
%%----------------------------------------------------------------------
code_change(_OldVsn, Data, _Extra) ->
{ok, Data}.
%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
bin2int(Bin) ->
lists:foldl(fun(N, Acc) -> Acc * 256 + N end, 0, binary_to_list(Bin)).
%% timeout once every hour even if the server handles traffic all the time.
start_long_timer() ->
erlang:send_after(long_to(), self(), long_timeout).
long_to() ->
60 * 60 * 1000.
%% timeout if the server is idle for more than 2 minutes.
to() ->
2 * 60 * 1000.
gnow() ->
calendar:datetime_to_gregorian_seconds(
calendar:local_time()).
send_cleanup_message(Sess,Msg) ->
case Sess#ysession.cleanup of
undefined ->
nocleanup;
Pid ->
Pid ! Msg
end.
report_timedout_sess(S) ->
send_cleanup_message(S,{yaws_session_end,timeout,
S#ysession.cookie, S#ysession.opaque}).
report_deleted_sess(S) ->
send_cleanup_message(S,{yaws_session_end,normal,
S#ysession.cookie, S#ysession.opaque}).
has_timedout(Y, Time) ->
Y#ysession.to =< Time.
cookie(Y) ->
Y#ysession.cookie.
%% Backend callbacks (ETS as default)
init_backend (_) ->
ets:new(?MODULE, [set, named_table, public, {keypos, 2}]).
stop_backend() ->
ok.
lookup(Key) ->
ets:lookup(?MODULE, Key).
insert(Session) ->
ets:insert(?MODULE, Session).
list() ->
ets:tab2list(?MODULE).
delete(Key) ->
ets:delete(?MODULE, Key).
cleanup() ->
ets:delete_all_objects(?MODULE).
traverse(N) ->
traverse(N, ets:first(?MODULE)).
traverse(_N, '$end_of_table') ->
ok;
traverse(N, Key) ->
case lookup(Key) of
[Y] ->
case has_timedout(Y, N) of
false ->
traverse(N, ets:next(?MODULE, Key));
true ->
report_timedout_sess(Y),
Next = ets:next(?MODULE, Key),
delete(Key),
traverse(N, Next)
end;
[] ->
traverse(N, ets:next(?MODULE, Key))
end.
Jump to Line
Something went wrong with that request. Please try again.