%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2004-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% Description: Cookie handling according to RFC 2109 %% The syntax for the Set-Cookie response header is %% %% set-cookie = "Set-Cookie:" cookies %% cookies = 1#cookie %% cookie = NAME "=" VALUE *(";" cookie-av) %% NAME = attr %% VALUE = value %% cookie-av = "Comment" "=" value %% | "Domain" "=" value %% | "Max-Age" "=" value %% | "Path" "=" value %% | "Secure" %% | "Version" "=" 1*DIGIT %% application:start(inets). %% httpc:set_options([{cookies, enabled}, {proxy, {{"www-proxy.ericsson.se",8080}, ["*.ericsson.se"]}}]). %% (catch httpc:request("http://www.expedia.com")). -module(httpc_cookie). -include("httpc_internal.hrl"). -export([open_db/3, close_db/1, insert/2, header/4, cookies/3]). -export([reset_db/1, which_cookies/1]). -export([image_of/2, print/2]). -record(cookie_db, {db, session_db}). %%%========================================================================= %%% API %%%========================================================================= %%-------------------------------------------------------------------- %% Func: open_db(DbName, DbDir, SessionDbName) -> #cookie_db{} %% Purpose: Create the cookie db %%-------------------------------------------------------------------- open_db(_, only_session_cookies, SessionDbName) -> ?hcrt("open (session cookies only) db", [{session_db_name, SessionDbName}]), SessionDb = ets:new(SessionDbName, [protected, bag, {keypos, #http_cookie.domain}]), #cookie_db{session_db = SessionDb}; open_db(Name, Dir, SessionDbName) -> ?hcrt("open db", [{name, Name}, {dir, Dir}, {session_db_name, SessionDbName}]), File = filename:join(Dir, atom_to_list(Name)), case dets:open_file(Name, [{keypos, #http_cookie.domain}, {type, bag}, {file, File}, {ram_file, true}]) of {ok, Db} -> SessionDb = ets:new(SessionDbName, [protected, bag, {keypos, #http_cookie.domain}]), #cookie_db{db = Db, session_db = SessionDb}; {error, Reason} -> throw({error, {failed_open_file, Name, File, Reason}}) end. %%-------------------------------------------------------------------- %% Func: reset_db(CookieDb) -> void() %% Purpose: Reset (empty) the cookie database %% %%-------------------------------------------------------------------- reset_db(#cookie_db{db = undefined, session_db = SessionDb}) -> ets:delete_all_objects(SessionDb), ok; reset_db(#cookie_db{db = Db, session_db = SessionDb}) -> dets:delete_all_objects(Db), ets:delete_all_objects(SessionDb), ok. %%-------------------------------------------------------------------- %% Func: close_db(CookieDb) -> ok %% Purpose: Close the cookie db %%-------------------------------------------------------------------- close_db(#cookie_db{db = Db, session_db = SessionDb}) -> ?hcrt("close db", []), maybe_dets_close(Db), ets:delete(SessionDb), ok. maybe_dets_close(undefined) -> ok; maybe_dets_close(Db) -> dets:close(Db). %%-------------------------------------------------------------------- %% Func: insert(CookieDb, Cookie) -> ok %% Purpose: insert cookies into the cookie db %%-------------------------------------------------------------------- %% If no persistent cookie database is defined we %% treat all cookies as if they where session cookies. insert(#cookie_db{db = undefined} = CookieDb, #http_cookie{max_age = Int} = Cookie) when is_integer(Int) -> insert(CookieDb, Cookie#http_cookie{max_age = session}); insert(#cookie_db{session_db = SessionDb} = CookieDb, #http_cookie{domain = Key, name = Name, path = Path, max_age = session} = Cookie) -> ?hcrt("insert session cookie", [{cookie, Cookie}]), Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}, case ets:match_object(SessionDb, Pattern) of [] -> ets:insert(SessionDb, Cookie); [NewCookie] -> delete(CookieDb, NewCookie), ets:insert(SessionDb, Cookie) end, ok; insert(#cookie_db{db = Db} = CookieDb, #http_cookie{domain = Key, name = Name, path = Path, max_age = 0}) -> ?hcrt("insert cookie", [{domain, Key}, {name, Name}, {path, Path}]), Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}, case dets:match_object(Db, Pattern) of [] -> ok; [NewCookie] -> delete(CookieDb, NewCookie) end, ok; insert(#cookie_db{db = Db} = CookieDb, #http_cookie{domain = Key, name = Name, path = Path} = Cookie) -> ?hcrt("insert cookie", [{cookie, Cookie}]), Pattern = #http_cookie{domain = Key, name = Name, path = Path, _ = '_'}, case dets:match_object(Db, Pattern) of [] -> dets:insert(Db, Cookie); [OldCookie] -> delete(CookieDb, OldCookie), dets:insert(Db, Cookie) end, ok. %%-------------------------------------------------------------------- %% Func: header(CookieDb) -> ok %% Purpose: Cookies %%-------------------------------------------------------------------- header(CookieDb, Scheme, {Host, _}, Path) -> ?hcrd("header", [{scheme, Scheme}, {host, Host}, {path, Path}]), case lookup_cookies(CookieDb, Host, Path) of [] -> {"cookie", ""}; Cookies -> %% print_cookies("Header Cookies", Cookies), {"cookie", cookies_to_string(Scheme, Cookies)} end. %%-------------------------------------------------------------------- %% Func: cookies(Headers, RequestPath, RequestHost) -> [cookie()] %% Purpose: Which cookies are stored %%-------------------------------------------------------------------- cookies(Headers, RequestPath, RequestHost) -> ?hcrt("cookies", [{headers, Headers}, {request_path, RequestPath}, {request_host, RequestHost}]), Cookies = parse_set_cookies(Headers, {RequestPath, RequestHost}), %% print_cookies("Parsed Cookies", Cookies), AcceptedCookies = accept_cookies(Cookies, RequestPath, RequestHost), %% print_cookies("Accepted Cookies", AcceptedCookies), AcceptedCookies. %%-------------------------------------------------------------------- %% Func: which_cookies(CookieDb) -> [cookie()] %% Purpose: For test and debug purpose, %% dump the entire cookie database %%-------------------------------------------------------------------- which_cookies(#cookie_db{db = undefined, session_db = SessionDb}) -> SessionCookies = ets:tab2list(SessionDb), [{session_cookies, SessionCookies}]; which_cookies(#cookie_db{db = Db, session_db = SessionDb}) -> Cookies = dets:match_object(Db, '_'), SessionCookies = ets:tab2list(SessionDb), [{cookies, Cookies}, {session_cookies, SessionCookies}]. %%%======================================================================== %%% Internal functions %%%======================================================================== delete(#cookie_db{session_db = SessionDb}, #http_cookie{max_age = session} = Cookie) -> ets:delete_object(SessionDb, Cookie); delete(#cookie_db{db = Db}, Cookie) -> dets:delete_object(Db, Cookie). lookup_cookies(#cookie_db{db = undefined, session_db = SessionDb}, Key) -> Pattern = #http_cookie{domain = Key, _ = '_'}, Cookies = ets:match_object(SessionDb, Pattern), ?hcrt("lookup cookies", [{cookies, Cookies}]), Cookies; lookup_cookies(#cookie_db{db = Db, session_db = SessionDb}, Key) -> Pattern = #http_cookie{domain = Key, _ = '_'}, SessionCookies = ets:match_object(SessionDb, Pattern), ?hcrt("lookup cookies", [{session_cookies, SessionCookies}]), Cookies = dets:match_object(Db, Pattern), ?hcrt("lookup cookies", [{cookies, Cookies}]), Cookies ++ SessionCookies. lookup_cookies(CookieDb, Host, Path) -> Cookies = case http_util:is_hostname(Host) of true -> HostCookies = lookup_cookies(CookieDb, Host), [_| DomainParts] = string:tokens(Host, "."), lookup_domain_cookies(CookieDb, DomainParts, HostCookies); false -> % IP-adress lookup_cookies(CookieDb, Host) end, ValidCookies = valid_cookies(CookieDb, Cookies), lists:filter(fun(Cookie) -> lists:prefix(Cookie#http_cookie.path, Path) end, ValidCookies). %% For instance if Host=localhost lookup_domain_cookies(_CookieDb, [], AccCookies) -> lists:flatten(AccCookies); %% Top domains can not have cookies lookup_domain_cookies(_CookieDb, [_], AccCookies) -> lists:flatten(AccCookies); lookup_domain_cookies(CookieDb, [Next | DomainParts], AccCookies) -> Domain = merge_domain_parts(DomainParts, [Next ++ "."]), lookup_domain_cookies(CookieDb, DomainParts, [lookup_cookies(CookieDb, Domain) | 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, [Cookie | _] = Cookies) -> 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 = Scheme, [#http_cookie{secure = true} = Cookie| Cookies], CookieStrs) -> Str = case Cookies of [] -> cookie_to_string(Cookie); _ -> cookie_to_string(Cookie) ++ "; " end, cookies_to_string(Scheme, 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(#http_cookie{name = Name, value = Value} = Cookie) -> 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. is_set_cookie_valid("") -> %% an empty Set-Cookie header is not valid false; is_set_cookie_valid([$=|_]) -> %% a Set-Cookie header without name is not valid false; is_set_cookie_valid(SetCookieHeader) -> %% a Set-Cookie header without name/value is not valid case string:chr(SetCookieHeader, $=) of 0 -> false; _ -> true end. parse_set_cookies(CookieHeaders, DefaultPathDomain) -> %% filter invalid Set-Cookie headers SetCookieHeaders = [Value || {"set-cookie", Value} <- CookieHeaders, is_set_cookie_valid(Value)], Cookies = [parse_set_cookie(SetCookieHeader, DefaultPathDomain) || SetCookieHeader <- SetCookieHeaders], %% print_cookies("Parsed Cookies", Cookies), Cookies. parse_set_cookie(CookieHeader, {DefaultPath, DefaultDomain}) -> %% io:format("Raw Cookie: ~s~n", [CookieHeader]), Pos = string:chr(CookieHeader, $=), Name = string:substr(CookieHeader, 1, Pos - 1), {Value, Attrs} = case string:substr(CookieHeader, Pos + 1) of [] -> {"", ""}; [$;|ValueAndAttrs] -> {"", string:tokens(ValueAndAttrs, ";")}; ValueAndAttrs -> [V | A] = string:tokens(ValueAndAttrs, ";"), {V, A} end, Cookie = #http_cookie{name = string:strip(Name), value = string:strip(Value)}, Attributes = parse_set_cookie_attributes(Attrs), TmpCookie = cookie_attributes(Attributes, Cookie), %% Add runtime defult values if necessary NewCookie = domain_default(path_default(TmpCookie, DefaultPath), DefaultDomain), NewCookie. parse_set_cookie_attributes(Attributes) when is_list(Attributes) -> [parse_set_cookie_attribute(A) || A <- Attributes]. parse_set_cookie_attribute(Attribute) -> {AName, AValue} = case string:tokens(Attribute, "=") of %% All attributes have the form %% Name=Value except "secure"! [Name] -> {Name, ""}; [Name, Value] -> {Name, Value}; %% Anything not expected will be %% disregarded _ -> {"Dummy", ""} end, StrippedName = http_util:to_lower(string:strip(AName)), StrippedValue = string:strip(AValue), {StrippedName, StrippedValue}. 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) -> try http_util:convert_netscapecookie_date(Value) of Time -> ExpireTime = calendar:datetime_to_gregorian_seconds(Time), cookie_attributes(Attributes, Cookie#http_cookie{max_age = ExpireTime}) catch _:_ -> cookie_attributes(Attributes, Cookie) end; 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(#http_cookie{path = undefined} = Cookie, 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 kept 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) -> Accepted = accept_path(Cookie, RequestPath) andalso accept_domain(Cookie, RequestHost), Accepted. 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(Db, Cookies) -> valid_cookies(Db, Cookies, []). valid_cookies(_Db, [], Valid) -> Valid; valid_cookies(Db, [Cookie | Cookies], Valid) -> case is_cookie_expired(Cookie) of true -> delete(Db, Cookie), valid_cookies(Db, Cookies, Valid); false -> valid_cookies(Db, Cookies, [Cookie | Valid]) end. path_sort(Cookies)-> lists:reverse(lists:keysort(#http_cookie.path, Cookies)). %% print_cookies(Header, Cookies) -> %% io:format("~s:~n", [Header]), %% Prefix = " ", %% lists:foreach(fun(Cookie) -> print(Prefix, Cookie) end, Cookies). image_of(Prefix, #http_cookie{domain = Domain, domain_default = DomainDef, name = Name, value = Value, comment = Comment, max_age = MaxAge, path = Path, path_default = PathDef, secure = Sec, version = Version}) -> lists:flatten( io_lib:format("~sCookie ~s: " "~n~s Value: ~p" "~n~s Domain: ~p" "~n~s DomainDef: ~p" "~n~s Comment: ~p" "~n~s MaxAge: ~p" "~n~s Path: ~p" "~n~s PathDef: ~p" "~n~s Secure: ~p" "~n~s Version: ~p", [Prefix, Name, Prefix, Value, Prefix, Domain, Prefix, DomainDef, Prefix, Comment, Prefix, MaxAge, Prefix, Path, Prefix, PathDef, Prefix, Sec, Prefix, Version])). print(Prefix, Cookie) when is_record(Cookie, http_cookie) -> io:format("~s~n", [image_of(Prefix, Cookie)]).