aboutsummaryrefslogtreecommitdiffstats
path: root/src/gun_cookies_list.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-03-02 14:32:22 +0100
committerLoïc Hoguin <[email protected]>2020-03-04 16:50:53 +0100
commit4e10d5c132a7b3a72f035eb1a993eb378b97ab1d (patch)
tree48fe5d5ccf2b3822bdf43390b6390a95fa99eb0c /src/gun_cookies_list.erl
parentaaf29783cc1932bcc167c63cdcd064cd09b07be2 (diff)
downloadgun-4e10d5c132a7b3a72f035eb1a993eb378b97ab1d.tar.gz
gun-4e10d5c132a7b3a72f035eb1a993eb378b97ab1d.tar.bz2
gun-4e10d5c132a7b3a72f035eb1a993eb378b97ab1d.zip
Initial implementation of the gun_cookies cookie store
Diffstat (limited to 'src/gun_cookies_list.erl')
-rw-r--r--src/gun_cookies_list.erl121
1 files changed, 121 insertions, 0 deletions
diff --git a/src/gun_cookies_list.erl b/src/gun_cookies_list.erl
new file mode 100644
index 0000000..2b4a666
--- /dev/null
+++ b/src/gun_cookies_list.erl
@@ -0,0 +1,121 @@
+%% Copyright (c) 2020, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% A reference cookie store implemented as a list of cookies.
+%% This cookie store cannot be shared between connections.
+-module(gun_cookies_list).
+
+-export([init/0]).
+-export([query/2]).
+-export([set_cookie_secure_match/2]).
+-export([set_cookie_take_exact_match/2]).
+-export([store/2]).
+
+-type state() :: #{
+ cookies := [gun_cookies:cookie()]
+ %% @todo Options would go here.
+}.
+
+-spec init() -> {?MODULE, state()}.
+init() ->
+ {?MODULE, #{cookies => []}}.
+
+-spec query(State, uri_string:uri_map())
+ -> {ok, [{binary(), binary()}], State}
+ when State::state().
+query(State=#{cookies := Cookies}, URI) ->
+ CurrentTime = erlang:universaltime(),
+ query(State, URI, Cookies, CurrentTime, [], []).
+
+query(State, _, [], _, CookieList, Cookies) ->
+ {ok, CookieList, State#{cookies => Cookies}};
+query(State, URI=#{scheme := Scheme, host := Host, path := Path},
+ [Cookie|Tail], CurrentTime, CookieList, Acc) ->
+ %% @todo This is probably not correct, says "canonicalized request-host"
+ %% and currently doesn't include the port number, it probably should?
+ Match0 = case Cookie of
+ #{host_only := true, domain := Host} ->
+ true;
+ #{host_only := false, domain := Domain} ->
+ gun_cookies:domain_match(Host, Domain);
+ _ ->
+ false
+ end,
+ Match1 = Match0 andalso
+ gun_cookies:path_match(Path, maps:get(path, Cookie)),
+ Match = Match1 andalso
+ case {Cookie, Scheme} of
+ {#{secure_only := true}, <<"https">>} -> true;
+ {#{secure_only := false}, _} -> true;
+ _ -> false
+ end,
+ %% @todo This is where we would check the http_only flag should
+ %% we want to implement a non-HTTP interface.
+ %% @todo This is where we would check for same-site/cross-site.
+ case Match of
+ true ->
+ UpdatedCookie = Cookie#{last_access_time => CurrentTime},
+ query(State, URI, Tail, CurrentTime,
+ [UpdatedCookie|CookieList],
+ [UpdatedCookie|Acc]);
+ false ->
+ query(State, URI, Tail, CurrentTime, CookieList, [Cookie|Acc])
+ end.
+
+-spec set_cookie_secure_match(state(), #{
+ name := binary(),
+% secure_only := true,
+ domain := binary(),
+ path := binary()
+}) -> match | nomatch.
+set_cookie_secure_match(#{cookies := Cookies},
+ #{name := Name, domain := Domain, path := Path}) ->
+ Result = [Cookie || Cookie=#{name := CookieName, secure_only := true} <- Cookies,
+ CookieName =:= Name,
+ gun_cookies:domain_match(Domain, maps:get(domain, Cookie))
+ orelse gun_cookies:domain_match(maps:get(domain, Cookie), Domain),
+ gun_cookies:path_match(Path, maps:get(path, Cookie))],
+ case Result of
+ [] -> nomatch;
+ _ -> match
+ end.
+
+-spec set_cookie_take_exact_match(State, #{
+ name := binary(),
+ domain := binary(),
+ host_only := boolean(),
+ path := binary()
+}) -> {ok, gun_cookies:cookie(), State} | error when State::state().
+set_cookie_take_exact_match(State=#{cookies := Cookies0}, Match) ->
+ Result = [Cookie || Cookie <- Cookies0,
+ Match =:= maps:with([name, domain, host_only, path], Cookie)],
+ Cookies = [Cookie || Cookie <- Cookies0,
+ Match =/= maps:with([name, domain, host_only, path], Cookie)],
+ case Result of
+ [] -> error;
+ [Cookie] -> {ok, Cookie, State#{cookies => Cookies}}
+ end.
+
+-spec store(State, gun_cookies:cookie())
+ -> {ok, State} | {error, any()}
+ when State::state().
+store(State=#{cookies := Cookies}, NewCookie=#{expiry_time := ExpiryTime}) ->
+ CurrentTime = erlang:universaltime(),
+ if
+ %% Do not store cookies with an expiry time in the past.
+ ExpiryTime =/= infinity, CurrentTime >= ExpiryTime ->
+ {ok, State};
+ true ->
+ {ok, State#{cookies => [NewCookie|Cookies]}}
+ end.