xref: /haiku/src/kits/network/libnetservices/NetworkCookie.cpp (revision 7aa55747139506809789e3048dbee94bb9437437)
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