1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2004-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%% Description: Cookie handling according to RFC 2109
21
22%% The syntax for the Set-Cookie response header is
23%%
24%% set-cookie      =       "Set-Cookie:" cookies
25%% cookies         =       1#cookie
26%% cookie          =       NAME "=" VALUE *(";" cookie-av)
27%% NAME            =       attr
28%% VALUE           =       value
29%% cookie-av       =       "Comment" "=" value
30%%                 |       "Domain" "=" value
31%%                 |       "Max-Age" "=" value
32%%                 |       "Path" "=" value
33%%                 |       "Secure"
34%%                 |       "Version" "=" 1*DIGIT
35
36
37%% application:start(inets).
38%% httpc:set_options([{cookies, enabled}, {proxy, {{"www-proxy.ericsson.se",8080}, ["*.ericsson.se"]}}]).
39%% (catch httpc:request("http://www.expedia.com")).
40
41-module(httpc_cookie).
42
43-include("httpc_internal.hrl").
44
45-export([open_db/3, close_db/1, insert/2, header/4, cookies/3]).
46-export([reset_db/1, which_cookies/1]).
47-export([image_of/2, print/2]).
48
49-record(cookie_db, {db, session_db}).
50
51
52%%%=========================================================================
53%%%  API
54%%%=========================================================================
55
56%%--------------------------------------------------------------------
57%% Func: open_db(DbName, DbDir, SessionDbName) -> #cookie_db{}
58%% Purpose: Create the cookie db
59%%--------------------------------------------------------------------
60
61open_db(_, only_session_cookies, SessionDbName) ->
62    ?hcrt("open (session cookies only) db",
63	  [{session_db_name, SessionDbName}]),
64    SessionDb = ets:new(SessionDbName,
65			[protected, bag, {keypos, #http_cookie.domain}]),
66    #cookie_db{session_db = SessionDb};
67
68open_db(Name, Dir, SessionDbName) ->
69    ?hcrt("open db",
70	  [{name, Name}, {dir, Dir}, {session_db_name, SessionDbName}]),
71    File = filename:join(Dir, atom_to_list(Name)),
72    case dets:open_file(Name, [{keypos, #http_cookie.domain},
73			       {type, bag},
74			       {file, File},
75			       {ram_file, true}]) of
76	{ok, Db} ->
77	    SessionDb = ets:new(SessionDbName,
78				[protected, bag,
79				 {keypos, #http_cookie.domain}]),
80	    #cookie_db{db = Db, session_db = SessionDb};
81	{error, Reason} ->
82	    throw({error, {failed_open_file, Name, File, Reason}})
83    end.
84
85
86%%--------------------------------------------------------------------
87%% Func: reset_db(CookieDb) -> void()
88%% Purpose: Reset (empty) the cookie database
89%%
90%%--------------------------------------------------------------------
91
92reset_db(#cookie_db{db = undefined, session_db = SessionDb}) ->
93    ets:delete_all_objects(SessionDb),
94    ok;
95reset_db(#cookie_db{db = Db, session_db = SessionDb}) ->
96    dets:delete_all_objects(Db),
97    ets:delete_all_objects(SessionDb),
98    ok.
99
100
101%%--------------------------------------------------------------------
102%% Func: close_db(CookieDb) -> ok
103%% Purpose: Close the cookie db
104%%--------------------------------------------------------------------
105
106close_db(#cookie_db{db = Db, session_db = SessionDb}) ->
107    ?hcrt("close db", []),
108    maybe_dets_close(Db),
109    ets:delete(SessionDb),
110    ok.
111
112maybe_dets_close(undefined) ->
113    ok;
114maybe_dets_close(Db) ->
115    dets:close(Db).
116
117
118%%--------------------------------------------------------------------
119%% Func: insert(CookieDb, Cookie) -> ok
120%% Purpose: insert cookies into the cookie db
121%%--------------------------------------------------------------------
122
123%% If no persistent cookie database is defined we
124%% treat all cookies as if they where session cookies.
125insert(#cookie_db{db = undefined} = CookieDb,
126       #http_cookie{max_age = Int} = Cookie) when is_integer(Int) ->
127    insert(CookieDb, Cookie#http_cookie{max_age = session});
128
129insert(#cookie_db{session_db = SessionDb} = CookieDb,
130       #http_cookie{domain  = Key,
131		    name    = Name,
132		    path    = Path,
133		    max_age = session} = Cookie) ->
134    ?hcrt("insert session cookie", [{cookie, Cookie}]),
135    Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'},
136    case ets:match_object(SessionDb, Pattern) of
137	[] ->
138	    ets:insert(SessionDb, Cookie);
139	[NewCookie] ->
140	    delete(CookieDb, NewCookie),
141	    ets:insert(SessionDb, Cookie)
142    end,
143    ok;
144insert(#cookie_db{db = Db} = CookieDb,
145       #http_cookie{domain  = Key,
146		    name    = Name,
147		    path    = Path,
148		    max_age = 0}) ->
149    ?hcrt("insert cookie", [{domain, Key}, {name, Name}, {path, Path}]),
150    Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'},
151    case dets:match_object(Db, Pattern) of
152	[] ->
153	    ok;
154	[NewCookie] ->
155	    delete(CookieDb, NewCookie)
156    end,
157    ok;
158insert(#cookie_db{db = Db} = CookieDb,
159       #http_cookie{domain = Key, name = Name, path = Path} = Cookie) ->
160    ?hcrt("insert cookie", [{cookie, Cookie}]),
161    Pattern = #http_cookie{domain = Key,
162			   name = Name,
163			   path = Path,
164			   _ = '_'},
165    case dets:match_object(Db, Pattern) of
166	[] ->
167	    dets:insert(Db, Cookie);
168	[OldCookie] ->
169	    delete(CookieDb, OldCookie),
170	    dets:insert(Db, Cookie)
171    end,
172    ok.
173
174
175
176%%--------------------------------------------------------------------
177%% Func: header(CookieDb) -> ok
178%% Purpose: Cookies
179%%--------------------------------------------------------------------
180
181header(CookieDb, Scheme, {Host, _}, Path) ->
182    ?hcrd("header", [{scheme, Scheme}, {host, Host}, {path, Path}]),
183    case lookup_cookies(CookieDb, Host, Path) of
184	[] ->
185	    {"cookie", ""};
186	Cookies ->
187	    %% print_cookies("Header Cookies", Cookies),
188	    {"cookie", cookies_to_string(Scheme, Cookies)}
189    end.
190
191
192%%--------------------------------------------------------------------
193%% Func: cookies(Headers, RequestPath, RequestHost) -> [cookie()]
194%% Purpose: Which cookies are stored
195%%--------------------------------------------------------------------
196
197cookies(Headers, RequestPath, RequestHost) ->
198
199    ?hcrt("cookies", [{headers,      Headers},
200		      {request_path, RequestPath},
201		      {request_host, RequestHost}]),
202
203    Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}),
204
205    %% print_cookies("Parsed Cookies", Cookies),
206
207    AcceptedCookies = accept_cookies(Cookies, RequestPath, RequestHost),
208
209    %% print_cookies("Accepted Cookies", AcceptedCookies),
210
211    AcceptedCookies.
212
213
214%%--------------------------------------------------------------------
215%% Func: which_cookies(CookieDb) -> [cookie()]
216%% Purpose: For test and debug purpose,
217%%          dump the entire cookie database
218%%--------------------------------------------------------------------
219
220which_cookies(#cookie_db{db = undefined, session_db = SessionDb}) ->
221    SessionCookies = ets:tab2list(SessionDb),
222    [{session_cookies, SessionCookies}];
223which_cookies(#cookie_db{db = Db, session_db = SessionDb}) ->
224    Cookies        = dets:match_object(Db, '_'),
225    SessionCookies = ets:tab2list(SessionDb),
226    [{cookies, Cookies}, {session_cookies, SessionCookies}].
227
228
229%%%========================================================================
230%%% Internal functions
231%%%========================================================================
232
233delete(#cookie_db{session_db = SessionDb},
234       #http_cookie{max_age = session} = Cookie) ->
235    ets:delete_object(SessionDb, Cookie);
236delete(#cookie_db{db = Db}, Cookie) ->
237    dets:delete_object(Db, Cookie).
238
239
240lookup_cookies(#cookie_db{db = undefined, session_db = SessionDb}, Key) ->
241    Pattern = #http_cookie{domain = Key, _ = '_'},
242    Cookies = ets:match_object(SessionDb, Pattern),
243    ?hcrt("lookup cookies", [{cookies, Cookies}]),
244    Cookies;
245
246lookup_cookies(#cookie_db{db = Db, session_db = SessionDb}, Key) ->
247    Pattern = #http_cookie{domain = Key, _ = '_'},
248    SessionCookies = ets:match_object(SessionDb, Pattern),
249    ?hcrt("lookup cookies", [{session_cookies, SessionCookies}]),
250    Cookies = dets:match_object(Db, Pattern),
251    ?hcrt("lookup cookies", [{cookies, Cookies}]),
252    Cookies ++ SessionCookies.
253
254
255lookup_cookies(CookieDb, Host, Path) ->
256    Cookies =
257	case http_util:is_hostname(Host) of
258	    true ->
259		HostCookies = lookup_cookies(CookieDb, Host),
260		[_| DomainParts] = string:tokens(Host, "."),
261		lookup_domain_cookies(CookieDb, DomainParts, HostCookies);
262	    false -> % IP-adress
263		lookup_cookies(CookieDb, Host)
264	end,
265    ValidCookies = valid_cookies(CookieDb, Cookies),
266    lists:filter(fun(Cookie) ->
267			 lists:prefix(Cookie#http_cookie.path, Path)
268		 end, ValidCookies).
269
270%% For instance if Host=localhost
271lookup_domain_cookies(_CookieDb, [], AccCookies) ->
272    lists:flatten(AccCookies);
273
274%% Top domains cannot have cookies
275lookup_domain_cookies(_CookieDb, [_], AccCookies) ->
276    lists:flatten(AccCookies);
277
278lookup_domain_cookies(CookieDb, [Next | DomainParts], AccCookies) ->
279    Domain = merge_domain_parts(DomainParts, [Next ++ "."]),
280    lookup_domain_cookies(CookieDb, DomainParts,
281			  [lookup_cookies(CookieDb, Domain) | AccCookies]).
282
283merge_domain_parts([Part], Merged) ->
284    lists:flatten(["." | lists:reverse([Part | Merged])]);
285merge_domain_parts([Part| Rest], Merged) ->
286    merge_domain_parts(Rest, [".", Part | Merged]).
287
288cookies_to_string(Scheme, [Cookie | _] = Cookies) ->
289    Version = "$Version=" ++ Cookie#http_cookie.version ++ "; ",
290    cookies_to_string(Scheme, path_sort(Cookies), [Version]).
291
292cookies_to_string(_, [], CookieStrs) ->
293    case length(CookieStrs) of
294	1 ->
295	    "";
296	_ ->
297	    lists:flatten(lists:reverse(CookieStrs))
298    end;
299
300cookies_to_string(https = Scheme,
301		  [#http_cookie{secure = true} = Cookie| Cookies],
302		  CookieStrs) ->
303    Str = case Cookies of
304	      [] ->
305		  cookie_to_string(Cookie);
306	      _ ->
307		  cookie_to_string(Cookie) ++ "; "
308	  end,
309    cookies_to_string(Scheme, Cookies, [Str | CookieStrs]);
310
311cookies_to_string(Scheme, [#http_cookie{secure = true}| Cookies],
312		  CookieStrs) ->
313    cookies_to_string(Scheme, Cookies, CookieStrs);
314
315cookies_to_string(Scheme, [Cookie | Cookies], CookieStrs) ->
316    Str = case Cookies of
317	      [] ->
318		  cookie_to_string(Cookie);
319	      _ ->
320		  cookie_to_string(Cookie) ++ "; "
321	  end,
322    cookies_to_string(Scheme, Cookies, [Str | CookieStrs]).
323
324cookie_to_string(#http_cookie{name = Name, value = Value} = Cookie) ->
325    Str = Name ++ "=" ++ Value,
326    add_domain(add_path(Str, Cookie), Cookie).
327
328add_path(Str, #http_cookie{path_default = true}) ->
329    Str;
330add_path(Str, #http_cookie{path = Path}) ->
331    Str ++ "; $Path=" ++  Path.
332
333add_domain(Str, #http_cookie{domain_default = true}) ->
334    Str;
335add_domain(Str, #http_cookie{domain = Domain}) ->
336    Str ++ "; $Domain=" ++  Domain.
337
338is_set_cookie_valid("") ->
339    %% an empty Set-Cookie header is not valid
340    false;
341is_set_cookie_valid([$=|_]) ->
342    %% a Set-Cookie header without name is not valid
343    false;
344is_set_cookie_valid(SetCookieHeader) ->
345    %% a Set-Cookie header without name/value is not valid
346    case string:chr(SetCookieHeader, $=) of
347        0 -> false;
348        _ -> true
349    end.
350
351parse_set_cookies(CookieHeaders, DefaultPathDomain) ->
352    %% filter invalid Set-Cookie headers
353    SetCookieHeaders = [Value || {"set-cookie", Value} <- CookieHeaders,
354                                 is_set_cookie_valid(Value)],
355    Cookies = [parse_set_cookie(SetCookieHeader, DefaultPathDomain) ||
356		  SetCookieHeader <- SetCookieHeaders],
357    %% print_cookies("Parsed Cookies", Cookies),
358    Cookies.
359
360parse_set_cookie(CookieHeader, {DefaultPath, DefaultDomain}) ->
361    %% io:format("Raw Cookie: ~s~n", [CookieHeader]),
362    Pos             = string:chr(CookieHeader, $=),
363    Name            = string:substr(CookieHeader, 1, Pos - 1),
364    {Value, Attrs}  =
365	case string:substr(CookieHeader, Pos + 1) of
366	    [] ->
367		{"", ""};
368	    [$;|ValueAndAttrs] ->
369		{"", string:tokens(ValueAndAttrs, ";")};
370	    ValueAndAttrs ->
371		[V | A] = string:tokens(ValueAndAttrs, ";"),
372		{V, A}
373	end,
374    Cookie          = #http_cookie{name  = string:strip(Name),
375				   value = string:strip(Value)},
376    Attributes      = parse_set_cookie_attributes(Attrs),
377    TmpCookie       = cookie_attributes(Attributes, Cookie),
378    %% Add runtime defult values if necessary
379    NewCookie       = domain_default(path_default(TmpCookie, DefaultPath),
380				     DefaultDomain),
381    NewCookie.
382
383parse_set_cookie_attributes(Attributes) when is_list(Attributes) ->
384    [parse_set_cookie_attribute(A) || A <- Attributes].
385
386parse_set_cookie_attribute(Attribute) ->
387    {AName, AValue} =
388	case string:tokens(Attribute, "=") of
389	    %% All attributes have the form
390	    %% Name=Value except "secure"!
391	    [Name] ->
392		{Name, ""};
393	    [Name, Value] ->
394		{Name, Value};
395	    %% Anything not expected will be
396	    %% disregarded
397	    _ ->
398		{"Dummy", ""}
399	end,
400    StrippedName  = http_util:to_lower(string:strip(AName)),
401    StrippedValue = string:strip(AValue),
402    {StrippedName, StrippedValue}.
403
404cookie_attributes([], Cookie) ->
405    Cookie;
406cookie_attributes([{"comment", Value}| Attributes], Cookie) ->
407    cookie_attributes(Attributes,
408				Cookie#http_cookie{comment = Value});
409cookie_attributes([{"domain", Value}| Attributes], Cookie) ->
410    cookie_attributes(Attributes,
411				Cookie#http_cookie{domain = Value});
412cookie_attributes([{"max-age", Value}| Attributes], Cookie) ->
413    ExpireTime = cookie_expires(list_to_integer(Value)),
414    cookie_attributes(Attributes,
415				Cookie#http_cookie{max_age = ExpireTime});
416%% Backwards compatibility with netscape cookies
417cookie_attributes([{"expires", Value}| Attributes], Cookie) ->
418    try http_util:convert_netscapecookie_date(Value) of
419	Time ->
420	    ExpireTime = calendar:datetime_to_gregorian_seconds(Time),
421	    cookie_attributes(Attributes,
422			      Cookie#http_cookie{max_age = ExpireTime})
423    catch
424	_:_ ->
425	    cookie_attributes(Attributes, Cookie)
426    end;
427cookie_attributes([{"path", Value}| Attributes], Cookie) ->
428    cookie_attributes(Attributes,
429		      Cookie#http_cookie{path = Value});
430cookie_attributes([{"secure", _}| Attributes], Cookie) ->
431    cookie_attributes(Attributes,
432		      Cookie#http_cookie{secure = true});
433cookie_attributes([{"version", Value}| Attributes], Cookie) ->
434    cookie_attributes(Attributes,
435		      Cookie#http_cookie{version = Value});
436%% Disregard unknown attributes.
437cookie_attributes([_| Attributes], Cookie) ->
438    cookie_attributes(Attributes, Cookie).
439
440domain_default(Cookie = #http_cookie{domain = undefined},
441	       DefaultDomain) ->
442    Cookie#http_cookie{domain = DefaultDomain, domain_default = true};
443domain_default(Cookie, _) ->
444    Cookie.
445
446path_default(#http_cookie{path = undefined} = Cookie, DefaultPath) ->
447    Cookie#http_cookie{path = skip_right_most_slash(DefaultPath),
448		       path_default = true};
449path_default(Cookie, _) ->
450    Cookie.
451
452%% Note: if the path is only / that / will be kept
453skip_right_most_slash("/") ->
454    "/";
455skip_right_most_slash(Str) ->
456    string:strip(Str, right, $/).
457
458accept_cookies(Cookies, RequestPath, RequestHost) ->
459    lists:filter(fun(Cookie) ->
460			 accept_cookie(Cookie, RequestPath, RequestHost)
461		 end, Cookies).
462
463accept_cookie(Cookie, RequestPath, RequestHost) ->
464    Accepted =
465	accept_path(Cookie, RequestPath) andalso
466	accept_domain(Cookie, RequestHost),
467    Accepted.
468
469accept_path(#http_cookie{path = Path}, RequestPath) ->
470    lists:prefix(Path, RequestPath).
471
472accept_domain(#http_cookie{domain = RequestHost}, RequestHost) ->
473    true;
474
475accept_domain(#http_cookie{domain = Domain}, RequestHost) ->
476    HostCheck =
477	case http_util:is_hostname(RequestHost) of
478	    true ->
479		(lists:suffix(Domain, RequestHost) andalso
480		 (not
481		  lists:member($.,
482			       string:substr(RequestHost, 1,
483					     (length(RequestHost) -
484					      length(Domain))))));
485	    false ->
486		false
487	end,
488    HostCheck
489	andalso (hd(Domain) =:= $.)
490	andalso (length(string:tokens(Domain, ".")) > 1).
491
492cookie_expires(0) ->
493    0;
494cookie_expires(DeltaSec) ->
495    NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}),
496    NowSec + DeltaSec.
497
498is_cookie_expired(#http_cookie{max_age = session}) ->
499    false;
500is_cookie_expired(#http_cookie{max_age = ExpireTime}) ->
501    NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}),
502    ExpireTime - NowSec =< 0.
503
504
505valid_cookies(Db, Cookies) ->
506    valid_cookies(Db, Cookies, []).
507
508valid_cookies(_Db, [], Valid) ->
509    Valid;
510
511valid_cookies(Db, [Cookie | Cookies], Valid) ->
512    case is_cookie_expired(Cookie) of
513	true ->
514	    delete(Db, Cookie),
515	    valid_cookies(Db, Cookies, Valid);
516	false ->
517	    valid_cookies(Db, Cookies, [Cookie | Valid])
518    end.
519
520path_sort(Cookies)->
521    lists:reverse(lists:keysort(#http_cookie.path, Cookies)).
522
523
524%% print_cookies(Header, Cookies) ->
525%%     io:format("~s:~n", [Header]),
526%%     Prefix = "   ",
527%%     lists:foreach(fun(Cookie) -> print(Prefix, Cookie) end, Cookies).
528
529image_of(Prefix,
530	 #http_cookie{domain         = Domain,
531		      domain_default = DomainDef,
532		      name           = Name,
533		      value          = Value,
534		      comment        = Comment,
535		      max_age        = MaxAge,
536		      path           = Path,
537		      path_default   = PathDef,
538		      secure         = Sec,
539		      version        = Version}) ->
540    lists:flatten(
541      io_lib:format("~sCookie ~s: "
542		    "~n~s   Value:     ~p"
543		    "~n~s   Domain:    ~p"
544		    "~n~s   DomainDef: ~p"
545		    "~n~s   Comment:   ~p"
546		    "~n~s   MaxAge:    ~p"
547		    "~n~s   Path:      ~p"
548		    "~n~s   PathDef:   ~p"
549		    "~n~s   Secure:    ~p"
550		    "~n~s   Version:   ~p",
551		    [Prefix, Name,
552		     Prefix, Value,
553		     Prefix, Domain,
554		     Prefix, DomainDef,
555		     Prefix, Comment,
556		     Prefix, MaxAge,
557		     Prefix, Path,
558		     Prefix, PathDef,
559		     Prefix, Sec,
560		     Prefix, Version])).
561
562print(Prefix, Cookie) when is_record(Cookie, http_cookie) ->
563    io:format("~s~n", [image_of(Prefix, Cookie)]).
564