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