summaryrefslogtreecommitdiff
path: root/src/couch_inets/http_cookie.erl
diff options
context:
space:
mode:
authorChristopher Lenz <cmlenz@apache.org>2008-03-28 23:32:19 +0000
committerChristopher Lenz <cmlenz@apache.org>2008-03-28 23:32:19 +0000
commit544a38dd45f6a58d34296c6c768afd086eb2ac70 (patch)
treec84cc02340b06aae189cff0dbfaee698f273f1f5 /src/couch_inets/http_cookie.erl
parent804cbbe033b8e7a3e8d7058aaf31bdf69ef18ac5 (diff)
Imported trunk.
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@642432 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/couch_inets/http_cookie.erl')
-rw-r--r--src/couch_inets/http_cookie.erl389
1 files changed, 389 insertions, 0 deletions
diff --git a/src/couch_inets/http_cookie.erl b/src/couch_inets/http_cookie.erl
new file mode 100644
index 00000000..a8e68651
--- /dev/null
+++ b/src/couch_inets/http_cookie.erl
@@ -0,0 +1,389 @@
+%% ``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.