|
@@ -0,0 +1,190 @@
|
|
|
|
+-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).
|
|
|
|
+
|