2 * Copyright 2010-2014 Haiku Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
6 * Adrien Destugues, pulkomandy@pulkomandy.tk
7 * Christophe Huriaux, c.huriaux@gmail.com
8 * Hamish Morrison, hamishm53@gmail.com
20 #include <NetworkCookie.h>
23 using BPrivate::BHttpTime
;
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";
36 BNetworkCookie::BNetworkCookie(const char* name
, const char* value
,
43 SetDomain(url
.Host());
45 if (url
.Protocol() == "file" && url
.Host().Length() == 0) {
46 SetDomain("localhost");
47 // make sure cookies set from a file:// URL are stored somewhere.
50 SetPath(_DefaultPathForUrl(url
));
54 BNetworkCookie::BNetworkCookie(const BString
& cookieString
, const BUrl
& url
)
57 fInitStatus
= ParseCookieString(cookieString
, url
);
61 BNetworkCookie::BNetworkCookie(BMessage
* archive
)
65 archive
->FindString(kArchivedCookieName
, &fName
);
66 archive
->FindString(kArchivedCookieValue
, &fValue
);
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
);
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
;
78 if (archive
->FindString(kArchivedCookieExpirationDate
, &expirationString
)
80 BDateTime time
= BHttpTime(expirationString
).Parse();
81 SetExpirationDate(time
);
82 } else if (archive
->FindInt32(kArchivedCookieExpirationDate
, &expiration
)
84 SetExpirationDate((time_t)expiration
);
89 BNetworkCookie::BNetworkCookie()
95 BNetworkCookie::~BNetworkCookie()
100 // #pragma mark String to cookie fields
104 BNetworkCookie::ParseCookieString(const BString
& string
, const BUrl
& url
)
108 // Set default values (these can be overriden later on)
109 SetPath(_DefaultPathForUrl(url
));
110 SetDomain(url
.Host());
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).
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
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
;
137 // Parse the remaining cookie attributes.
138 while (index
< string
.Length()) {
139 ASSERT(string
[index
] == ';');
142 index
= _ExtractAttributeValuePair(string
, name
, value
, index
);
144 if (name
.ICompare("secure") == 0)
146 else if (name
.ICompare("httponly") == 0)
149 // The following attributes require a value.
151 if (name
.ICompare("max-age") == 0) {
152 if (value
.IsEmpty()) {
153 result
= B_BAD_VALUE
;
156 // Validate the max-age value.
159 long maxAge
= strtol(value
.String(), &end
, 10);
161 SetMaxAge((int)maxAge
);
162 else if (errno
== ERANGE
&& maxAge
== LONG_MAX
)
165 SetMaxAge(-1); // cookie will expire immediately
166 } else if (name
.ICompare("expires") == 0) {
167 if (value
.IsEmpty()) {
168 // Will be a session cookie.
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
;
179 status_t domainResult
= SetDomain(value
);
180 // Do not reset the result to B_OK if something else already failed
182 result
= domainResult
;
183 } else if (name
.ICompare("path") == 0) {
184 if (value
.IsEmpty()) {
185 result
= B_BAD_VALUE
;
188 status_t pathResult
= SetPath(value
);
194 if (!_CanBeSetFromUrl(url
))
195 result
= B_NOT_ALLOWED
;
204 // #pragma mark Cookie fields modification
208 BNetworkCookie::SetName(const BString
& name
)
211 fRawFullCookieValid
= false;
212 fRawCookieValid
= false;
218 BNetworkCookie::SetValue(const BString
& value
)
221 fRawFullCookieValid
= false;
222 fRawCookieValid
= false;
228 BNetworkCookie::SetPath(const BString
& to
)
231 fRawFullCookieValid
= false;
233 // Limit the path to 4096 characters to not let the cookie jar grow huge.
234 if (to
[0] != '/' || to
.Length() > 4096)
237 // Check that there aren't any "." or ".." segments in the path.
238 if (to
.EndsWith("/.") || to
.EndsWith("/.."))
240 if (to
.FindFirst("/../") >= 0 || to
.FindFirst("/./") >= 0)
249 BNetworkCookie::SetDomain(const BString
& domain
)
251 // TODO: canonicalize the domain
252 BString newDomain
= domain
;
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);
263 // check we're not trying to set a cookie on a TLD or empty domain
264 if (newDomain
.FindLast('.') <= 0)
267 fDomain
= newDomain
.ToLower();
271 fRawFullCookieValid
= false;
277 BNetworkCookie::SetMaxAge(int32 maxAge
)
279 BDateTime expiration
= BDateTime::CurrentDateTime(B_LOCAL_TIME
);
281 // Compute the expiration date (watch out for overflows)
282 int64_t date
= expiration
.Time_t();
283 date
+= (int64_t)maxAge
;
287 expiration
.SetTime_t(date
);
289 return SetExpirationDate(expiration
);
294 BNetworkCookie::SetExpirationDate(time_t expireDate
)
296 BDateTime expiration
;
297 expiration
.SetTime_t(expireDate
);
298 return SetExpirationDate(expiration
);
303 BNetworkCookie::SetExpirationDate(BDateTime
& expireDate
)
305 if (!expireDate
.IsValid()) {
306 fExpiration
.SetTime_t(0);
307 fSessionCookie
= true;
309 fExpiration
= expireDate
;
310 fSessionCookie
= false;
313 fExpirationStringValid
= false;
314 fRawFullCookieValid
= false;
321 BNetworkCookie::SetSecure(bool secure
)
324 fRawFullCookieValid
= false;
330 BNetworkCookie::SetHttpOnly(bool httpOnly
)
332 fHttpOnly
= httpOnly
;
333 fRawFullCookieValid
= false;
338 // #pragma mark Cookie fields access
342 BNetworkCookie::Name() const
349 BNetworkCookie::Value() const
356 BNetworkCookie::Domain() const
363 BNetworkCookie::Path() const
370 BNetworkCookie::ExpirationDate() const
372 return fExpiration
.Time_t();
377 BNetworkCookie::ExpirationString() const
379 BHttpTime
date(fExpiration
);
381 if (!fExpirationStringValid
) {
382 fExpirationString
= date
.ToString(BPrivate::B_HTTP_TIME_FORMAT_COOKIE
);
383 fExpirationStringValid
= true;
386 return fExpirationString
;
391 BNetworkCookie::Secure() const
398 BNetworkCookie::HttpOnly() const
405 BNetworkCookie::RawCookie(bool full
) const
407 if (!fRawCookieValid
) {
408 fRawCookie
.Truncate(0);
409 fRawCookieValid
= true;
411 fRawCookie
<< fName
<< "=" << fValue
;
417 if (!fRawFullCookieValid
) {
418 fRawFullCookie
= fRawCookie
;
419 fRawFullCookieValid
= true;
422 fRawFullCookie
<< "; Domain=" << fDomain
;
423 if (HasExpirationDate())
424 fRawFullCookie
<< "; Expires=" << ExpirationString();
426 fRawFullCookie
<< "; Path=" << fPath
;
428 fRawFullCookie
<< "; Secure";
430 fRawFullCookie
<< "; HttpOnly";
434 return fRawFullCookie
;
438 // #pragma mark Cookie test
442 BNetworkCookie::IsHostOnly() const
449 BNetworkCookie::IsSessionCookie() const
451 return fSessionCookie
;
456 BNetworkCookie::IsValid() const
458 return fInitStatus
== B_OK
&& HasName() && HasDomain();
463 BNetworkCookie::IsValidForUrl(const BUrl
& url
) const
465 if (Secure() && url
.Protocol() != "https")
468 if (url
.Protocol() == "file")
469 return Domain() == "localhost" && IsValidForPath(url
.Path());
471 return IsValidForDomain(url
.Host()) && IsValidForPath(url
.Path());
476 BNetworkCookie::IsValidForDomain(const BString
& domain
) const
478 // TODO: canonicalize both domains
479 const BString
& cookieDomain
= Domain();
481 int32 difference
= domain
.Length() - cookieDomain
.Length();
482 // If the cookie domain is longer than the domain string it cannot
487 // If the cookie is host-only the domains must match exactly.
489 return domain
== cookieDomain
;
491 // FIXME do not do substring matching on IP addresses. The RFCs disallow it.
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] == '.'));
502 BNetworkCookie::IsValidForPath(const BString
& path
) const
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);
510 if (normalizedPath
.Length() < cookiePath
.Length())
513 // The cookie path must be a prefix of the path string
514 return normalizedPath
.Compare(cookiePath
, cookiePath
.Length()) == 0;
519 BNetworkCookie::_CanBeSetFromUrl(const BUrl
& url
) const
521 if (url
.Protocol() == "file")
522 return Domain() == "localhost" && _CanBeSetFromPath(url
.Path());
524 return _CanBeSetFromDomain(url
.Host()) && _CanBeSetFromPath(url
.Path());
529 BNetworkCookie::_CanBeSetFromDomain(const BString
& domain
) const
531 // TODO: canonicalize both domains
532 const BString
& cookieDomain
= Domain();
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] == '.'));
542 // If the cookie is host-only the domains must match exactly.
544 return domain
== cookieDomain
;
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
551 // FIXME do not do substring matching on IP addresses. The RFCs disallow it.
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] == '.'));
562 BNetworkCookie::_CanBeSetFromPath(const BString
& path
) const
564 BString normalizedPath
= path
;
565 int slashPos
= normalizedPath
.FindLast('/');
566 normalizedPath
.Truncate(slashPos
);
568 if (Path().Compare(normalizedPath
, normalizedPath
.Length()) == 0)
570 else if (normalizedPath
.Compare(Path(), Path().Length()) == 0)
576 // #pragma mark Cookie fields existence tests
580 BNetworkCookie::HasName() const
582 return fName
.Length() > 0;
587 BNetworkCookie::HasValue() const
589 return fValue
.Length() > 0;
594 BNetworkCookie::HasDomain() const
596 return fDomain
.Length() > 0;
601 BNetworkCookie::HasPath() const
603 return fPath
.Length() > 0;
608 BNetworkCookie::HasExpirationDate() const
610 return !IsSessionCookie();
614 // #pragma mark Cookie delete test
618 BNetworkCookie::ShouldDeleteAtExit() const
620 return IsSessionCookie() || ShouldDeleteNow();
625 BNetworkCookie::ShouldDeleteNow() const
627 if (HasExpirationDate())
628 return (BDateTime::CurrentDateTime(B_GMT_TIME
) > fExpiration
);
634 // #pragma mark BArchivable members
638 BNetworkCookie::Archive(BMessage
* into
, bool deep
) const
640 status_t error
= BArchivable::Archive(into
, deep
);
645 error
= into
->AddString(kArchivedCookieName
, fName
);
649 error
= into
->AddString(kArchivedCookieValue
, fValue
);
654 // We add optional fields only if they're defined
656 error
= into
->AddString(kArchivedCookieDomain
, fDomain
);
661 if (HasExpirationDate()) {
662 error
= into
->AddString(kArchivedCookieExpirationDate
,
663 BHttpTime(fExpiration
).ToString());
669 error
= into
->AddString(kArchivedCookiePath
, fPath
);
675 error
= into
->AddBool(kArchivedCookieSecure
, fSecure
);
681 error
= into
->AddBool(kArchivedCookieHttpOnly
, fHttpOnly
);
687 error
= into
->AddBool(kArchivedCookieHostOnly
, true);
696 /*static*/ BArchivable
*
697 BNetworkCookie::Instantiate(BMessage
* archive
)
699 if (archive
->HasString(kArchivedCookieName
)
700 && archive
->HasString(kArchivedCookieValue
))
701 return new(std::nothrow
) BNetworkCookie(archive
);
707 // #pragma mark Overloaded operators
711 BNetworkCookie::operator==(const BNetworkCookie
& other
)
713 // Equality : name and values equals
714 return fName
== other
.fName
&& fValue
== other
.fValue
;
719 BNetworkCookie::operator!=(const BNetworkCookie
& other
)
721 return !(*this == other
);
726 BNetworkCookie::_Reset()
734 fExpiration
= BDateTime();
738 fSessionCookie
= true;
741 fRawCookieValid
= false;
742 fRawFullCookieValid
= false;
743 fExpirationStringValid
= false;
748 skip_whitespace_forward(const BString
& string
, int32 index
)
750 while (index
< string
.Length() && (string
[index
] == ' '
751 || string
[index
] == '\t'))
758 skip_whitespace_backward(const BString
& string
, int32 index
)
760 while (index
>= 0 && (string
[index
] == ' ' || string
[index
] == '\t'))
767 BNetworkCookie::_ExtractNameValuePair(const BString
& cookieString
,
768 BString
& name
, BString
& value
, int32 index
)
770 // Find our name-value-pair and the delimiter.
771 int32 firstEquals
= cookieString
.FindFirst('=', index
);
772 int32 nameValueEnd
= cookieString
.FindFirst(';', index
);
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();
779 // If the name-value-pair lacks an equals, the parse should fail.
780 if (firstEquals
== -1 || firstEquals
> nameValueEnd
)
783 int32 first
= skip_whitespace_forward(cookieString
, index
);
784 int32 last
= skip_whitespace_backward(cookieString
, firstEquals
- 1);
786 // If we lack a name, fail to parse.
790 cookieString
.CopyInto(name
, first
, last
- first
+ 1);
792 first
= skip_whitespace_forward(cookieString
, firstEquals
+ 1);
793 last
= skip_whitespace_backward(cookieString
, nameValueEnd
- 1);
795 cookieString
.CopyInto(value
, first
, last
- first
+ 1);
804 BNetworkCookie::_ExtractAttributeValuePair(const BString
& cookieString
,
805 BString
& attribute
, BString
& value
, int32 index
)
807 // Find the end of our cookie-av.
808 int32 cookieAVEnd
= cookieString
.FindFirst(';', index
);
810 // If the unparsed-attributes lacks a semicolon, then the cookie-av is the
812 if (cookieAVEnd
== -1)
813 cookieAVEnd
= cookieString
.Length();
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
;
821 int32 first
= skip_whitespace_forward(cookieString
, index
);
822 int32 last
= skip_whitespace_backward(cookieString
, attributeNameEnd
- 1);
825 cookieString
.CopyInto(attribute
, first
, last
- first
+ 1);
829 if (attributeNameEnd
== cookieAVEnd
) {
834 first
= skip_whitespace_forward(cookieString
, attributeNameEnd
+ 1);
835 last
= skip_whitespace_backward(cookieString
, cookieAVEnd
- 1);
837 cookieString
.CopyInto(value
, first
, last
- first
+ 1);
841 // values may (or may not) have quotes around them.
842 if (value
[0] == '"' && value
[value
.Length() - 1] == '"') {
844 value
.Remove(value
.Length() - 1, 1);
852 BNetworkCookie::_DefaultPathForUrl(const BUrl
& url
)
854 const BString
& path
= url
.Path();
855 if (path
.IsEmpty() || path
.ByteAt(0) != '/')
858 int32 index
= path
.FindLast('/');
862 BString newPath
= path
;
863 newPath
.Truncate(index
);