A library to provide sessions to a cowboy application.
Adding a visit counter to cowboy's hello_world example handler:
handle(Req, State) ->
{Count, Req2} = cowboy_sessions:session_data(Req, count),
Count2 = case Count of
undefined -> 1;
_ -> Count + 1
end,
Req3 = cowboy_sessions:update_session(Req2, count, Count2),
Hello = io_lib:format("Hello, your visit count is: ~p.", [Count2]),
{ok, Req4} = cowboy_req:reply(200, [
{<<"content-type">>, <<"text/plain">>}
], Hello, Req3),
{ok, Req4, State}.
-spec session_id(cowboy_req:req() -> {binary() | undefined, cowboy_req:req()})
Given a cowboy request record, return the session ID (a binary string) on the request if present, or the atom undefined
if the request doesn't have a session cookie.
-spec update_session(cowboy_req:req(), atom(), any()) -> cowboy_req:req()
Given a cowboy request, a key (as an atom) and a value, replace the key with the provided value on the session on the request. Returns the updated request record.
-spec session_data(cowboy_req:req(), atom()) -> {any(), cowboy_req:req()}.
Given a cowboy request and a key, return the data associated with the key in the session on the request (or the atom undefined
if the key isn't found), and the updated request record.
-spec redirect_session(cowboy_req:req(), State) -> {ok, cowboy_req:req(), State}.
Create a response for redirecting the user through a cookie-detection loop. Used to ensure the browser supports cookies, as described elsewhere.
Cowboy-sessions is storage agnostic- it depends on a registered process to handle storing and retrieving session data. The library ships with an example backed by ets, which should only be used for illustration.
The storage process should be registered as ebb_session_service
, and should implement the following API.
Two message types will be sent to the storage process: a "get" and a "set" message.
The set
message is a 4-tuple- the first element is a tuple of the sending process ID and a reference, which will be sent back to the caller. The second element is the atom set
. The third elelement is a binary string which should be used as the key for storing the fourth element, which will be a property list.
The storage implementation should store the Value, then send to the caller a 2-tuple, as shown.
{{From, Ref}, set, SessionID, Value} -> {{self(), Ref}, ok}
The get
message is a 3-tuple. The first element is a tuple of the sending process ID and a reference, which will be sent back to the caller. The second element is the atom get
. The third element is the SessionID, a binary string which should be used to fetch the stored session data.
The implementation should send a 3-tuple back to the caller, as shown. If session data cannot be found, Val
may be the atom undefined
.
{{From, Ref}, get, SessionID} -> {{self(), Ref}, ok, Val}
This library makes use of cowboy's request hooks to examine and set cookie headers. Your application needs to be configured to integrate the library: its ProtoOpts
needs to hook in a couple of functions, as shown:
ProtoOpts = [...,
{onrequest, fun cowboy_sessions:on_request/1},
{onresponse, fun cowboy_sessions:on_response/4}],
Cowboy_sessions operates on browser cookies. If the user has disabled cookies in her browser, sessions will not work. The library implements a scheme for detecting cookie support in the browser when a cookie is not yet present.
The redirect_session
function sets a cookie on the provided request and redirects through a special endpoint which tries to read the newly-set cookie. If the cookie is present, it redirects back to the page on the initial request, now with a valid session.
If the session cookie is not present upon redirect, the user is given a 403 response.
So, the redirect_session
function should be used only to protect resources which absolutely need a session present: probably in conjunction with other libraries which support authentication, for example.
Here's the hello world example again, protected by redirect_session
. A user without a session who hits the handle function (at /hello
, say) will be redirected through a cookie-detection loop, ending up back at /hello
if her browser supports cookies. She'll have a session ID and will be given a greeting. If her browser does not support cookies, the user will receive a 302 error.
inner_handle(Req, State) ->
{Count, Req2} = cowboy_sessions:session_data(Req, count),
Count2 = case Count of
undefined -> 1;
_ -> Count + 1
end,
Req3 = cowboy_sessions:update_session(Req2, count, Count2),
Hello = io_lib:format("Hello, your visit count is: ~p.", [Count2]),
{ok, Req4} = cowboy_req:reply(200, [
{<<"content-type">>, <<"text/plain">>}
], Hello, Req3),
{ok, Req4, State}.
handle(Req, State) ->
{SessionID, Req2} = cowboy_sessions:session_id(Req),
case SessionID of
undefined ->
cowboy_sessions:redirect_session(Req2, State);
_ ->
inner_handle(Req2, State)
end.
Until I figure out how to get erlang.mk to run eunit tests, or move tests to common_tests, the existing eunit tests can be run with
erl -noshell -pa ebin -pa deps/*/ebin -eval "eunit:test(sessions_test, [verbose])" -s init stop