4
%% Copyright Ericsson AB 2004-2009. All Rights Reserved.
6
%% The contents of this file are subject to the Erlang Public License,
7
%% Version 1.1, (the "License"); you may not use this file except in
8
%% compliance with the License. You should have received a copy of the
9
%% Erlang Public License along with this software. If not, it can be
10
%% retrieved online at http://www.erlang.org/.
12
%% Software distributed under the License is distributed on an "AS IS"
13
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
14
%% the License for the specific language governing rights and limitations
19
%% Description: Cookie handling according to RFC 2109
23
-include("httpc_internal.hrl").
25
-export([header/4, cookies/3, open_cookie_db/1, close_cookie_db/1, insert/2]).
27
%%%=========================================================================
29
%%%=========================================================================
30
header(Scheme, {Host, _}, Path, CookieDb) ->
31
case lookup_cookies(Host, Path, CookieDb) of
35
{"cookie", cookies_to_string(Scheme, Cookies)}
38
cookies(Headers, RequestPath, RequestHost) ->
39
Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}),
40
accept_cookies(Cookies, RequestPath, RequestHost).
42
open_cookie_db({{_, only_session_cookies}, SessionDbName}) ->
43
EtsDb = ets:new(SessionDbName, [protected, bag,
44
{keypos, #http_cookie.domain}]),
47
open_cookie_db({{DbName, Dbdir}, SessionDbName}) ->
48
File = filename:join(Dbdir, atom_to_list(DbName)),
49
{ok, DetsDb} = dets:open_file(DbName, [{keypos, #http_cookie.domain},
53
EtsDb = ets:new(SessionDbName, [protected, bag,
54
{keypos, #http_cookie.domain}]),
57
close_cookie_db({undefined, EtsDb}) ->
60
close_cookie_db({DetsDb, EtsDb}) ->
64
%% If no persistent cookie database is defined we
65
%% treat all cookies as if they where session cookies.
66
insert(Cookie = #http_cookie{max_age = Int},
67
Dbs = {undefined, _}) when is_integer(Int) ->
68
insert(Cookie#http_cookie{max_age = session}, Dbs);
70
insert(Cookie = #http_cookie{domain = Key, name = Name,
71
path = Path, max_age = session},
72
Db = {_, CookieDb}) ->
73
case ets:match_object(CookieDb, #http_cookie{domain = Key,
78
ets:insert(CookieDb, Cookie);
80
delete(NewCookie, Db),
81
ets:insert(CookieDb, Cookie)
84
insert(#http_cookie{domain = Key, name = Name,
85
path = Path, max_age = 0},
86
Db = {CookieDb, _}) ->
87
case dets:match_object(CookieDb, #http_cookie{domain = Key,
97
insert(Cookie = #http_cookie{domain = Key, name = Name, path = Path},
98
Db = {CookieDb, _}) ->
99
case dets:match_object(CookieDb, #http_cookie{domain = Key,
104
dets:insert(CookieDb, Cookie);
106
delete(NewCookie, Db),
107
dets:insert(CookieDb, Cookie)
111
%%%========================================================================
112
%%% Internal functions
113
%%%========================================================================
114
lookup_cookies(Key, {undefined, Ets}) ->
115
ets:match_object(Ets, #http_cookie{domain = Key,
117
lookup_cookies(Key, {Dets,Ets}) ->
118
SessionCookies = ets:match_object(Ets, #http_cookie{domain = Key,
120
Cookies = dets:match_object(Dets, #http_cookie{domain = Key,
122
Cookies ++ SessionCookies.
124
delete(Cookie = #http_cookie{max_age = session}, {_, CookieDb}) ->
125
ets:delete_object(CookieDb, Cookie);
126
delete(Cookie, {CookieDb, _}) ->
127
dets:delete_object(CookieDb, Cookie).
129
lookup_cookies(Host, Path, Db) ->
131
case http_util:is_hostname(Host) of
133
HostCookies = lookup_cookies(Host, Db),
134
[_| DomainParts] = string:tokens(Host, "."),
135
lookup_domain_cookies(DomainParts, Db, HostCookies);
137
lookup_cookies(Host, Db)
139
ValidCookies = valid_cookies(Cookies, [], Db),
140
lists:filter(fun(Cookie) ->
141
lists:prefix(Cookie#http_cookie.path, Path)
144
%% For instance if Host=localhost
145
lookup_domain_cookies([], _, AccCookies) ->
146
lists:flatten(AccCookies);
147
%% Top domains can not have cookies
148
lookup_domain_cookies([_], _, AccCookies) ->
149
lists:flatten(AccCookies);
150
lookup_domain_cookies([Next | DomainParts], CookieDb, AccCookies) ->
151
Domain = merge_domain_parts(DomainParts, [Next ++ "."]),
152
lookup_domain_cookies(DomainParts, CookieDb,
153
[lookup_cookies(Domain, CookieDb)
156
merge_domain_parts([Part], Merged) ->
157
lists:flatten(["." | lists:reverse([Part | Merged])]);
158
merge_domain_parts([Part| Rest], Merged) ->
159
merge_domain_parts(Rest, [".", Part | Merged]).
161
cookies_to_string(Scheme, Cookies = [Cookie | _]) ->
162
Version = "$Version=" ++ Cookie#http_cookie.version ++ "; ",
163
cookies_to_string(Scheme, path_sort(Cookies), [Version]).
165
cookies_to_string(_, [], CookieStrs) ->
166
case length(CookieStrs) of
170
lists:flatten(lists:reverse(CookieStrs))
173
cookies_to_string(https, [Cookie = #http_cookie{secure = true}| Cookies],
175
Str = case Cookies of
177
cookie_to_string(Cookie);
179
cookie_to_string(Cookie) ++ "; "
181
cookies_to_string(https, Cookies, [Str | CookieStrs]);
183
cookies_to_string(Scheme, [#http_cookie{secure = true}| Cookies],
185
cookies_to_string(Scheme, Cookies, CookieStrs);
187
cookies_to_string(Scheme, [Cookie | Cookies], CookieStrs) ->
188
Str = case Cookies of
190
cookie_to_string(Cookie);
192
cookie_to_string(Cookie) ++ "; "
194
cookies_to_string(Scheme, Cookies, [Str | CookieStrs]).
196
cookie_to_string(Cookie = #http_cookie{name = Name, value = Value}) ->
197
Str = Name ++ "=" ++ Value,
198
add_domain(add_path(Str, Cookie), Cookie).
200
add_path(Str, #http_cookie{path_default = true}) ->
202
add_path(Str, #http_cookie{path = Path}) ->
203
Str ++ "; $Path=" ++ Path.
205
add_domain(Str, #http_cookie{domain_default = true}) ->
207
add_domain(Str, #http_cookie{domain = Domain}) ->
208
Str ++ "; $Domain=" ++ Domain.
210
parse_set_cookies(OtherHeaders, DefaultPathDomain) ->
211
SetCookieHeaders = lists:foldl(fun({"set-cookie", Value}, Acc) ->
212
[string:tokens(Value, ",")| Acc];
215
end, [], OtherHeaders),
217
lists:flatten(lists:map(fun(CookieHeader) ->
219
fix_netscape_cookie(CookieHeader,
221
parse_set_cookie(NewHeader, [],
222
DefaultPathDomain) end,
225
parse_set_cookie([], AccCookies, _) ->
227
parse_set_cookie([CookieHeader | CookieHeaders], AccCookies,
228
Defaults = {DefaultPath, DefaultDomain}) ->
229
[CookieStr | Attributes] = case string:tokens(CookieHeader, ";") of
235
Pos = string:chr(CookieStr, $=),
236
Name = string:substr(CookieStr, 1, Pos - 1),
237
Value = string:substr(CookieStr, Pos + 1),
238
Cookie = #http_cookie{name = string:strip(Name),
239
value = string:strip(Value)},
240
NewAttributes = parse_set_cookie_attributes(Attributes),
241
TmpCookie = cookie_attributes(NewAttributes, Cookie),
242
%% Add runtime defult values if necessary
243
NewCookie = domain_default(path_default(TmpCookie, DefaultPath),
245
parse_set_cookie(CookieHeaders, [NewCookie | AccCookies], Defaults).
247
parse_set_cookie_attributes([]) ->
249
parse_set_cookie_attributes([Attributes]) ->
250
lists:map(fun(Attr) ->
251
[AttrName, AttrValue] =
252
case string:tokens(Attr, "=") of
253
%% All attributes have the form
254
%% Name=Value except "secure"!
259
%% Anything not expected will be
264
{http_util:to_lower(string:strip(AttrName)),
265
string:strip(AttrValue)}
268
cookie_attributes([], Cookie) ->
270
cookie_attributes([{"comment", Value}| Attributes], Cookie) ->
271
cookie_attributes(Attributes,
272
Cookie#http_cookie{comment = Value});
273
cookie_attributes([{"domain", Value}| Attributes], Cookie) ->
274
cookie_attributes(Attributes,
275
Cookie#http_cookie{domain = Value});
276
cookie_attributes([{"max-age", Value}| Attributes], Cookie) ->
277
ExpireTime = cookie_expires(list_to_integer(Value)),
278
cookie_attributes(Attributes,
279
Cookie#http_cookie{max_age = ExpireTime});
280
%% Backwards compatibility with netscape cookies
281
cookie_attributes([{"expires", Value}| Attributes], Cookie) ->
282
Time = http_util:convert_netscapecookie_date(Value),
283
ExpireTime = calendar:datetime_to_gregorian_seconds(Time),
284
cookie_attributes(Attributes,
285
Cookie#http_cookie{max_age = ExpireTime});
286
cookie_attributes([{"path", Value}| Attributes], Cookie) ->
287
cookie_attributes(Attributes,
288
Cookie#http_cookie{path = Value});
289
cookie_attributes([{"secure", _}| Attributes], Cookie) ->
290
cookie_attributes(Attributes,
291
Cookie#http_cookie{secure = true});
292
cookie_attributes([{"version", Value}| Attributes], Cookie) ->
293
cookie_attributes(Attributes,
294
Cookie#http_cookie{version = Value});
295
%% Disregard unknown attributes.
296
cookie_attributes([_| Attributes], Cookie) ->
297
cookie_attributes(Attributes, Cookie).
299
domain_default(Cookie = #http_cookie{domain = undefined},
301
Cookie#http_cookie{domain = DefaultDomain, domain_default = true};
302
domain_default(Cookie, _) ->
305
path_default(Cookie = #http_cookie{path = undefined},
307
Cookie#http_cookie{path = skip_right_most_slash(DefaultPath),
308
path_default = true};
309
path_default(Cookie, _) ->
312
%% Note: if the path is only / that / will be keept
313
skip_right_most_slash("/") ->
315
skip_right_most_slash(Str) ->
316
string:strip(Str, right, $/).
318
accept_cookies(Cookies, RequestPath, RequestHost) ->
319
lists:filter(fun(Cookie) ->
320
accept_cookie(Cookie, RequestPath, RequestHost)
323
accept_cookie(Cookie, RequestPath, RequestHost) ->
324
accept_path(Cookie, RequestPath) and accept_domain(Cookie, RequestHost).
326
accept_path(#http_cookie{path = Path}, RequestPath) ->
327
lists:prefix(Path, RequestPath).
329
accept_domain(#http_cookie{domain = RequestHost}, RequestHost) ->
332
accept_domain(#http_cookie{domain = Domain}, RequestHost) ->
333
HostCheck = case http_util:is_hostname(RequestHost) of
335
(lists:suffix(Domain, RequestHost) andalso
338
string:substr(RequestHost, 1,
339
(length(RequestHost) -
344
HostCheck andalso (hd(Domain) == $.)
345
andalso (length(string:tokens(Domain, ".")) > 1).
349
cookie_expires(DeltaSec) ->
350
NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}),
353
is_cookie_expired(#http_cookie{max_age = session}) ->
355
is_cookie_expired(#http_cookie{max_age = ExpireTime}) ->
356
NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}),
357
ExpireTime - NowSec =< 0.
359
valid_cookies([], Valid, _) ->
362
valid_cookies([Cookie | Cookies], Valid, Db) ->
363
case is_cookie_expired(Cookie) of
366
valid_cookies(Cookies, Valid, Db);
368
valid_cookies(Cookies, [Cookie | Valid], Db)
372
lists:reverse(lists:keysort(#http_cookie.path, Cookies)).
375
%% Informally, the Set-Cookie response header comprises the token
376
%% Set-Cookie:, followed by a comma-separated list of one or more
377
%% cookies. Netscape cookies expires attribute may also have a
378
%% , in this case the header list will have been incorrectly split
379
%% in parse_set_cookies/2 this functions fixs that problem.
380
fix_netscape_cookie([Cookie1, Cookie2 | Rest], Acc) ->
381
case inets_regexp:match(Cookie1, "expires=") of
383
fix_netscape_cookie(Rest, [Cookie1 ++ Cookie2 | Acc]);
385
fix_netscape_cookie([Cookie2 |Rest], [Cookie1| Acc])
387
fix_netscape_cookie([Cookie | Rest], Acc) ->
388
fix_netscape_cookie(Rest, [Cookie | Acc]);
390
fix_netscape_cookie([], Acc) ->