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)
47 SetDomain("localhost");
48 // make sure cookies set from a file:// URL are stored somewhere.
51 SetPath(_DefaultPathForUrl(url
));
55 BNetworkCookie::BNetworkCookie(const BString
& cookieString
, const BUrl
& url
)
58 fInitStatus
= ParseCookieString(cookieString
, url
);
62 BNetworkCookie::BNetworkCookie(BMessage
* archive
)
66 archive
->FindString(kArchivedCookieName
, &fName
);
67 archive
->FindString(kArchivedCookieValue
, &fValue
);
69 archive
->FindString(kArchivedCookieDomain
, &fDomain
);
70 archive
->FindString(kArchivedCookiePath
, &fPath
);
71 archive
->FindBool(kArchivedCookieSecure
, &fSecure
);
72 archive
->FindBool(kArchivedCookieHttpOnly
, &fHttpOnly
);
73 archive
->FindBool(kArchivedCookieHostOnly
, &fHostOnly
);
75 // We store the expiration date as a string, which should not overflow.
76 // But we still parse the old archive format, where an int32 was used.
77 BString expirationString
;
79 if (archive
->FindString(kArchivedCookieExpirationDate
, &expirationString
)
81 BDateTime time
= BHttpTime(expirationString
).Parse();
82 SetExpirationDate(time
);
83 } else if (archive
->FindInt32(kArchivedCookieExpirationDate
, &expiration
)
85 SetExpirationDate((time_t)expiration
);
90 BNetworkCookie::BNetworkCookie()
96 BNetworkCookie::~BNetworkCookie()
101 // #pragma mark String to cookie fields
105 BNetworkCookie::ParseCookieString(const BString
& string
, const BUrl
& url
)
109 // Set default values (these can be overriden later on)
110 SetPath(_DefaultPathForUrl(url
));
111 SetDomain(url
.Host());
113 if (url
.Protocol() == "file" && url
.Host().Length() == 0)
115 fDomain
= "localhost";
116 // make sure cookies set from a file:// URL are stored somewhere.
117 // not going through SetDomain as it requires at least one '.'
118 // in the domain (to avoid setting cookies on TLDs).
125 // Parse the name and value of the cookie
126 index
= _ExtractNameValuePair(string
, name
, value
, index
);
127 if (index
== -1 || value
.Length() > 4096) {
128 // The set-cookie-string is not valid
135 // Note on error handling: even if there are parse errors, we will continue
136 // and try to parse as much from the cookie as we can.
137 status_t result
= B_OK
;
139 // Parse the remaining cookie attributes.
140 while (index
< string
.Length()) {
141 ASSERT(string
[index
] == ';');
144 index
= _ExtractAttributeValuePair(string
, name
, value
, index
);
146 if (name
.ICompare("secure") == 0)
148 else if (name
.ICompare("httponly") == 0)
151 // The following attributes require a value.
153 if (name
.ICompare("max-age") == 0) {
154 if (value
.IsEmpty()) {
155 result
= B_BAD_VALUE
;
158 // Validate the max-age value.
161 long maxAge
= strtol(value
.String(), &end
, 10);
163 SetMaxAge((int)maxAge
);
164 else if (errno
== ERANGE
&& maxAge
== LONG_MAX
)
167 SetMaxAge(-1); // cookie will expire immediately
168 } else if (name
.ICompare("expires") == 0) {
169 if (value
.IsEmpty()) {
170 // Will be a session cookie.
173 BDateTime parsed
= BHttpTime(value
).Parse();
174 SetExpirationDate(parsed
);
175 } else if (name
.ICompare("domain") == 0) {
176 if (value
.IsEmpty()) {
177 result
= B_BAD_VALUE
;
181 status_t domainResult
= SetDomain(value
);
182 // Do not reset the result to B_OK if something else already failed
184 result
= domainResult
;
185 } else if (name
.ICompare("path") == 0) {
186 if (value
.IsEmpty()) {
187 result
= B_BAD_VALUE
;
190 status_t pathResult
= SetPath(value
);
196 if (!_CanBeSetFromUrl(url
))
197 result
= B_NOT_ALLOWED
;
206 // #pragma mark Cookie fields modification
210 BNetworkCookie::SetName(const BString
& name
)
213 fRawFullCookieValid
= false;
214 fRawCookieValid
= false;
220 BNetworkCookie::SetValue(const BString
& value
)
223 fRawFullCookieValid
= false;
224 fRawCookieValid
= false;
230 BNetworkCookie::SetPath(const BString
& to
)
233 fRawFullCookieValid
= false;
235 // Limit the path to 4096 characters to not let the cookie jar grow huge.
236 if (to
[0] != '/' || to
.Length() > 4096)
239 // Check that there aren't any "." or ".." segments in the path.
240 if (to
.EndsWith("/.") || to
.EndsWith("/.."))
242 if (to
.FindFirst("/../") >= 0 || to
.FindFirst("/./") >= 0)
251 BNetworkCookie::SetDomain(const BString
& domain
)
253 // TODO: canonicalize the domain
254 BString newDomain
= domain
;
256 // RFC 2109 (legacy) support: domain string may start with a dot,
257 // meant to indicate the cookie should also be used for subdomains.
258 // RFC 6265 makes all cookies work for subdomains, unless the domain is
259 // not specified at all (in this case it has to exactly match the Url of
260 // the page that set the cookie). In any case, we don't need to handle
261 // dot-cookies specifically anymore, so just remove the extra dot.
262 if (newDomain
[0] == '.')
263 newDomain
.Remove(0, 1);
265 // check we're not trying to set a cookie on a TLD or empty domain
266 if (newDomain
.FindLast('.') <= 0)
269 fDomain
= newDomain
.ToLower();
273 fRawFullCookieValid
= false;
279 BNetworkCookie::SetMaxAge(int32 maxAge
)
281 BDateTime expiration
= BDateTime::CurrentDateTime(B_LOCAL_TIME
);
282 expiration
.SetTime_t(expiration
.Time_t() + maxAge
);
284 return SetExpirationDate(expiration
);
289 BNetworkCookie::SetExpirationDate(time_t expireDate
)
291 BDateTime expiration
;
292 expiration
.SetTime_t(expireDate
);
293 return SetExpirationDate(expiration
);
298 BNetworkCookie::SetExpirationDate(BDateTime
& expireDate
)
300 if (!expireDate
.IsValid()) {
301 fExpiration
.SetTime_t(0);
302 fSessionCookie
= true;
304 fExpiration
= expireDate
;
305 fSessionCookie
= false;
308 fExpirationStringValid
= false;
309 fRawFullCookieValid
= false;
316 BNetworkCookie::SetSecure(bool secure
)
319 fRawFullCookieValid
= false;
325 BNetworkCookie::SetHttpOnly(bool httpOnly
)
327 fHttpOnly
= httpOnly
;
328 fRawFullCookieValid
= false;
333 // #pragma mark Cookie fields access
337 BNetworkCookie::Name() const
344 BNetworkCookie::Value() const
351 BNetworkCookie::Domain() const
358 BNetworkCookie::Path() const
365 BNetworkCookie::ExpirationDate() const
367 return fExpiration
.Time_t();
372 BNetworkCookie::ExpirationString() const
374 BHttpTime
date(fExpiration
);
376 if (!fExpirationStringValid
) {
377 fExpirationString
= date
.ToString(BPrivate::B_HTTP_TIME_FORMAT_COOKIE
);
378 fExpirationStringValid
= true;
381 return fExpirationString
;
386 BNetworkCookie::Secure() const
393 BNetworkCookie::HttpOnly() const
400 BNetworkCookie::RawCookie(bool full
) const
402 if (!fRawCookieValid
) {
403 fRawCookie
.Truncate(0);
404 fRawCookieValid
= true;
406 fRawCookie
<< fName
<< "=" << fValue
;
412 if (!fRawFullCookieValid
) {
413 fRawFullCookie
= fRawCookie
;
414 fRawFullCookieValid
= true;
417 fRawFullCookie
<< "; Domain=" << fDomain
;
418 if (HasExpirationDate())
419 fRawFullCookie
<< "; Expires=" << ExpirationString();
421 fRawFullCookie
<< "; Path=" << fPath
;
423 fRawFullCookie
<< "; Secure";
425 fRawFullCookie
<< "; HttpOnly";
429 return fRawFullCookie
;
433 // #pragma mark Cookie test
437 BNetworkCookie::IsHostOnly() const
444 BNetworkCookie::IsSessionCookie() const
446 return fSessionCookie
;
451 BNetworkCookie::IsValid() const
453 return fInitStatus
== B_OK
&& HasName() && HasDomain();
458 BNetworkCookie::IsValidForUrl(const BUrl
& url
) const
460 if (Secure() && url
.Protocol() != "https")
463 if (url
.Protocol() == "file")
464 return Domain() == "localhost" && IsValidForPath(url
.Path());
466 return IsValidForDomain(url
.Host()) && IsValidForPath(url
.Path());
471 BNetworkCookie::IsValidForDomain(const BString
& domain
) const
473 // TODO: canonicalize both domains
474 const BString
& cookieDomain
= Domain();
476 int32 difference
= domain
.Length() - cookieDomain
.Length();
477 // If the cookie domain is longer than the domain string it cannot
482 // If the cookie is host-only the domains must match exactly.
484 return domain
== cookieDomain
;
486 // FIXME do not do substring matching on IP addresses. The RFCs disallow it.
488 // Otherwise, the domains must match exactly, or the domain must have a dot
489 // character just before the common suffix.
490 const char* suffix
= domain
.String() + difference
;
491 return (strcmp(suffix
, cookieDomain
.String()) == 0 && (difference
== 0
492 || domain
[difference
- 1] == '.'));
497 BNetworkCookie::IsValidForPath(const BString
& path
) const
499 const BString
& cookiePath
= Path();
500 BString normalizedPath
= path
;
501 int slashPos
= normalizedPath
.FindLast('/');
502 if (slashPos
!= normalizedPath
.Length() - 1)
503 normalizedPath
.Truncate(slashPos
+ 1);
505 if (normalizedPath
.Length() < cookiePath
.Length())
508 // The cookie path must be a prefix of the path string
509 return normalizedPath
.Compare(cookiePath
, cookiePath
.Length()) == 0;
514 BNetworkCookie::_CanBeSetFromUrl(const BUrl
& url
) const
516 if (url
.Protocol() == "file")
517 return Domain() == "localhost" && _CanBeSetFromPath(url
.Path());
519 return _CanBeSetFromDomain(url
.Host()) && _CanBeSetFromPath(url
.Path());
524 BNetworkCookie::_CanBeSetFromDomain(const BString
& domain
) const
526 // TODO: canonicalize both domains
527 const BString
& cookieDomain
= Domain();
529 int32 difference
= domain
.Length() - cookieDomain
.Length();
530 if (difference
< 0) {
531 // Setting a cookie on a subdomain is allowed.
532 const char* suffix
= cookieDomain
.String() + difference
;
533 return (strcmp(suffix
, domain
.String()) == 0 && (difference
== 0
534 || cookieDomain
[difference
- 1] == '.'));
537 // If the cookie is host-only the domains must match exactly.
539 return domain
== cookieDomain
;
541 // FIXME prevent supercookies with a domain of ".com" or similar
542 // This is NOT as straightforward as relying on the last dot in the domain.
543 // Here's a list of TLD:
544 // https://github.com/rsimoes/Mozilla-PublicSuffix/blob/master/effective_tld_names.dat
546 // FIXME do not do substring matching on IP addresses. The RFCs disallow it.
548 // Otherwise, the domains must match exactly, or the domain must have a dot
549 // character just before the common suffix.
550 const char* suffix
= domain
.String() + difference
;
551 return (strcmp(suffix
, cookieDomain
.String()) == 0 && (difference
== 0
552 || domain
[difference
- 1] == '.'));
557 BNetworkCookie::_CanBeSetFromPath(const BString
& path
) const
559 BString normalizedPath
= path
;
560 int slashPos
= normalizedPath
.FindLast('/');
561 normalizedPath
.Truncate(slashPos
);
563 if (Path().Compare(normalizedPath
, normalizedPath
.Length()) == 0)
565 else if (normalizedPath
.Compare(Path(), Path().Length()) == 0)
571 // #pragma mark Cookie fields existence tests
575 BNetworkCookie::HasName() const
577 return fName
.Length() > 0;
582 BNetworkCookie::HasValue() const
584 return fValue
.Length() > 0;
589 BNetworkCookie::HasDomain() const
591 return fDomain
.Length() > 0;
596 BNetworkCookie::HasPath() const
598 return fPath
.Length() > 0;
603 BNetworkCookie::HasExpirationDate() const
605 return !IsSessionCookie();
609 // #pragma mark Cookie delete test
613 BNetworkCookie::ShouldDeleteAtExit() const
615 return IsSessionCookie() || ShouldDeleteNow();
620 BNetworkCookie::ShouldDeleteNow() const
622 if (HasExpirationDate())
623 return (BDateTime::CurrentDateTime(B_GMT_TIME
) > fExpiration
);
629 // #pragma mark BArchivable members
633 BNetworkCookie::Archive(BMessage
* into
, bool deep
) const
635 status_t error
= BArchivable::Archive(into
, deep
);
640 error
= into
->AddString(kArchivedCookieName
, fName
);
644 error
= into
->AddString(kArchivedCookieValue
, fValue
);
649 // We add optional fields only if they're defined
651 error
= into
->AddString(kArchivedCookieDomain
, fDomain
);
656 if (HasExpirationDate()) {
657 error
= into
->AddString(kArchivedCookieExpirationDate
,
658 BHttpTime(fExpiration
).ToString());
664 error
= into
->AddString(kArchivedCookiePath
, fPath
);
670 error
= into
->AddBool(kArchivedCookieSecure
, fSecure
);
676 error
= into
->AddBool(kArchivedCookieHttpOnly
, fHttpOnly
);
682 error
= into
->AddBool(kArchivedCookieHostOnly
, true);
691 /*static*/ BArchivable
*
692 BNetworkCookie::Instantiate(BMessage
* archive
)
694 if (archive
->HasString(kArchivedCookieName
)
695 && archive
->HasString(kArchivedCookieValue
))
696 return new(std::nothrow
) BNetworkCookie(archive
);
702 // #pragma mark Overloaded operators
706 BNetworkCookie::operator==(const BNetworkCookie
& other
)
708 // Equality : name and values equals
709 return fName
== other
.fName
&& fValue
== other
.fValue
;
714 BNetworkCookie::operator!=(const BNetworkCookie
& other
)
716 return !(*this == other
);
721 BNetworkCookie::_Reset()
729 fExpiration
= BDateTime();
733 fSessionCookie
= true;
736 fRawCookieValid
= false;
737 fRawFullCookieValid
= false;
738 fExpirationStringValid
= false;
743 skip_whitespace_forward(const BString
& string
, int32 index
)
745 while (index
< string
.Length() && (string
[index
] == ' '
746 || string
[index
] == '\t'))
753 skip_whitespace_backward(const BString
& string
, int32 index
)
755 while (index
>= 0 && (string
[index
] == ' ' || string
[index
] == '\t'))
762 BNetworkCookie::_ExtractNameValuePair(const BString
& cookieString
,
763 BString
& name
, BString
& value
, int32 index
)
765 // Find our name-value-pair and the delimiter.
766 int32 firstEquals
= cookieString
.FindFirst('=', index
);
767 int32 nameValueEnd
= cookieString
.FindFirst(';', index
);
769 // If the set-cookie-string lacks a semicolon, the name-value-pair
770 // is the whole string.
771 if (nameValueEnd
== -1)
772 nameValueEnd
= cookieString
.Length();
774 // If the name-value-pair lacks an equals, the parse should fail.
775 if (firstEquals
== -1 || firstEquals
> nameValueEnd
)
778 int32 first
= skip_whitespace_forward(cookieString
, index
);
779 int32 last
= skip_whitespace_backward(cookieString
, firstEquals
- 1);
781 // If we lack a name, fail to parse.
785 cookieString
.CopyInto(name
, first
, last
- first
+ 1);
787 first
= skip_whitespace_forward(cookieString
, firstEquals
+ 1);
788 last
= skip_whitespace_backward(cookieString
, nameValueEnd
- 1);
790 cookieString
.CopyInto(value
, first
, last
- first
+ 1);
799 BNetworkCookie::_ExtractAttributeValuePair(const BString
& cookieString
,
800 BString
& attribute
, BString
& value
, int32 index
)
802 // Find the end of our cookie-av.
803 int32 cookieAVEnd
= cookieString
.FindFirst(';', index
);
805 // If the unparsed-attributes lacks a semicolon, then the cookie-av is the
807 if (cookieAVEnd
== -1)
808 cookieAVEnd
= cookieString
.Length();
810 int32 attributeNameEnd
= cookieString
.FindFirst('=', index
);
811 // If the cookie-av has no equals, the attribute-name is the entire
812 // cookie-av and the attribute-value is empty.
813 if (attributeNameEnd
== -1 || attributeNameEnd
> cookieAVEnd
)
814 attributeNameEnd
= cookieAVEnd
;
816 int32 first
= skip_whitespace_forward(cookieString
, index
);
817 int32 last
= skip_whitespace_backward(cookieString
, attributeNameEnd
- 1);
820 cookieString
.CopyInto(attribute
, first
, last
- first
+ 1);
824 if (attributeNameEnd
== cookieAVEnd
) {
829 first
= skip_whitespace_forward(cookieString
, attributeNameEnd
+ 1);
830 last
= skip_whitespace_backward(cookieString
, cookieAVEnd
- 1);
832 cookieString
.CopyInto(value
, first
, last
- first
+ 1);
836 // values may (or may not) have quotes around them.
837 if (value
[0] == '"' && value
[value
.Length() - 1] == '"') {
839 value
.Remove(value
.Length() - 1, 1);
847 BNetworkCookie::_DefaultPathForUrl(const BUrl
& url
)
849 const BString
& path
= url
.Path();
850 if (path
.IsEmpty() || path
.ByteAt(0) != '/')
853 int32 index
= path
.FindLast('/');
857 BString newPath
= path
;
858 newPath
.Truncate(index
);