%% ``The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved via the world wide web at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% The Initial Developer of the Original Code is Ericsson Utvecklings AB. %% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings %% AB. All Rights Reserved.'' %% %% $Id$ %% Description: Cookie handling according to RFC 2109 -module(http_cookie). -include("httpc_internal.hrl"). -export([header/4, cookies/3, open_cookie_db/1, close_cookie_db/1, insert/2]). %%%========================================================================= %%% API %%%========================================================================= header(Scheme, {Host, _}, Path, CookieDb) -> case lookup_cookies(Host, Path, CookieDb) of [] -> {"cookie", ""}; Cookies -> {"cookie", cookies_to_string(Scheme, Cookies)} end. cookies(Headers, RequestPath, RequestHost) -> Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}), accept_cookies(Cookies, RequestPath, RequestHost). open_cookie_db({{_, only_session_cookies}, SessionDbName}) -> EtsDb = ets:new(SessionDbName, [protected, bag, {keypos, #http_cookie.domain}]), {undefined, EtsDb}; open_cookie_db({{DbName, Dbdir}, SessionDbName}) -> File = filename:join(Dbdir, atom_to_list(DbName)), {ok, DetsDb} = dets:open_file(DbName, [{keypos, #http_cookie.domain}, {type, bag}, {file, File}, {ram_file, true}]), EtsDb = ets:new(SessionDbName, [protected, bag, {keypos, #http_cookie.domain}]), {DetsDb, EtsDb}. close_cookie_db({undefined, EtsDb}) -> ets:delete(EtsDb); close_cookie_db({DetsDb, EtsDb}) -> dets:close(DetsDb), ets:delete(EtsDb). %% If no persistent cookie database is defined we %% treat all cookies as if they where session cookies. insert(Cookie = #http_cookie{max_age = Int}, Dbs = {undefined, _}) when is_integer(Int) -> insert(Cookie#http_cookie{max_age = session}, Dbs); insert(Cookie = #http_cookie{domain = Key, name = Name, path = Path, max_age = session}, Db = {_, CookieDb}) -> case ets:match_object(CookieDb, #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}) of [] -> ets:insert(CookieDb, Cookie); [NewCookie] -> delete(NewCookie, Db), ets:insert(CookieDb, Cookie) end, ok; insert(#http_cookie{domain = Key, name = Name, path = Path, max_age = 0}, Db = {CookieDb, _}) -> case dets:match_object(CookieDb, #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}) of [] -> ok; [NewCookie] -> delete(NewCookie, Db) end, ok; insert(Cookie = #http_cookie{domain = Key, name = Name, path = Path}, Db = {CookieDb, _}) -> case dets:match_object(CookieDb, #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}) of [] -> dets:insert(CookieDb, Cookie); [NewCookie] -> delete(NewCookie, Db), dets:insert(CookieDb, Cookie) end, ok. %%%======================================================================== %%% Internal functions %%%======================================================================== lookup_cookies(Key, {undefined, Ets}) -> ets:match_object(Ets, #http_cookie{domain = Key, _ = '_'}); lookup_cookies(Key, {Dets,Ets}) -> SessionCookies = ets:match_object(Ets, #http_cookie{domain = Key, _ = '_'}), Cookies = dets:match_object(Dets, #http_cookie{domain = Key, _ = '_'}), Cookies ++ SessionCookies. delete(Cookie = #http_cookie{max_age = session}, {_, CookieDb}) -> ets:delete_object(CookieDb, Cookie); delete(Cookie, {CookieDb, _}) -> dets:delete_object(CookieDb, Cookie). lookup_cookies(Host, Path, Db) -> Cookies = case http_util:is_hostname(Host) of true -> HostCookies = lookup_cookies(Host, Db), [_| DomainParts] = string:tokens(Host, "."), lookup_domain_cookies(DomainParts, Db, HostCookies); false -> % IP-adress lookup_cookies(Host, Db) end, ValidCookies = valid_cookies(Cookies, [], Db), lists:filter(fun(Cookie) -> lists:prefix(Cookie#http_cookie.path, Path) end, ValidCookies). %% For instance if Host=localhost lookup_domain_cookies([], _, AccCookies) -> lists:flatten(AccCookies); %% Top domains can not have cookies lookup_domain_cookies([_], _, AccCookies) -> lists:flatten(AccCookies); lookup_domain_cookies([Next | DomainParts], CookieDb, AccCookies) -> Domain = merge_domain_parts(DomainParts, [Next ++ "."]), lookup_domain_cookies(DomainParts, CookieDb, [lookup_cookies(Domain, CookieDb) | AccCookies]). merge_domain_parts([Part], Merged) -> lists:flatten(["." | lists:reverse([Part | Merged])]); merge_domain_parts([Part| Rest], Merged) -> merge_domain_parts(Rest, [".", Part | Merged]). cookies_to_string(Scheme, Cookies = [Cookie | _]) -> Version = "$Version=" ++ Cookie#http_cookie.version ++ "; ", cookies_to_string(Scheme, path_sort(Cookies), [Version]). cookies_to_string(_, [], CookieStrs) -> case length(CookieStrs) of 1 -> ""; _ -> lists:flatten(lists:reverse(CookieStrs)) end; cookies_to_string(https, [Cookie = #http_cookie{secure = true}| Cookies], CookieStrs) -> Str = case Cookies of [] -> cookie_to_string(Cookie); _ -> cookie_to_string(Cookie) ++ "; " end, cookies_to_string(https, Cookies, [Str | CookieStrs]); cookies_to_string(Scheme, [#http_cookie{secure = true}| Cookies], CookieStrs) -> cookies_to_string(Scheme, Cookies, CookieStrs); cookies_to_string(Scheme, [Cookie | Cookies], CookieStrs) -> Str = case Cookies of [] -> cookie_to_string(Cookie); _ -> cookie_to_string(Cookie) ++ "; " end, cookies_to_string(Scheme, Cookies, [Str | CookieStrs]). cookie_to_string(Cookie = #http_cookie{name = Name, value = Value}) -> Str = Name ++ "=" ++ Value, add_domain(add_path(Str, Cookie), Cookie). add_path(Str, #http_cookie{path_default = true}) -> Str; add_path(Str, #http_cookie{path = Path}) -> Str ++ "; $Path=" ++ Path. add_domain(Str, #http_cookie{domain_default = true}) -> Str; add_domain(Str, #http_cookie{domain = Domain}) -> Str ++ "; $Domain=" ++ Domain. parse_set_cookies(OtherHeaders, DefaultPathDomain) -> SetCookieHeaders = lists:foldl(fun({"set-cookie", Value}, Acc) -> [string:tokens(Value, ",")| Acc]; (_, Acc) -> Acc end, [], OtherHeaders), lists:flatten(lists:map(fun(CookieHeader) -> NewHeader = fix_netscape_cookie(CookieHeader, []), parse_set_cookie(NewHeader, [], DefaultPathDomain) end, SetCookieHeaders)). parse_set_cookie([], AccCookies, _) -> AccCookies; parse_set_cookie([CookieHeader | CookieHeaders], AccCookies, Defaults = {DefaultPath, DefaultDomain}) -> [CookieStr | Attributes] = case string:tokens(CookieHeader, ";") of [CStr] -> [CStr, ""]; [CStr | Attr] -> [CStr, Attr] end, Pos = string:chr(CookieStr, $=), Name = string:substr(CookieStr, 1, Pos - 1), Value = string:substr(CookieStr, Pos + 1), Cookie = #http_cookie{name = string:strip(Name), value = string:strip(Value)}, NewAttributes = parse_set_cookie_attributes(Attributes), TmpCookie = cookie_attributes(NewAttributes, Cookie), %% Add runtime defult values if necessary NewCookie = domain_default(path_default(TmpCookie, DefaultPath), DefaultDomain), parse_set_cookie(CookieHeaders, [NewCookie | AccCookies], Defaults). parse_set_cookie_attributes([]) -> []; parse_set_cookie_attributes([Attributes]) -> lists:map(fun(Attr) -> [AttrName, AttrValue] = case string:tokens(Attr, "=") of %% All attributes have the form %% Name=Value except "secure"! [Name] -> [Name, ""]; [Name, Value] -> [Name, Value]; %% Anything not expected will be %% disregarded _ -> ["Dummy",""] end, {http_util:to_lower(string:strip(AttrName)), string:strip(AttrValue)} end, Attributes). cookie_attributes([], Cookie) -> Cookie; cookie_attributes([{"comment", Value}| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie#http_cookie{comment = Value}); cookie_attributes([{"domain", Value}| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie#http_cookie{domain = Value}); cookie_attributes([{"max-age", Value}| Attributes], Cookie) -> ExpireTime = cookie_expires(list_to_integer(Value)), cookie_attributes(Attributes, Cookie#http_cookie{max_age = ExpireTime}); %% Backwards compatibility with netscape cookies cookie_attributes([{"expires", Value}| Attributes], Cookie) -> Time = http_util:convert_netscapecookie_date(Value), ExpireTime = calendar:datetime_to_gregorian_seconds(Time), cookie_attributes(Attributes, Cookie#http_cookie{max_age = ExpireTime}); cookie_attributes([{"path", Value}| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie#http_cookie{path = Value}); cookie_attributes([{"secure", _}| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie#http_cookie{secure = true}); cookie_attributes([{"version", Value}| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie#http_cookie{version = Value}); %% Disregard unknown attributes. cookie_attributes([_| Attributes], Cookie) -> cookie_attributes(Attributes, Cookie). domain_default(Cookie = #http_cookie{domain = undefined}, DefaultDomain) -> Cookie#http_cookie{domain = DefaultDomain, domain_default = true}; domain_default(Cookie, _) -> Cookie. path_default(Cookie = #http_cookie{path = undefined}, DefaultPath) -> Cookie#http_cookie{path = skip_right_most_slash(DefaultPath), path_default = true}; path_default(Cookie, _) -> Cookie. %% Note: if the path is only / that / will be keept skip_right_most_slash("/") -> "/"; skip_right_most_slash(Str) -> string:strip(Str, right, $/). accept_cookies(Cookies, RequestPath, RequestHost) -> lists:filter(fun(Cookie) -> accept_cookie(Cookie, RequestPath, RequestHost) end, Cookies). accept_cookie(Cookie, RequestPath, RequestHost) -> accept_path(Cookie, RequestPath) and accept_domain(Cookie, RequestHost). accept_path(#http_cookie{path = Path}, RequestPath) -> lists:prefix(Path, RequestPath). accept_domain(#http_cookie{domain = RequestHost}, RequestHost) -> true; accept_domain(#http_cookie{domain = Domain}, RequestHost) -> HostCheck = case http_util:is_hostname(RequestHost) of true -> (lists:suffix(Domain, RequestHost) andalso (not lists:member($., string:substr(RequestHost, 1, (length(RequestHost) - length(Domain)))))); false -> false end, HostCheck andalso (hd(Domain) == $.) andalso (length(string:tokens(Domain, ".")) > 1). cookie_expires(0) -> 0; cookie_expires(DeltaSec) -> NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), NowSec + DeltaSec. is_cookie_expired(#http_cookie{max_age = session}) -> false; is_cookie_expired(#http_cookie{max_age = ExpireTime}) -> NowSec = calendar:datetime_to_gregorian_seconds({date(), time()}), ExpireTime - NowSec =< 0. valid_cookies([], Valid, _) -> Valid; valid_cookies([Cookie | Cookies], Valid, Db) -> case is_cookie_expired(Cookie) of true -> delete(Cookie, Db), valid_cookies(Cookies, Valid, Db); false -> valid_cookies(Cookies, [Cookie | Valid], Db) end. path_sort(Cookies)-> lists:reverse(lists:keysort(#http_cookie.path, Cookies)). %% Informally, the Set-Cookie response header comprises the token %% Set-Cookie:, followed by a comma-separated list of one or more %% cookies. Netscape cookies expires attribute may also have a %% , in this case the header list will have been incorrectly split %% in parse_set_cookies/2 this functions fixs that problem. fix_netscape_cookie([Cookie1, Cookie2 | Rest], Acc) -> case regexp:match(Cookie1, "expires=") of {_, _, _} -> fix_netscape_cookie(Rest, [Cookie1 ++ Cookie2 | Acc]); nomatch -> fix_netscape_cookie([Cookie2 |Rest], [Cookie1| Acc]) end; fix_netscape_cookie([Cookie | Rest], Acc) -> fix_netscape_cookie(Rest, [Cookie | Acc]); fix_netscape_cookie([], Acc) -> Acc.