BPicture: Fix archive constructor.
[haiku.git] / src / kits / network / libnetapi / NetworkCookie.cpp
blobe7cc37fcb61e88b9e0e015de617907765369a355
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)
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)
57 _Reset();
58 fInitStatus = ParseCookieString(cookieString, url);
62 BNetworkCookie::BNetworkCookie(BMessage* archive)
64 _Reset();
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;
78 int32 expiration;
79 if (archive->FindString(kArchivedCookieExpirationDate, &expirationString)
80 == B_OK) {
81 BDateTime time = BHttpTime(expirationString).Parse();
82 SetExpirationDate(time);
83 } else if (archive->FindInt32(kArchivedCookieExpirationDate, &expiration)
84 == B_OK) {
85 SetExpirationDate((time_t)expiration);
90 BNetworkCookie::BNetworkCookie()
92 _Reset();
96 BNetworkCookie::~BNetworkCookie()
101 // #pragma mark String to cookie fields
104 status_t
105 BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
107 _Reset();
109 // Set default values (these can be overriden later on)
110 SetPath(_DefaultPathForUrl(url));
111 SetDomain(url.Host());
112 fHostOnly = true;
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).
121 BString name;
122 BString value;
123 int32 index = 0;
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
129 return B_BAD_DATA;
132 SetName(name);
133 SetValue(value);
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] == ';');
142 index++;
144 index = _ExtractAttributeValuePair(string, name, value, index);
146 if (name.ICompare("secure") == 0)
147 SetSecure(true);
148 else if (name.ICompare("httponly") == 0)
149 SetHttpOnly(true);
151 // The following attributes require a value.
153 if (name.ICompare("max-age") == 0) {
154 if (value.IsEmpty()) {
155 result = B_BAD_VALUE;
156 continue;
158 // Validate the max-age value.
159 char* end = NULL;
160 errno = 0;
161 long maxAge = strtol(value.String(), &end, 10);
162 if (*end == '\0')
163 SetMaxAge((int)maxAge);
164 else if (errno == ERANGE && maxAge == LONG_MAX)
165 SetMaxAge(INT_MAX);
166 else
167 SetMaxAge(-1); // cookie will expire immediately
168 } else if (name.ICompare("expires") == 0) {
169 if (value.IsEmpty()) {
170 // Will be a session cookie.
171 continue;
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;
178 continue;
181 status_t domainResult = SetDomain(value);
182 // Do not reset the result to B_OK if something else already failed
183 if (result == B_OK)
184 result = domainResult;
185 } else if (name.ICompare("path") == 0) {
186 if (value.IsEmpty()) {
187 result = B_BAD_VALUE;
188 continue;
190 status_t pathResult = SetPath(value);
191 if (result == B_OK)
192 result = pathResult;
196 if (!_CanBeSetFromUrl(url))
197 result = B_NOT_ALLOWED;
199 if (result != B_OK)
200 _Reset();
202 return result;
206 // #pragma mark Cookie fields modification
209 BNetworkCookie&
210 BNetworkCookie::SetName(const BString& name)
212 fName = name;
213 fRawFullCookieValid = false;
214 fRawCookieValid = false;
215 return *this;
219 BNetworkCookie&
220 BNetworkCookie::SetValue(const BString& value)
222 fValue = value;
223 fRawFullCookieValid = false;
224 fRawCookieValid = false;
225 return *this;
229 status_t
230 BNetworkCookie::SetPath(const BString& to)
232 fPath.Truncate(0);
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)
237 return B_BAD_DATA;
239 // Check that there aren't any "." or ".." segments in the path.
240 if (to.EndsWith("/.") || to.EndsWith("/.."))
241 return B_BAD_DATA;
242 if (to.FindFirst("/../") >= 0 || to.FindFirst("/./") >= 0)
243 return B_BAD_DATA;
245 fPath = to;
246 return B_OK;
250 status_t
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)
267 return B_BAD_DATA;
269 fDomain = newDomain.ToLower();
271 fHostOnly = false;
273 fRawFullCookieValid = false;
274 return B_OK;
278 BNetworkCookie&
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);
288 BNetworkCookie&
289 BNetworkCookie::SetExpirationDate(time_t expireDate)
291 BDateTime expiration;
292 expiration.SetTime_t(expireDate);
293 return SetExpirationDate(expiration);
297 BNetworkCookie&
298 BNetworkCookie::SetExpirationDate(BDateTime& expireDate)
300 if (!expireDate.IsValid()) {
301 fExpiration.SetTime_t(0);
302 fSessionCookie = true;
303 } else {
304 fExpiration = expireDate;
305 fSessionCookie = false;
308 fExpirationStringValid = false;
309 fRawFullCookieValid = false;
311 return *this;
315 BNetworkCookie&
316 BNetworkCookie::SetSecure(bool secure)
318 fSecure = secure;
319 fRawFullCookieValid = false;
320 return *this;
324 BNetworkCookie&
325 BNetworkCookie::SetHttpOnly(bool httpOnly)
327 fHttpOnly = httpOnly;
328 fRawFullCookieValid = false;
329 return *this;
333 // #pragma mark Cookie fields access
336 const BString&
337 BNetworkCookie::Name() const
339 return fName;
343 const BString&
344 BNetworkCookie::Value() const
346 return fValue;
350 const BString&
351 BNetworkCookie::Domain() const
353 return fDomain;
357 const BString&
358 BNetworkCookie::Path() const
360 return fPath;
364 time_t
365 BNetworkCookie::ExpirationDate() const
367 return fExpiration.Time_t();
371 const BString&
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;
385 bool
386 BNetworkCookie::Secure() const
388 return fSecure;
392 bool
393 BNetworkCookie::HttpOnly() const
395 return fHttpOnly;
399 const BString&
400 BNetworkCookie::RawCookie(bool full) const
402 if (!fRawCookieValid) {
403 fRawCookie.Truncate(0);
404 fRawCookieValid = true;
406 fRawCookie << fName << "=" << fValue;
409 if (!full)
410 return fRawCookie;
412 if (!fRawFullCookieValid) {
413 fRawFullCookie = fRawCookie;
414 fRawFullCookieValid = true;
416 if (HasDomain())
417 fRawFullCookie << "; Domain=" << fDomain;
418 if (HasExpirationDate())
419 fRawFullCookie << "; Expires=" << ExpirationString();
420 if (HasPath())
421 fRawFullCookie << "; Path=" << fPath;
422 if (Secure())
423 fRawFullCookie << "; Secure";
424 if (HttpOnly())
425 fRawFullCookie << "; HttpOnly";
429 return fRawFullCookie;
433 // #pragma mark Cookie test
436 bool
437 BNetworkCookie::IsHostOnly() const
439 return fHostOnly;
443 bool
444 BNetworkCookie::IsSessionCookie() const
446 return fSessionCookie;
450 bool
451 BNetworkCookie::IsValid() const
453 return fInitStatus == B_OK && HasName() && HasDomain();
457 bool
458 BNetworkCookie::IsValidForUrl(const BUrl& url) const
460 if (Secure() && url.Protocol() != "https")
461 return false;
463 if (url.Protocol() == "file")
464 return Domain() == "localhost" && IsValidForPath(url.Path());
466 return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
470 bool
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
478 // be valid.
479 if (difference < 0)
480 return false;
482 // If the cookie is host-only the domains must match exactly.
483 if (IsHostOnly())
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] == '.'));
496 bool
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())
506 return false;
508 // The cookie path must be a prefix of the path string
509 return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
513 bool
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());
523 bool
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.
538 if (IsHostOnly())
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] == '.'));
556 bool
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)
564 return true;
565 else if (normalizedPath.Compare(Path(), Path().Length()) == 0)
566 return true;
567 return false;
571 // #pragma mark Cookie fields existence tests
574 bool
575 BNetworkCookie::HasName() const
577 return fName.Length() > 0;
581 bool
582 BNetworkCookie::HasValue() const
584 return fValue.Length() > 0;
588 bool
589 BNetworkCookie::HasDomain() const
591 return fDomain.Length() > 0;
595 bool
596 BNetworkCookie::HasPath() const
598 return fPath.Length() > 0;
602 bool
603 BNetworkCookie::HasExpirationDate() const
605 return !IsSessionCookie();
609 // #pragma mark Cookie delete test
612 bool
613 BNetworkCookie::ShouldDeleteAtExit() const
615 return IsSessionCookie() || ShouldDeleteNow();
619 bool
620 BNetworkCookie::ShouldDeleteNow() const
622 if (HasExpirationDate())
623 return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
625 return false;
629 // #pragma mark BArchivable members
632 status_t
633 BNetworkCookie::Archive(BMessage* into, bool deep) const
635 status_t error = BArchivable::Archive(into, deep);
637 if (error != B_OK)
638 return error;
640 error = into->AddString(kArchivedCookieName, fName);
641 if (error != B_OK)
642 return error;
644 error = into->AddString(kArchivedCookieValue, fValue);
645 if (error != B_OK)
646 return error;
649 // We add optional fields only if they're defined
650 if (HasDomain()) {
651 error = into->AddString(kArchivedCookieDomain, fDomain);
652 if (error != B_OK)
653 return error;
656 if (HasExpirationDate()) {
657 error = into->AddString(kArchivedCookieExpirationDate,
658 BHttpTime(fExpiration).ToString());
659 if (error != B_OK)
660 return error;
663 if (HasPath()) {
664 error = into->AddString(kArchivedCookiePath, fPath);
665 if (error != B_OK)
666 return error;
669 if (Secure()) {
670 error = into->AddBool(kArchivedCookieSecure, fSecure);
671 if (error != B_OK)
672 return error;
675 if (HttpOnly()) {
676 error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly);
677 if (error != B_OK)
678 return error;
681 if (IsHostOnly()) {
682 error = into->AddBool(kArchivedCookieHostOnly, true);
683 if (error != B_OK)
684 return error;
687 return B_OK;
691 /*static*/ BArchivable*
692 BNetworkCookie::Instantiate(BMessage* archive)
694 if (archive->HasString(kArchivedCookieName)
695 && archive->HasString(kArchivedCookieValue))
696 return new(std::nothrow) BNetworkCookie(archive);
698 return NULL;
702 // #pragma mark Overloaded operators
705 bool
706 BNetworkCookie::operator==(const BNetworkCookie& other)
708 // Equality : name and values equals
709 return fName == other.fName && fValue == other.fValue;
713 bool
714 BNetworkCookie::operator!=(const BNetworkCookie& other)
716 return !(*this == other);
720 void
721 BNetworkCookie::_Reset()
723 fInitStatus = false;
725 fName.Truncate(0);
726 fValue.Truncate(0);
727 fDomain.Truncate(0);
728 fPath.Truncate(0);
729 fExpiration = BDateTime();
730 fSecure = false;
731 fHttpOnly = false;
733 fSessionCookie = true;
734 fHostOnly = true;
736 fRawCookieValid = false;
737 fRawFullCookieValid = false;
738 fExpirationStringValid = false;
742 int32
743 skip_whitespace_forward(const BString& string, int32 index)
745 while (index < string.Length() && (string[index] == ' '
746 || string[index] == '\t'))
747 index++;
748 return index;
752 int32
753 skip_whitespace_backward(const BString& string, int32 index)
755 while (index >= 0 && (string[index] == ' ' || string[index] == '\t'))
756 index--;
757 return index;
761 int32
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)
776 return -1;
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.
782 if (first > last)
783 return -1;
785 cookieString.CopyInto(name, first, last - first + 1);
787 first = skip_whitespace_forward(cookieString, firstEquals + 1);
788 last = skip_whitespace_backward(cookieString, nameValueEnd - 1);
789 if (first <= last)
790 cookieString.CopyInto(value, first, last - first + 1);
791 else
792 value.SetTo("");
794 return nameValueEnd;
798 int32
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
806 // whole string.
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);
819 if (first <= last)
820 cookieString.CopyInto(attribute, first, last - first + 1);
821 else
822 attribute.SetTo("");
824 if (attributeNameEnd == cookieAVEnd) {
825 value.SetTo("");
826 return cookieAVEnd;
829 first = skip_whitespace_forward(cookieString, attributeNameEnd + 1);
830 last = skip_whitespace_backward(cookieString, cookieAVEnd - 1);
831 if (first <= last)
832 cookieString.CopyInto(value, first, last - first + 1);
833 else
834 value.SetTo("");
836 // values may (or may not) have quotes around them.
837 if (value[0] == '"' && value[value.Length() - 1] == '"') {
838 value.Remove(0, 1);
839 value.Remove(value.Length() - 1, 1);
842 return cookieAVEnd;
846 BString
847 BNetworkCookie::_DefaultPathForUrl(const BUrl& url)
849 const BString& path = url.Path();
850 if (path.IsEmpty() || path.ByteAt(0) != '/')
851 return "";
853 int32 index = path.FindLast('/');
854 if (index == 0)
855 return "";
857 BString newPath = path;
858 newPath.Truncate(index);
859 return newPath;