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