1/* 2 * Copyright 2010-2014 Haiku Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Adrien Destugues, pulkomandy@pulkomandy.tk 7 * Christophe Huriaux, c.huriaux@gmail.com 8 * Hamish Morrison, hamishm53@gmail.com 9 */ 10 11 12#include <new> 13 14#include <errno.h> 15#include <stdio.h> 16#include <stdlib.h> 17#include <time.h> 18 19#include <Debug.h> 20#include <HttpTime.h> 21#include <NetworkCookie.h> 22 23using namespace BPrivate::Network; 24 25 26static const char* kArchivedCookieName = "be:cookie.name"; 27static const char* kArchivedCookieValue = "be:cookie.value"; 28static const char* kArchivedCookieDomain = "be:cookie.domain"; 29static const char* kArchivedCookiePath = "be:cookie.path"; 30static const char* kArchivedCookieExpirationDate = "be:cookie.expirationdate"; 31static const char* kArchivedCookieSecure = "be:cookie.secure"; 32static const char* kArchivedCookieHttpOnly = "be:cookie.httponly"; 33static const char* kArchivedCookieHostOnly = "be:cookie.hostonly"; 34 35 36BNetworkCookie::BNetworkCookie(const char* name, const char* value, 37 const BUrl& url) 38{ 39 _Reset(); 40 fName = name; 41 fValue = value; 42 43 SetDomain(url.Host()); 44 45 if (url.Protocol() == "file" && url.Host().Length() == 0) { 46 SetDomain("localhost"); 47 // make sure cookies set from a file:// URL are stored somewhere. 48 } 49 50 SetPath(_DefaultPathForUrl(url)); 51} 52 53 54BNetworkCookie::BNetworkCookie(const BString& cookieString, const BUrl& url) 55{ 56 _Reset(); 57 fInitStatus = ParseCookieString(cookieString, url); 58} 59 60 61BNetworkCookie::BNetworkCookie(BMessage* archive) 62{ 63 _Reset(); 64 65 archive->FindString(kArchivedCookieName, &fName); 66 archive->FindString(kArchivedCookieValue, &fValue); 67 68 archive->FindString(kArchivedCookieDomain, &fDomain); 69 archive->FindString(kArchivedCookiePath, &fPath); 70 archive->FindBool(kArchivedCookieSecure, &fSecure); 71 archive->FindBool(kArchivedCookieHttpOnly, &fHttpOnly); 72 archive->FindBool(kArchivedCookieHostOnly, &fHostOnly); 73 74 // We store the expiration date as a string, which should not overflow. 75 // But we still parse the old archive format, where an int32 was used. 76 BString expirationString; 77 int32 expiration; 78 if (archive->FindString(kArchivedCookieExpirationDate, &expirationString) 79 == B_OK) { 80 BDateTime time = BHttpTime(expirationString).Parse(); 81 SetExpirationDate(time); 82 } else if (archive->FindInt32(kArchivedCookieExpirationDate, &expiration) 83 == B_OK) { 84 SetExpirationDate((time_t)expiration); 85 } 86} 87 88 89BNetworkCookie::BNetworkCookie() 90{ 91 _Reset(); 92} 93 94 95BNetworkCookie::~BNetworkCookie() 96{ 97} 98 99 100// #pragma mark String to cookie fields 101 102 103status_t 104BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url) 105{ 106 _Reset(); 107 108 // Set default values (these can be overriden later on) 109 SetPath(_DefaultPathForUrl(url)); 110 SetDomain(url.Host()); 111 fHostOnly = true; 112 if (url.Protocol() == "file" && url.Host().Length() == 0) { 113 fDomain = "localhost"; 114 // make sure cookies set from a file:// URL are stored somewhere. 115 // not going through SetDomain as it requires at least one '.' 116 // in the domain (to avoid setting cookies on TLDs). 117 } 118 119 BString name; 120 BString value; 121 int32 index = 0; 122 123 // Parse the name and value of the cookie 124 index = _ExtractNameValuePair(string, name, value, index); 125 if (index == -1 || value.Length() > 4096) { 126 // The set-cookie-string is not valid 127 return B_BAD_DATA; 128 } 129 130 SetName(name); 131 SetValue(value); 132 133 // Note on error handling: even if there are parse errors, we will continue 134 // and try to parse as much from the cookie as we can. 135 status_t result = B_OK; 136 137 // Parse the remaining cookie attributes. 138 while (index < string.Length()) { 139 ASSERT(string[index] == ';'); 140 index++; 141 142 index = _ExtractAttributeValuePair(string, name, value, index); 143 144 if (name.ICompare("secure") == 0) 145 SetSecure(true); 146 else if (name.ICompare("httponly") == 0) 147 SetHttpOnly(true); 148 149 // The following attributes require a value. 150 151 if (name.ICompare("max-age") == 0) { 152 if (value.IsEmpty()) { 153 result = B_BAD_VALUE; 154 continue; 155 } 156 // Validate the max-age value. 157 char* end = NULL; 158 errno = 0; 159 long maxAge = strtol(value.String(), &end, 10); 160 if (*end == '\0') 161 SetMaxAge((int)maxAge); 162 else if (errno == ERANGE && maxAge == LONG_MAX) 163 SetMaxAge(INT_MAX); 164 else 165 SetMaxAge(-1); // cookie will expire immediately 166 } else if (name.ICompare("expires") == 0) { 167 if (value.IsEmpty()) { 168 // Will be a session cookie. 169 continue; 170 } 171 BDateTime parsed = BHttpTime(value).Parse(); 172 SetExpirationDate(parsed); 173 } else if (name.ICompare("domain") == 0) { 174 if (value.IsEmpty()) { 175 result = B_BAD_VALUE; 176 continue; 177 } 178 179 status_t domainResult = SetDomain(value); 180 // Do not reset the result to B_OK if something else already failed 181 if (result == B_OK) 182 result = domainResult; 183 } else if (name.ICompare("path") == 0) { 184 if (value.IsEmpty()) { 185 result = B_BAD_VALUE; 186 continue; 187 } 188 status_t pathResult = SetPath(value); 189 if (result == B_OK) 190 result = pathResult; 191 } 192 } 193 194 if (!_CanBeSetFromUrl(url)) 195 result = B_NOT_ALLOWED; 196 197 if (result != B_OK) 198 _Reset(); 199 200 return result; 201} 202 203 204// #pragma mark Cookie fields modification 205 206 207BNetworkCookie& 208BNetworkCookie::SetName(const BString& name) 209{ 210 fName = name; 211 fRawFullCookieValid = false; 212 fRawCookieValid = false; 213 return *this; 214} 215 216 217BNetworkCookie& 218BNetworkCookie::SetValue(const BString& value) 219{ 220 fValue = value; 221 fRawFullCookieValid = false; 222 fRawCookieValid = false; 223 return *this; 224} 225 226 227status_t 228BNetworkCookie::SetPath(const BString& to) 229{ 230 fPath.Truncate(0); 231 fRawFullCookieValid = false; 232 233 // Limit the path to 4096 characters to not let the cookie jar grow huge. 234 if (to[0] != '/' || to.Length() > 4096) 235 return B_BAD_DATA; 236 237 // Check that there aren't any "." or ".." segments in the path. 238 if (to.EndsWith("/.") || to.EndsWith("/..")) 239 return B_BAD_DATA; 240 if (to.FindFirst("/../") >= 0 || to.FindFirst("/./") >= 0) 241 return B_BAD_DATA; 242 243 fPath = to; 244 return B_OK; 245} 246 247 248status_t 249BNetworkCookie::SetDomain(const BString& domain) 250{ 251 // TODO: canonicalize the domain 252 BString newDomain = domain; 253 254 // RFC 2109 (legacy) support: domain string may start with a dot, 255 // meant to indicate the cookie should also be used for subdomains. 256 // RFC 6265 makes all cookies work for subdomains, unless the domain is 257 // not specified at all (in this case it has to exactly match the Url of 258 // the page that set the cookie). In any case, we don't need to handle 259 // dot-cookies specifically anymore, so just remove the extra dot. 260 if (newDomain[0] == '.') 261 newDomain.Remove(0, 1); 262 263 // check we're not trying to set a cookie on a TLD or empty domain 264 if (newDomain.FindLast('.') <= 0) 265 return B_BAD_DATA; 266 267 fDomain = newDomain.ToLower(); 268 269 fHostOnly = false; 270 271 fRawFullCookieValid = false; 272 return B_OK; 273} 274 275 276BNetworkCookie& 277BNetworkCookie::SetMaxAge(int32 maxAge) 278{ 279 BDateTime expiration = BDateTime::CurrentDateTime(B_LOCAL_TIME); 280 281 // Compute the expiration date (watch out for overflows) 282 int64_t date = expiration.Time_t(); 283 date += (int64_t)maxAge; 284 if (date > INT_MAX) 285 date = INT_MAX; 286 287 expiration.SetTime_t(date); 288 289 return SetExpirationDate(expiration); 290} 291 292 293BNetworkCookie& 294BNetworkCookie::SetExpirationDate(time_t expireDate) 295{ 296 BDateTime expiration; 297 expiration.SetTime_t(expireDate); 298 return SetExpirationDate(expiration); 299} 300 301 302BNetworkCookie& 303BNetworkCookie::SetExpirationDate(BDateTime& expireDate) 304{ 305 if (!expireDate.IsValid()) { 306 fExpiration.SetTime_t(0); 307 fSessionCookie = true; 308 } else { 309 fExpiration = expireDate; 310 fSessionCookie = false; 311 } 312 313 fExpirationStringValid = false; 314 fRawFullCookieValid = false; 315 316 return *this; 317} 318 319 320BNetworkCookie& 321BNetworkCookie::SetSecure(bool secure) 322{ 323 fSecure = secure; 324 fRawFullCookieValid = false; 325 return *this; 326} 327 328 329BNetworkCookie& 330BNetworkCookie::SetHttpOnly(bool httpOnly) 331{ 332 fHttpOnly = httpOnly; 333 fRawFullCookieValid = false; 334 return *this; 335} 336 337 338// #pragma mark Cookie fields access 339 340 341const BString& 342BNetworkCookie::Name() const 343{ 344 return fName; 345} 346 347 348const BString& 349BNetworkCookie::Value() const 350{ 351 return fValue; 352} 353 354 355const BString& 356BNetworkCookie::Domain() const 357{ 358 return fDomain; 359} 360 361 362const BString& 363BNetworkCookie::Path() const 364{ 365 return fPath; 366} 367 368 369time_t 370BNetworkCookie::ExpirationDate() const 371{ 372 return fExpiration.Time_t(); 373} 374 375 376const BString& 377BNetworkCookie::ExpirationString() const 378{ 379 BHttpTime date(fExpiration); 380 381 if (!fExpirationStringValid) { 382 fExpirationString = date.ToString(B_HTTP_TIME_FORMAT_COOKIE); 383 fExpirationStringValid = true; 384 } 385 386 return fExpirationString; 387} 388 389 390bool 391BNetworkCookie::Secure() const 392{ 393 return fSecure; 394} 395 396 397bool 398BNetworkCookie::HttpOnly() const 399{ 400 return fHttpOnly; 401} 402 403 404const BString& 405BNetworkCookie::RawCookie(bool full) const 406{ 407 if (!fRawCookieValid) { 408 fRawCookie.Truncate(0); 409 fRawCookieValid = true; 410 411 fRawCookie << fName << "=" << fValue; 412 } 413 414 if (!full) 415 return fRawCookie; 416 417 if (!fRawFullCookieValid) { 418 fRawFullCookie = fRawCookie; 419 fRawFullCookieValid = true; 420 421 if (HasDomain()) 422 fRawFullCookie << "; Domain=" << fDomain; 423 if (HasExpirationDate()) 424 fRawFullCookie << "; Expires=" << ExpirationString(); 425 if (HasPath()) 426 fRawFullCookie << "; Path=" << fPath; 427 if (Secure()) 428 fRawFullCookie << "; Secure"; 429 if (HttpOnly()) 430 fRawFullCookie << "; HttpOnly"; 431 432 } 433 434 return fRawFullCookie; 435} 436 437 438// #pragma mark Cookie test 439 440 441bool 442BNetworkCookie::IsHostOnly() const 443{ 444 return fHostOnly; 445} 446 447 448bool 449BNetworkCookie::IsSessionCookie() const 450{ 451 return fSessionCookie; 452} 453 454 455bool 456BNetworkCookie::IsValid() const 457{ 458 return fInitStatus == B_OK && HasName() && HasDomain(); 459} 460 461 462bool 463BNetworkCookie::IsValidForUrl(const BUrl& url) const 464{ 465 if (Secure() && url.Protocol() != "https") 466 return false; 467 468 if (url.Protocol() == "file") 469 return Domain() == "localhost" && IsValidForPath(url.Path()); 470 471 return IsValidForDomain(url.Host()) && IsValidForPath(url.Path()); 472} 473 474 475bool 476BNetworkCookie::IsValidForDomain(const BString& domain) const 477{ 478 // TODO: canonicalize both domains 479 const BString& cookieDomain = Domain(); 480 481 int32 difference = domain.Length() - cookieDomain.Length(); 482 // If the cookie domain is longer than the domain string it cannot 483 // be valid. 484 if (difference < 0) 485 return false; 486 487 // If the cookie is host-only the domains must match exactly. 488 if (IsHostOnly()) 489 return domain == cookieDomain; 490 491 // FIXME do not do substring matching on IP addresses. The RFCs disallow it. 492 493 // Otherwise, the domains must match exactly, or the domain must have a dot 494 // character just before the common suffix. 495 const char* suffix = domain.String() + difference; 496 return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0 497 || domain[difference - 1] == '.')); 498} 499 500 501bool 502BNetworkCookie::IsValidForPath(const BString& path) const 503{ 504 const BString& cookiePath = Path(); 505 BString normalizedPath = path; 506 int slashPos = normalizedPath.FindLast('/'); 507 if (slashPos != normalizedPath.Length() - 1) 508 normalizedPath.Truncate(slashPos + 1); 509 510 if (normalizedPath.Length() < cookiePath.Length()) 511 return false; 512 513 // The cookie path must be a prefix of the path string 514 return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0; 515} 516 517 518bool 519BNetworkCookie::_CanBeSetFromUrl(const BUrl& url) const 520{ 521 if (url.Protocol() == "file") 522 return Domain() == "localhost" && _CanBeSetFromPath(url.Path()); 523 524 return _CanBeSetFromDomain(url.Host()) && _CanBeSetFromPath(url.Path()); 525} 526 527 528bool 529BNetworkCookie::_CanBeSetFromDomain(const BString& domain) const 530{ 531 // TODO: canonicalize both domains 532 const BString& cookieDomain = Domain(); 533 534 int32 difference = domain.Length() - cookieDomain.Length(); 535 if (difference < 0) { 536 // Setting a cookie on a subdomain is allowed. 537 const char* suffix = cookieDomain.String() + difference; 538 return (strcmp(suffix, domain.String()) == 0 && (difference == 0 539 || cookieDomain[difference - 1] == '.')); 540 } 541 542 // If the cookie is host-only the domains must match exactly. 543 if (IsHostOnly()) 544 return domain == cookieDomain; 545 546 // FIXME prevent supercookies with a domain of ".com" or similar 547 // This is NOT as straightforward as relying on the last dot in the domain. 548 // Here's a list of TLD: 549 // https://github.com/rsimoes/Mozilla-PublicSuffix/blob/master/effective_tld_names.dat 550 551 // FIXME do not do substring matching on IP addresses. The RFCs disallow it. 552 553 // Otherwise, the domains must match exactly, or the domain must have a dot 554 // character just before the common suffix. 555 const char* suffix = domain.String() + difference; 556 return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0 557 || domain[difference - 1] == '.')); 558} 559 560 561bool 562BNetworkCookie::_CanBeSetFromPath(const BString& path) const 563{ 564 BString normalizedPath = path; 565 int slashPos = normalizedPath.FindLast('/'); 566 normalizedPath.Truncate(slashPos); 567 568 if (Path().Compare(normalizedPath, normalizedPath.Length()) == 0) 569 return true; 570 else if (normalizedPath.Compare(Path(), Path().Length()) == 0) 571 return true; 572 return false; 573} 574 575 576// #pragma mark Cookie fields existence tests 577 578 579bool 580BNetworkCookie::HasName() const 581{ 582 return fName.Length() > 0; 583} 584 585 586bool 587BNetworkCookie::HasValue() const 588{ 589 return fValue.Length() > 0; 590} 591 592 593bool 594BNetworkCookie::HasDomain() const 595{ 596 return fDomain.Length() > 0; 597} 598 599 600bool 601BNetworkCookie::HasPath() const 602{ 603 return fPath.Length() > 0; 604} 605 606 607bool 608BNetworkCookie::HasExpirationDate() const 609{ 610 return !IsSessionCookie(); 611} 612 613 614// #pragma mark Cookie delete test 615 616 617bool 618BNetworkCookie::ShouldDeleteAtExit() const 619{ 620 return IsSessionCookie() || ShouldDeleteNow(); 621} 622 623 624bool 625BNetworkCookie::ShouldDeleteNow() const 626{ 627 if (HasExpirationDate()) 628 return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration); 629 630 return false; 631} 632 633 634// #pragma mark BArchivable members 635 636 637status_t 638BNetworkCookie::Archive(BMessage* into, bool deep) const 639{ 640 status_t error = BArchivable::Archive(into, deep); 641 642 if (error != B_OK) 643 return error; 644 645 error = into->AddString(kArchivedCookieName, fName); 646 if (error != B_OK) 647 return error; 648 649 error = into->AddString(kArchivedCookieValue, fValue); 650 if (error != B_OK) 651 return error; 652 653 654 // We add optional fields only if they're defined 655 if (HasDomain()) { 656 error = into->AddString(kArchivedCookieDomain, fDomain); 657 if (error != B_OK) 658 return error; 659 } 660 661 if (HasExpirationDate()) { 662 error = into->AddString(kArchivedCookieExpirationDate, 663 BHttpTime(fExpiration).ToString()); 664 if (error != B_OK) 665 return error; 666 } 667 668 if (HasPath()) { 669 error = into->AddString(kArchivedCookiePath, fPath); 670 if (error != B_OK) 671 return error; 672 } 673 674 if (Secure()) { 675 error = into->AddBool(kArchivedCookieSecure, fSecure); 676 if (error != B_OK) 677 return error; 678 } 679 680 if (HttpOnly()) { 681 error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly); 682 if (error != B_OK) 683 return error; 684 } 685 686 if (IsHostOnly()) { 687 error = into->AddBool(kArchivedCookieHostOnly, true); 688 if (error != B_OK) 689 return error; 690 } 691 692 return B_OK; 693} 694 695 696/*static*/ BArchivable* 697BNetworkCookie::Instantiate(BMessage* archive) 698{ 699 if (archive->HasString(kArchivedCookieName) 700 && archive->HasString(kArchivedCookieValue)) 701 return new(std::nothrow) BNetworkCookie(archive); 702 703 return NULL; 704} 705 706 707// #pragma mark Overloaded operators 708 709 710bool 711BNetworkCookie::operator==(const BNetworkCookie& other) 712{ 713 // Equality : name and values equals 714 return fName == other.fName && fValue == other.fValue; 715} 716 717 718bool 719BNetworkCookie::operator!=(const BNetworkCookie& other) 720{ 721 return !(*this == other); 722} 723 724 725void 726BNetworkCookie::_Reset() 727{ 728 fInitStatus = false; 729 730 fName.Truncate(0); 731 fValue.Truncate(0); 732 fDomain.Truncate(0); 733 fPath.Truncate(0); 734 fExpiration = BDateTime(); 735 fSecure = false; 736 fHttpOnly = false; 737 738 fSessionCookie = true; 739 fHostOnly = true; 740 741 fRawCookieValid = false; 742 fRawFullCookieValid = false; 743 fExpirationStringValid = false; 744} 745 746 747int32 748skip_whitespace_forward(const BString& string, int32 index) 749{ 750 while (index < string.Length() && (string[index] == ' ' 751 || string[index] == '\t')) 752 index++; 753 return index; 754} 755 756 757int32 758skip_whitespace_backward(const BString& string, int32 index) 759{ 760 while (index >= 0 && (string[index] == ' ' || string[index] == '\t')) 761 index--; 762 return index; 763} 764 765 766int32 767BNetworkCookie::_ExtractNameValuePair(const BString& cookieString, 768 BString& name, BString& value, int32 index) 769{ 770 // Find our name-value-pair and the delimiter. 771 int32 firstEquals = cookieString.FindFirst('=', index); 772 int32 nameValueEnd = cookieString.FindFirst(';', index); 773 774 // If the set-cookie-string lacks a semicolon, the name-value-pair 775 // is the whole string. 776 if (nameValueEnd == -1) 777 nameValueEnd = cookieString.Length(); 778 779 // If the name-value-pair lacks an equals, the parse should fail. 780 if (firstEquals == -1 || firstEquals > nameValueEnd) 781 return -1; 782 783 int32 first = skip_whitespace_forward(cookieString, index); 784 int32 last = skip_whitespace_backward(cookieString, firstEquals - 1); 785 786 // If we lack a name, fail to parse. 787 if (first > last) 788 return -1; 789 790 cookieString.CopyInto(name, first, last - first + 1); 791 792 first = skip_whitespace_forward(cookieString, firstEquals + 1); 793 last = skip_whitespace_backward(cookieString, nameValueEnd - 1); 794 if (first <= last) 795 cookieString.CopyInto(value, first, last - first + 1); 796 else 797 value.SetTo(""); 798 799 return nameValueEnd; 800} 801 802 803int32 804BNetworkCookie::_ExtractAttributeValuePair(const BString& cookieString, 805 BString& attribute, BString& value, int32 index) 806{ 807 // Find the end of our cookie-av. 808 int32 cookieAVEnd = cookieString.FindFirst(';', index); 809 810 // If the unparsed-attributes lacks a semicolon, then the cookie-av is the 811 // whole string. 812 if (cookieAVEnd == -1) 813 cookieAVEnd = cookieString.Length(); 814 815 int32 attributeNameEnd = cookieString.FindFirst('=', index); 816 // If the cookie-av has no equals, the attribute-name is the entire 817 // cookie-av and the attribute-value is empty. 818 if (attributeNameEnd == -1 || attributeNameEnd > cookieAVEnd) 819 attributeNameEnd = cookieAVEnd; 820 821 int32 first = skip_whitespace_forward(cookieString, index); 822 int32 last = skip_whitespace_backward(cookieString, attributeNameEnd - 1); 823 824 if (first <= last) 825 cookieString.CopyInto(attribute, first, last - first + 1); 826 else 827 attribute.SetTo(""); 828 829 if (attributeNameEnd == cookieAVEnd) { 830 value.SetTo(""); 831 return cookieAVEnd; 832 } 833 834 first = skip_whitespace_forward(cookieString, attributeNameEnd + 1); 835 last = skip_whitespace_backward(cookieString, cookieAVEnd - 1); 836 if (first <= last) 837 cookieString.CopyInto(value, first, last - first + 1); 838 else 839 value.SetTo(""); 840 841 // values may (or may not) have quotes around them. 842 if (value[0] == '"' && value[value.Length() - 1] == '"') { 843 value.Remove(0, 1); 844 value.Remove(value.Length() - 1, 1); 845 } 846 847 return cookieAVEnd; 848} 849 850 851BString 852BNetworkCookie::_DefaultPathForUrl(const BUrl& url) 853{ 854 const BString& path = url.Path(); 855 if (path.IsEmpty() || path.ByteAt(0) != '/') 856 return ""; 857 858 int32 index = path.FindLast('/'); 859 if (index == 0) 860 return ""; 861 862 BString newPath = path; 863 newPath.Truncate(index); 864 return newPath; 865} 866