Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

355 lines (291 sloc) 11.348 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.