aboutsummaryrefslogtreecommitdiffstats
path: root/src/gun_cookies_list.erl
blob: 2b4a666990135d0240cbc2f7d5294211e250b026 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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.