-
Notifications
You must be signed in to change notification settings - Fork 0
/
jar.ex
159 lines (136 loc) · 4.74 KB
/
jar.ex
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
defmodule HttpCookie.Jar do
defstruct [:cookies, :opts]
alias HttpCookie
@type t :: %__MODULE__{
cookies: list(HttpCookie.t()),
opts: keyword()
}
@doc """
Creates a new empty cookie jar.
## Options
- `:reject_public_suffixes` - controls whether to reject public suffixes to guard against "supercookies", defaults to true
"""
@spec new() :: %__MODULE__{}
@spec new(opts :: keyword()) :: %__MODULE__{}
def new(opts \\ []) do
%__MODULE__{
cookies: %{},
opts: opts
}
end
@doc """
Processes the response header list for the given request URL.
Parses set-cookie/set-cookie2 headers and stores valid cookies.
"""
@spec put_cookies_from_headers(jar :: t(), request_url :: URI.t(), headers :: list()) :: t()
def put_cookies_from_headers(jar, request_url, headers) do
cookies =
headers
|> Enum.filter(fn {k, _} -> k =~ ~r/^set-cookie2?$/i end)
|> Enum.flat_map(fn {_, header} ->
case HttpCookie.from_cookie_string(header, request_url) do
{:ok, cookie} -> [cookie]
_ -> []
end
end)
put_cookies(jar, cookies)
end
@doc """
Stores the provided cookies in the jar.
"""
@spec put_cookies(jar :: %__MODULE__{}, cookies :: list(HttpCookie.t())) :: %__MODULE__{}
def put_cookies(jar, cookies) do
Enum.reduce(cookies, jar, fn cookie, jar ->
put_cookie(jar, cookie)
end)
end
@doc """
Stores the provided cookie in the jar.
"""
@spec put_cookie(jar :: %__MODULE__{}, cookie :: HttpCookie.t()) :: %__MODULE__{}
def put_cookie(jar, cookie) do
cookie_key = key(cookie)
cookie =
case Map.get(jar.cookies, cookie_key) do
nil ->
cookie
old_cookie ->
# If the user agent receives a new cookie with the same cookie-name,
# domain-value, and path-value as a cookie that it has already stored,
# the existing cookie is evicted and replaced with the new cookie.
%{cookie | creation_time: old_cookie.creation_time}
end
update_in(jar.cookies, fn cookies ->
Map.put(cookies, cookie_key, cookie)
end)
end
@spec get_cookie_header_value(jar :: %__MODULE__{}, request_url :: URI.t()) ::
{:ok, String.t()} | {:error, :no_matching_cookies}
def get_cookie_header_value(jar, request_url) do
cookies =
jar
|> get_matching_cookies(request_url)
|> Enum.map(&HttpCookie.to_header_value/1)
if Enum.empty?(cookies) do
{:error, :no_matching_cookies}
else
{:ok, Enum.join(cookies, "; ")}
end
end
@doc """
Gets all the cookies in the store which match the given request URL.
"""
@spec get_matching_cookies(jar :: %__MODULE__{}, request_url :: URI.t()) :: list(HttpCookie.t())
def get_matching_cookies(jar, request_url) do
now = DateTime.utc_now()
jar.cookies
|> Map.values()
|> Enum.filter(fn cookie ->
!HttpCookie.expired?(cookie, now) and HttpCookie.matches_url?(cookie, request_url)
end)
|> sort_cookies()
|> Enum.map(&HttpCookie.update_last_access_time/1)
end
@doc """
Removes cookies which expired before the provided time from the jar.
Uses the current time if no time is provided.
"""
@spec clear_expired_cookies(jar :: %__MODULE__{}) :: %__MODULE__{}
@spec clear_expired_cookies(jar :: %__MODULE__{}, now :: DateTime.t()) :: %__MODULE__{}
def clear_expired_cookies(jar, now \\ DateTime.utc_now()) do
valid_cookies = Map.reject(jar.cookies, fn {_k, c} -> HttpCookie.expired?(c, now) end)
%{jar | cookies: valid_cookies}
end
@doc """
Removes session cookies from the jar.
Cookies which don't have an explicit expiry time set are considered session cookies and they expire when a user session ends.
"""
@spec clear_session_cookies(jar :: %__MODULE__{}) :: %__MODULE__{}
def clear_session_cookies(jar) do
persistent_cookies = Map.filter(jar.cookies, fn {_l, c} -> c.persistent? end)
%{jar | cookies: persistent_cookies}
end
# 2. The user agent SHOULD sort the cookie-list in the following
# order:
#
# * Cookies with longer paths are listed before cookies with
# shorter paths.
#
# * Among cookies that have equal-length path fields, cookies with
# earlier creation-times are listed before cookies with later
# creation-times.
defp sort_cookies(cookies) do
Enum.sort(cookies, fn lhs_cookie, rhs_cookie ->
lhs_path_size = byte_size(lhs_cookie.path)
rhs_path_size = byte_size(rhs_cookie.path)
if lhs_path_size == rhs_path_size do
DateTime.before?(lhs_cookie.creation_time, rhs_cookie.creation_time)
else
lhs_path_size > rhs_path_size
end
end)
end
defp key(cookie) do
{cookie.name, cookie.domain, cookie.path}
end
end