aboutsummaryrefslogblamecommitdiffstats
path: root/lib/inets/src/http_client/httpc_cookie.erl
blob: 4d61f82b5ab1297970a083a9bea1ee9bacd13f97 (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                   


                                                        




                                                                      
  



                                                                         
  



                                                     
                      


                               




                                                                 



                                                                            



























                                                                         

        




































                                                                      


                                                       











                                                                             
             
                                          
                      

                                         

        







                                                                             


                      
                                       

        







                                                                        
             



                                        


        












































                                                                      


                                                                           



















                                                                          

                              
 
                                       


                                            
                                                             
                                                            
                                                                          
                                
                                              
            
                                                    




                                                                     
                                                   
                              
 
                                   
                                                    
                              

                                                                        
                                                            

                                                                           





                                                          
                                                    










                                                                 
                                                                          





















                                                                   
                                                                      













                                                       





                                                             
    





                                                                           




























































                                                                          
                                                                

                                                           
                                                        

                                                         
                                                         

                                                              
                                                           









                                                                      
                                                                     
















                                                                        



                                                 







                                                                 













                                                                   













                                                                      




                                   

          
                                               

                                     

                                              
                
                                                        





                                                             




                                                                 
                                                      
                                                                    









                                                                  
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2004-2010. All Rights Reserved.
%%
%% 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 online 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.
%%
%% %CopyrightEnd%
%%
%% Description: Cookie handling according to RFC 2109

-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]). 

-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) -> ok
%% Purpose: Close 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", [{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}]),
    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 ->
	    {"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}),
    accept_cookies(Cookies, RequestPath, RequestHost).
	

%%--------------------------------------------------------------------
%% 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, [#http_cookie{secure = true} = Cookie| 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(#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.

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(#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 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) ->
    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)).


%% 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 fix that problem.
fix_netscape_cookie([Cookie1, Cookie2 | Rest], Acc) ->
    case inets_regexp:match(string:to_lower(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.