-module(n4u_session). -include_lib("n4u/include/n4u.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). % todo read how this works -export([init/2, finish/2]). -export([ensure_sid/3, session_sid/2, session_sid/4, expired/2, lookup_ets/1, clear/0, clear/1, cookie_expire/1, ttl/0, till/2, new_sid/0, new_cookie_value/1, new_cookie_value/2, session_cookie_name/1, set_session_value/3, set_value/2, invalidate_sessions/0, get_value/2, get_value/3, remove_value/1]). % todo check where used init(State, Ctx) -> case application:get_env(n4u, auto_session, "") of disabled -> {ok, State, Ctx}; _ -> ?MODULE:ensure_sid(State, Ctx, []) end. finish(State, Ctx) -> {ok, State, Ctx}. ensure_sid(State, Ctx, []) -> ensure_sid(State, Ctx, site); ensure_sid(State, Ctx, From) -> Cookie_Name = nitro:to_atom(session_cookie_name(From)), Session_Id = wf:cookie_req(Cookie_Name, Ctx#cx.req), wf:info(?MODULE, "Ensure SID ~p-sid=~p~n", [From, Session_Id]), session_sid(State, Ctx, Session_Id, From). session_sid(SID, Source) -> session_sid([], ?CTX, SID, Source). session_sid(State, Ctx, Session_Id, From) -> wf:info(?MODULE, "Session Init ~p: ~p~n", [From, Session_Id]), Lookup = lookup_ets({Session_Id, <<"auth">>}), New_Till = till(calendar:local_time(), ttl()), Session_Cookie = case Lookup of undefined -> Cookie_Value = case Session_Id of undefined -> case wf:qc(application:get_env(n4u, transfer_session, <<"csid">>), Ctx) of undefined -> new_cookie_value(From); Csid -> new_cookie_value(Csid, From) end; _ -> new_cookie_value(Session_Id, From) end, Cookie = {{Cookie_Value, <<"auth">>}, <<"/">>, os:timestamp(), New_Till, new}, ets:insert(cookies, Cookie), wf:info(?MODULE, "Auth Cookie New: ~p~n", [Cookie]), Cookie; {{Session, Key}, Path, Issued, Till, Status} -> case expired(Issued, Till) of false -> Cookie = {{Session, Key}, Path, Issued, Till, Status}, wf:info(?MODULE, "Auth Cookie Same: ~p~n", [Cookie]), Cookie; true -> Cookie = {{new_cookie_value(From), <<"auth">>}, <<"/">>, os:timestamp(), New_Till, new}, clear(Session), ets:insert(cookies, Cookie), wf:info(?MODULE, "Auth Cookie Expired in Session ~p~n", [Session]), Cookie end; What -> wf:info(?MODULE, "Auth Cookie Error: ~p~n", [What]), What end, {{ID, _}, _, _, _, _} = Session_Cookie, erlang:put(session_id, ID), wf:info(?MODULE, "State: ~p~n", [Session_Cookie]), {ok, State, Ctx#cx{session = Session_Cookie}}. expired(_Issued, Till) -> Till < calendar:local_time(). lookup_ets(Key) -> Res = ets:lookup(cookies, Key), %wf:info(?MODULE, "Lookup ETS: ~p", [{Res, Key}]), case Res of [] -> undefined; [Value] -> Value; Values -> Values end. clear() -> clear(erlang:get(session_id)). clear(Session) -> [ets:delete(cookies, X) || X <- ets:select(cookies, ets:fun2ms(fun(A) when (erlang:element(1, erlang:element(1, A)) == Session) -> erlang:element(1, A) end))]. cookie_expire(Seconds_To_Live) -> Seconds = calendar:datetime_to_gregorian_seconds(calendar:local_time()), DateTime = calendar:gregorian_seconds_to_datetime(Seconds + Seconds_To_Live), cow_date:rfc2109(DateTime). ttl() -> application:get_env(n4u, ttl, 60 * 15). till(Now, TTL) -> calendar:gregorian_seconds_to_datetime( calendar:datetime_to_gregorian_seconds(Now) + TTL). new_sid() -> nitro:hex(binary:part( crypto:mac(application:get_env(n4u, mac_type, hmac), application:get_env(n4u, mac_subtype, sha256), n4u_secret:secret(), erlang:term_to_binary(os:timestamp())), 0, 16)). new_cookie_value(From) -> new_cookie_value(new_sid(), From). new_cookie_value(undefined, From) -> new_cookie_value(new_sid(), From); new_cookie_value(Session_Key, From) -> F = nitro:f("document.cookie='~s=~s; path=/; expires=~s';", [nitro:to_list(session_cookie_name(From)), nitro:to_list(Session_Key), cookie_expire(2147483647)]), wf:info(?MODULE, "Cookie: ~p~n", [F]), nitro:wire(F), % NOTE: Infinity-expire cookie will allow to clean up all session cookies % by request from browser so we don't need to sweep them on server. % Actually we should anyway to cleanup outdated cookies % that will never be requested. Session_Key. session_cookie_name([]) -> session_cookie_name(site); session_cookie_name(From) -> nitro:to_binary([nitro:to_binary(From), <<"-sid">>]). set_session_value(Session, Key, Value) -> Till = till(calendar:local_time(), ttl()), ets:insert(cookies, {{Session, Key}, <<"/">>, os:timestamp(), Till, Value}), Value. set_value(Key, Value) -> New_Till = till(calendar:local_time(), ttl()), ets:insert(cookies, {{erlang:get(session_id), Key}, <<"/">>, os:timestamp(), New_Till, Value}), Value. invalidate_sessions() -> ets:foldl(fun(X, A) -> {Sid, Key} = erlang:element(1, X), ?MODULE:get_value(Sid, Key, undefined), A end, 0, cookies). get_value(Key, Default_Value) -> get_value(erlang:get(session_id), Key, Default_Value). get_value(SID, Key, Default_Value) -> Res = case lookup_ets({SID, Key}) of undefined -> Default_Value; {{SID, Key}, _, Issued, Till, Value} -> case expired(Issued, Till) of false -> Value; true -> ets:delete(cookies, {SID, Key}), Default_Value end end, %wf:info(?MODULE, "Session Lookup Key ~p Value ~p~n", [Key, Res]), Res. remove_value(Key) -> ets:delete(cookies, Key).