vfs: check userland buffers before reading them.
[haiku.git] / src / kits / network / libnetapi / NetworkCookie.cpp
blob41e341d184f2c83b75d421d0321bec6d05ee1085
1 /*
2 * Copyright 2010-2014 Haiku Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
5 * Authors:
6 * Adrien Destugues, pulkomandy@pulkomandy.tk
7 * Christophe Huriaux, c.huriaux@gmail.com
8 * Hamish Morrison, hamishm53@gmail.com
9 */
12 #include <new>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <time.h>
18 #include <Debug.h>
19 #include <HttpTime.h>
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,
37 const BUrl& url)
39 _Reset();
40 fName = name;
41 fValue = 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)
56 _Reset();
57 fInitStatus = ParseCookieString(cookieString, url);
61 BNetworkCookie::BNetworkCookie(BMessage* archive)
63 _Reset();
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;
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);
89 BNetworkCookie::BNetworkCookie()
91 _Reset();
95 BNetworkCookie::~BNetworkCookie()
100 // #pragma mark String to cookie fields
103 status_t
104 BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
106 _Reset();
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).
119 BString name;
120 BString value;
121 int32 index = 0;
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;
130 SetName(name);
131 SetValue(value);
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] == ';');
140 index++;
142 index = _ExtractAttributeValuePair(string, name, value, index);
144 if (name.ICompare("secure") == 0)
145 SetSecure(true);
146 else if (name.ICompare("httponly") == 0)
147 SetHttpOnly(true);
149 // The following attributes require a value.
151 if (name.ICompare("max-age") == 0) {
152 if (value.IsEmpty()) {
153 result = B_BAD_VALUE;
154 continue;
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;
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;
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;
188 status_t pathResult = SetPath(value);
189 if (result == B_OK)
190 result = pathResult;
194 if (!_CanBeSetFromUrl(url))
195 result = B_NOT_ALLOWED;
197 if (result != B_OK)
198 _Reset();
200 return result;
204 // #pragma mark Cookie fields modification
207 BNetworkCookie&
208 BNetworkCookie::SetName(const BString& name)
210 fName = name;
211 fRawFullCookieValid = false;
212 fRawCookieValid = false;
213 return *this;
217 BNetworkCookie&
218 BNetworkCookie::SetValue(const BString& value)
220 fValue = value;
221 fRawFullCookieValid = false;
222 fRawCookieValid = false;
223 return *this;
227 status_t
228 BNetworkCookie::SetPath(const BString& to)
230 fPath.Truncate(0);
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)
235 return B_BAD_DATA;
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;
243 fPath = to;
244 return B_OK;
248 status_t
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)
265 return B_BAD_DATA;
267 fDomain = newDomain.ToLower();
269 fHostOnly = false;
271 fRawFullCookieValid = false;
272 return B_OK;
276 BNetworkCookie&
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;
284 if (date > INT_MAX)
285 date = INT_MAX;
287 expiration.SetTime_t(date);
289 return SetExpirationDate(expiration);
293 BNetworkCookie&
294 BNetworkCookie::SetExpirationDate(time_t expireDate)
296 BDateTime expiration;
297 expiration.SetTime_t(expireDate);
298 return SetExpirationDate(expiration);
302 BNetworkCookie&
303 BNetworkCookie::SetExpirationDate(BDateTime& expireDate)
305 if (!expireDate.IsValid()) {
306 fExpiration.SetTime_t(0);
307 fSessionCookie = true;
308 } else {
309 fExpiration = expireDate;
310 fSessionCookie = false;
313 fExpirationStringValid = false;
314 fRawFullCookieValid = false;
316 return *this;
320 BNetworkCookie&
321 BNetworkCookie::SetSecure(bool secure)
323 fSecure = secure;
324 fRawFullCookieValid = false;
325 return *this;
329 BNetworkCookie&
330 BNetworkCookie::SetHttpOnly(bool httpOnly)
332 fHttpOnly = httpOnly;
333 fRawFullCookieValid = false;
334 return *this;
338 // #pragma mark Cookie fields access
341 const BString&
342 BNetworkCookie::Name() const
344 return fName;
348 const BString&
349 BNetworkCookie::Value() const
351 return fValue;
355 const BString&
356 BNetworkCookie::Domain() const
358 return fDomain;
362 const BString&
363 BNetworkCookie::Path() const
365 return fPath;
369 time_t
370 BNetworkCookie::ExpirationDate() const
372 return fExpiration.Time_t();
376 const BString&
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;
390 bool
391 BNetworkCookie::Secure() const
393 return fSecure;
397 bool
398 BNetworkCookie::HttpOnly() const
400 return fHttpOnly;
404 const BString&
405 BNetworkCookie::RawCookie(bool full) const
407 if (!fRawCookieValid) {
408 fRawCookie.Truncate(0);
409 fRawCookieValid = true;
411 fRawCookie << fName << "=" << fValue;
414 if (!full)
415 return fRawCookie;
417 if (!fRawFullCookieValid) {
418 fRawFullCookie = fRawCookie;
419 fRawFullCookieValid = true;
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";
434 return fRawFullCookie;
438 // #pragma mark Cookie test
441 bool
442 BNetworkCookie::IsHostOnly() const
444 return fHostOnly;
448 bool
449 BNetworkCookie::IsSessionCookie() const
451 return fSessionCookie;
455 bool
456 BNetworkCookie::IsValid() const
458 return fInitStatus == B_OK && HasName() && HasDomain();
462 bool
463 BNetworkCookie::IsValidForUrl(const BUrl& url) const
465 if (Secure() && url.Protocol() != "https")
466 return false;
468 if (url.Protocol() == "file")
469 return Domain() == "localhost" && IsValidForPath(url.Path());
471 return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
475 bool
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
483 // be valid.
484 if (difference < 0)
485 return false;
487 // If the cookie is host-only the domains must match exactly.
488 if (IsHostOnly())
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] == '.'));
501 bool
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())
511 return false;
513 // The cookie path must be a prefix of the path string
514 return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
518 bool
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());
528 bool
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.
543 if (IsHostOnly())
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] == '.'));
561 bool
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)
569 return true;
570 else if (normalizedPath.Compare(Path(), Path().Length()) == 0)
571 return true;
572 return false;
576 // #pragma mark Cookie fields existence tests
579 bool
580 BNetworkCookie::HasName() const
582 return fName.Length() > 0;
586 bool
587 BNetworkCookie::HasValue() const
589 return fValue.Length() > 0;
593 bool
594 BNetworkCookie::HasDomain() const
596 return fDomain.Length() > 0;
600 bool
601 BNetworkCookie::HasPath() const
603 return fPath.Length() > 0;
607 bool
608 BNetworkCookie::HasExpirationDate() const
610 return !IsSessionCookie();
614 // #pragma mark Cookie delete test
617 bool
618 BNetworkCookie::ShouldDeleteAtExit() const
620 return IsSessionCookie() || ShouldDeleteNow();
624 bool
625 BNetworkCookie::ShouldDeleteNow() const
627 if (HasExpirationDate())
628 return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
630 return false;
634 // #pragma mark BArchivable members
637 status_t
638 BNetworkCookie::Archive(BMessage* into, bool deep) const
640 status_t error = BArchivable::Archive(into, deep);
642 if (error != B_OK)
643 return error;
645 error = into->AddString(kArchivedCookieName, fName);
646 if (error != B_OK)
647 return error;
649 error = into->AddString(kArchivedCookieValue, fValue);
650 if (error != B_OK)
651 return error;
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;
661 if (HasExpirationDate()) {
662 error = into->AddString(kArchivedCookieExpirationDate,
663 BHttpTime(fExpiration).ToString());
664 if (error != B_OK)
665 return error;
668 if (HasPath()) {
669 error = into->AddString(kArchivedCookiePath, fPath);
670 if (error != B_OK)
671 return error;
674 if (Secure()) {
675 error = into->AddBool(kArchivedCookieSecure, fSecure);
676 if (error != B_OK)
677 return error;
680 if (HttpOnly()) {
681 error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly);
682 if (error != B_OK)
683 return error;
686 if (IsHostOnly()) {
687 error = into->AddBool(kArchivedCookieHostOnly, true);
688 if (error != B_OK)
689 return error;
692 return B_OK;
696 /*static*/ BArchivable*
697 BNetworkCookie::Instantiate(BMessage* archive)
699 if (archive->HasString(kArchivedCookieName)
700 && archive->HasString(kArchivedCookieValue))
701 return new(std::nothrow) BNetworkCookie(archive);
703 return NULL;
707 // #pragma mark Overloaded operators
710 bool
711 BNetworkCookie::operator==(const BNetworkCookie& other)
713 // Equality : name and values equals
714 return fName == other.fName && fValue == other.fValue;
718 bool
719 BNetworkCookie::operator!=(const BNetworkCookie& other)
721 return !(*this == other);
725 void
726 BNetworkCookie::_Reset()
728 fInitStatus = false;
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;
738 fSessionCookie = true;
739 fHostOnly = true;
741 fRawCookieValid = false;
742 fRawFullCookieValid = false;
743 fExpirationStringValid = false;
747 int32
748 skip_whitespace_forward(const BString& string, int32 index)
750 while (index < string.Length() && (string[index] == ' '
751 || string[index] == '\t'))
752 index++;
753 return index;
757 int32
758 skip_whitespace_backward(const BString& string, int32 index)
760 while (index >= 0 && (string[index] == ' ' || string[index] == '\t'))
761 index--;
762 return index;
766 int32
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)
781 return -1;
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.
787 if (first > last)
788 return -1;
790 cookieString.CopyInto(name, first, last - first + 1);
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("");
799 return nameValueEnd;
803 int32
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
811 // whole string.
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);
824 if (first <= last)
825 cookieString.CopyInto(attribute, first, last - first + 1);
826 else
827 attribute.SetTo("");
829 if (attributeNameEnd == cookieAVEnd) {
830 value.SetTo("");
831 return cookieAVEnd;
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("");
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);
847 return cookieAVEnd;
851 BString
852 BNetworkCookie::_DefaultPathForUrl(const BUrl& url)
854 const BString& path = url.Path();
855 if (path.IsEmpty() || path.ByteAt(0) != '/')
856 return "";
858 int32 index = path.FindLast('/');
859 if (index == 0)
860 return "";
862 BString newPath = path;
863 newPath.Truncate(index);
864 return newPath;