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 23 using namespace BPrivate::Network; 24 25 26 static const char* kArchivedCookieName = "be:cookie.name"; 27 static const char* kArchivedCookieValue = "be:cookie.value"; 28 static const char* kArchivedCookieDomain = "be:cookie.domain"; 29 static const char* kArchivedCookiePath = "be:cookie.path"; 30 static const char* kArchivedCookieExpirationDate = "be:cookie.expirationdate"; 31 static const char* kArchivedCookieSecure = "be:cookie.secure"; 32 static const char* kArchivedCookieHttpOnly = "be:cookie.httponly"; 33 static const char* kArchivedCookieHostOnly = "be:cookie.hostonly"; 34 35 36 BNetworkCookie::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 54 BNetworkCookie::BNetworkCookie(const BString& cookieString, const BUrl& url) 55 { 56 _Reset(); 57 fInitStatus = ParseCookieString(cookieString, url); 58 } 59 60 61 BNetworkCookie::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 89 BNetworkCookie::BNetworkCookie() 90 { 91 _Reset(); 92 } 93 94 95 BNetworkCookie::~BNetworkCookie() 96 { 97 } 98 99 100 // #pragma mark String to cookie fields 101 102 103 status_t 104 BNetworkCookie::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 207 BNetworkCookie& 208 BNetworkCookie::SetName(const BString& name) 209 { 210 fName = name; 211 fRawFullCookieValid = false; 212 fRawCookieValid = false; 213 return *this; 214 } 215 216 217 BNetworkCookie& 218 BNetworkCookie::SetValue(const BString& value) 219 { 220 fValue = value; 221 fRawFullCookieValid = false; 222 fRawCookieValid = false; 223 return *this; 224 } 225 226 227 status_t 228 BNetworkCookie::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 248 status_t 249 BNetworkCookie::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 276 BNetworkCookie& 277 BNetworkCookie::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 293 BNetworkCookie& 294 BNetworkCookie::SetExpirationDate(time_t expireDate) 295 { 296 BDateTime expiration; 297 expiration.SetTime_t(expireDate); 298 return SetExpirationDate(expiration); 299 } 300 301 302 BNetworkCookie& 303 BNetworkCookie::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 320 BNetworkCookie& 321 BNetworkCookie::SetSecure(bool secure) 322 { 323 fSecure = secure; 324 fRawFullCookieValid = false; 325 return *this; 326 } 327 328 329 BNetworkCookie& 330 BNetworkCookie::SetHttpOnly(bool httpOnly) 331 { 332 fHttpOnly = httpOnly; 333 fRawFullCookieValid = false; 334 return *this; 335 } 336 337 338 // #pragma mark Cookie fields access 339 340 341 const BString& 342 BNetworkCookie::Name() const 343 { 344 return fName; 345 } 346 347 348 const BString& 349 BNetworkCookie::Value() const 350 { 351 return fValue; 352 } 353 354 355 const BString& 356 BNetworkCookie::Domain() const 357 { 358 return fDomain; 359 } 360 361 362 const BString& 363 BNetworkCookie::Path() const 364 { 365 return fPath; 366 } 367 368 369 time_t 370 BNetworkCookie::ExpirationDate() const 371 { 372 return fExpiration.Time_t(); 373 } 374 375 376 const BString& 377 BNetworkCookie::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 390 bool 391 BNetworkCookie::Secure() const 392 { 393 return fSecure; 394 } 395 396 397 bool 398 BNetworkCookie::HttpOnly() const 399 { 400 return fHttpOnly; 401 } 402 403 404 const BString& 405 BNetworkCookie::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 441 bool 442 BNetworkCookie::IsHostOnly() const 443 { 444 return fHostOnly; 445 } 446 447 448 bool 449 BNetworkCookie::IsSessionCookie() const 450 { 451 return fSessionCookie; 452 } 453 454 455 bool 456 BNetworkCookie::IsValid() const 457 { 458 return fInitStatus == B_OK && HasName() && HasDomain(); 459 } 460 461 462 bool 463 BNetworkCookie::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 475 bool 476 BNetworkCookie::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 501 bool 502 BNetworkCookie::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 518 bool 519 BNetworkCookie::_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 528 bool 529 BNetworkCookie::_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 561 bool 562 BNetworkCookie::_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 579 bool 580 BNetworkCookie::HasName() const 581 { 582 return fName.Length() > 0; 583 } 584 585 586 bool 587 BNetworkCookie::HasValue() const 588 { 589 return fValue.Length() > 0; 590 } 591 592 593 bool 594 BNetworkCookie::HasDomain() const 595 { 596 return fDomain.Length() > 0; 597 } 598 599 600 bool 601 BNetworkCookie::HasPath() const 602 { 603 return fPath.Length() > 0; 604 } 605 606 607 bool 608 BNetworkCookie::HasExpirationDate() const 609 { 610 return !IsSessionCookie(); 611 } 612 613 614 // #pragma mark Cookie delete test 615 616 617 bool 618 BNetworkCookie::ShouldDeleteAtExit() const 619 { 620 return IsSessionCookie() || ShouldDeleteNow(); 621 } 622 623 624 bool 625 BNetworkCookie::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 637 status_t 638 BNetworkCookie::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* 697 BNetworkCookie::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 710 bool 711 BNetworkCookie::operator==(const BNetworkCookie& other) 712 { 713 // Equality : name and values equals 714 return fName == other.fName && fValue == other.fValue; 715 } 716 717 718 bool 719 BNetworkCookie::operator!=(const BNetworkCookie& other) 720 { 721 return !(*this == other); 722 } 723 724 725 void 726 BNetworkCookie::_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 747 int32 748 skip_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 757 int32 758 skip_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 766 int32 767 BNetworkCookie::_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 803 int32 804 BNetworkCookie::_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 851 BString 852 BNetworkCookie::_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