From 4e10d5c132a7b3a72f035eb1a993eb378b97ab1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 2 Mar 2020 14:32:22 +0100 Subject: Initial implementation of the gun_cookies cookie store --- src/gun_cookies_list.erl | 121 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/gun_cookies_list.erl (limited to 'src/gun_cookies_list.erl') 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 +%% +%% 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. -- cgit v1.2.3