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
23using namespace BPrivate::Network;
24
25
26static const char* kArchivedCookieName = "be:cookie.name";
27static const char* kArchivedCookieValue = "be:cookie.value";
28static const char* kArchivedCookieDomain = "be:cookie.domain";
29static const char* kArchivedCookiePath = "be:cookie.path";
30static const char* kArchivedCookieExpirationDate = "be:cookie.expirationdate";
31static const char* kArchivedCookieSecure = "be:cookie.secure";
32static const char* kArchivedCookieHttpOnly = "be:cookie.httponly";
33static const char* kArchivedCookieHostOnly = "be:cookie.hostonly";
34
35
36BNetworkCookie::BNetworkCookie(const char* name, const char* value,
37	const BUrl& url)
38{
39	_Reset();
40	fName = name;
41	fValue = value;
42
43	SetDomain(url.Host());
44
45	if (url.Protocol() == "file" && url.Host().Length() == 0) {
46		SetDomain("localhost");
47			// make sure cookies set from a file:// URL are stored somewhere.
48	}
49
50	SetPath(_DefaultPathForUrl(url));
51}
52
53
54BNetworkCookie::BNetworkCookie(const BString& cookieString, const BUrl& url)
55{
56	_Reset();
57	fInitStatus = ParseCookieString(cookieString, url);
58}
59
60
61BNetworkCookie::BNetworkCookie(BMessage* archive)
62{
63	_Reset();
64
65	archive->FindString(kArchivedCookieName, &fName);
66	archive->FindString(kArchivedCookieValue, &fValue);
67
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);
73
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);
85	}
86}
87
88
89BNetworkCookie::BNetworkCookie()
90{
91	_Reset();
92}
93
94
95BNetworkCookie::~BNetworkCookie()
96{
97}
98
99
100// #pragma mark String to cookie fields
101
102
103status_t
104BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
105{
106	_Reset();
107
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).
117	}
118
119	BString name;
120	BString value;
121	int32 index = 0;
122
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;
128	}
129
130	SetName(name);
131	SetValue(value);
132
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;
136
137	// Parse the remaining cookie attributes.
138	while (index < string.Length()) {
139		ASSERT(string[index] == ';');
140		index++;
141
142		index = _ExtractAttributeValuePair(string, name, value, index);
143
144		if (name.ICompare("secure") == 0)
145			SetSecure(true);
146		else if (name.ICompare("httponly") == 0)
147			SetHttpOnly(true);
148
149		// The following attributes require a value.
150
151		if (name.ICompare("max-age") == 0) {
152			if (value.IsEmpty()) {
153				result = B_BAD_VALUE;
154				continue;
155			}
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;
170			}
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;
177			}
178
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;
187			}
188			status_t pathResult = SetPath(value);
189			if (result == B_OK)
190				result = pathResult;
191		}
192	}
193
194	if (!_CanBeSetFromUrl(url))
195		result = B_NOT_ALLOWED;
196
197	if (result != B_OK)
198		_Reset();
199
200	return result;
201}
202
203
204// #pragma mark Cookie fields modification
205
206
207BNetworkCookie&
208BNetworkCookie::SetName(const BString& name)
209{
210	fName = name;
211	fRawFullCookieValid = false;
212	fRawCookieValid = false;
213	return *this;
214}
215
216
217BNetworkCookie&
218BNetworkCookie::SetValue(const BString& value)
219{
220	fValue = value;
221	fRawFullCookieValid = false;
222	fRawCookieValid = false;
223	return *this;
224}
225
226
227status_t
228BNetworkCookie::SetPath(const BString& to)
229{
230	fPath.Truncate(0);
231	fRawFullCookieValid = false;
232
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;
236
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;
242
243	fPath = to;
244	return B_OK;
245}
246
247
248status_t
249BNetworkCookie::SetDomain(const BString& domain)
250{
251	// TODO: canonicalize the domain
252	BString newDomain = domain;
253
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);
262
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;
266
267	fDomain = newDomain.ToLower();
268
269	fHostOnly = false;
270
271	fRawFullCookieValid = false;
272	return B_OK;
273}
274
275
276BNetworkCookie&
277BNetworkCookie::SetMaxAge(int32 maxAge)
278{
279	BDateTime expiration = BDateTime::CurrentDateTime(B_LOCAL_TIME);
280
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;
286
287	expiration.SetTime_t(date);
288
289	return SetExpirationDate(expiration);
290}
291
292
293BNetworkCookie&
294BNetworkCookie::SetExpirationDate(time_t expireDate)
295{
296	BDateTime expiration;
297	expiration.SetTime_t(expireDate);
298	return SetExpirationDate(expiration);
299}
300
301
302BNetworkCookie&
303BNetworkCookie::SetExpirationDate(BDateTime& expireDate)
304{
305	if (!expireDate.IsValid()) {
306		fExpiration.SetTime_t(0);
307		fSessionCookie = true;
308	} else {
309		fExpiration = expireDate;
310		fSessionCookie = false;
311	}
312
313	fExpirationStringValid = false;
314	fRawFullCookieValid = false;
315
316	return *this;
317}
318
319
320BNetworkCookie&
321BNetworkCookie::SetSecure(bool secure)
322{
323	fSecure = secure;
324	fRawFullCookieValid = false;
325	return *this;
326}
327
328
329BNetworkCookie&
330BNetworkCookie::SetHttpOnly(bool httpOnly)
331{
332	fHttpOnly = httpOnly;
333	fRawFullCookieValid = false;
334	return *this;
335}
336
337
338// #pragma mark Cookie fields access
339
340
341const BString&
342BNetworkCookie::Name() const
343{
344	return fName;
345}
346
347
348const BString&
349BNetworkCookie::Value() const
350{
351	return fValue;
352}
353
354
355const BString&
356BNetworkCookie::Domain() const
357{
358	return fDomain;
359}
360
361
362const BString&
363BNetworkCookie::Path() const
364{
365	return fPath;
366}
367
368
369time_t
370BNetworkCookie::ExpirationDate() const
371{
372	return fExpiration.Time_t();
373}
374
375
376const BString&
377BNetworkCookie::ExpirationString() const
378{
379	BHttpTime date(fExpiration);
380
381	if (!fExpirationStringValid) {
382		fExpirationString = date.ToString(B_HTTP_TIME_FORMAT_COOKIE);
383		fExpirationStringValid = true;
384	}
385
386	return fExpirationString;
387}
388
389
390bool
391BNetworkCookie::Secure() const
392{
393	return fSecure;
394}
395
396
397bool
398BNetworkCookie::HttpOnly() const
399{
400	return fHttpOnly;
401}
402
403
404const BString&
405BNetworkCookie::RawCookie(bool full) const
406{
407	if (!fRawCookieValid) {
408		fRawCookie.Truncate(0);
409		fRawCookieValid = true;
410
411		fRawCookie << fName << "=" << fValue;
412	}
413
414	if (!full)
415		return fRawCookie;
416
417	if (!fRawFullCookieValid) {
418		fRawFullCookie = fRawCookie;
419		fRawFullCookieValid = true;
420
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";
431
432	}
433
434	return fRawFullCookie;
435}
436
437
438// #pragma mark Cookie test
439
440
441bool
442BNetworkCookie::IsHostOnly() const
443{
444	return fHostOnly;
445}
446
447
448bool
449BNetworkCookie::IsSessionCookie() const
450{
451	return fSessionCookie;
452}
453
454
455bool
456BNetworkCookie::IsValid() const
457{
458	return fInitStatus == B_OK && HasName() && HasDomain();
459}
460
461
462bool
463BNetworkCookie::IsValidForUrl(const BUrl& url) const
464{
465	if (Secure() && url.Protocol() != "https")
466		return false;
467
468	if (url.Protocol() == "file")
469		return Domain() == "localhost" && IsValidForPath(url.Path());
470
471	return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
472}
473
474
475bool
476BNetworkCookie::IsValidForDomain(const BString& domain) const
477{
478	// TODO: canonicalize both domains
479	const BString& cookieDomain = Domain();
480
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;
486
487	// If the cookie is host-only the domains must match exactly.
488	if (IsHostOnly())
489		return domain == cookieDomain;
490
491	// FIXME do not do substring matching on IP addresses. The RFCs disallow it.
492
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] == '.'));
498}
499
500
501bool
502BNetworkCookie::IsValidForPath(const BString& path) const
503{
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);
509
510	if (normalizedPath.Length() < cookiePath.Length())
511		return false;
512
513	// The cookie path must be a prefix of the path string
514	return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
515}
516
517
518bool
519BNetworkCookie::_CanBeSetFromUrl(const BUrl& url) const
520{
521	if (url.Protocol() == "file")
522		return Domain() == "localhost" && _CanBeSetFromPath(url.Path());
523
524	return _CanBeSetFromDomain(url.Host()) && _CanBeSetFromPath(url.Path());
525}
526
527
528bool
529BNetworkCookie::_CanBeSetFromDomain(const BString& domain) const
530{
531	// TODO: canonicalize both domains
532	const BString& cookieDomain = Domain();
533
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] == '.'));
540	}
541
542	// If the cookie is host-only the domains must match exactly.
543	if (IsHostOnly())
544		return domain == cookieDomain;
545
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
550
551	// FIXME do not do substring matching on IP addresses. The RFCs disallow it.
552
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] == '.'));
558}
559
560
561bool
562BNetworkCookie::_CanBeSetFromPath(const BString& path) const
563{
564	BString normalizedPath = path;
565	int slashPos = normalizedPath.FindLast('/');
566	normalizedPath.Truncate(slashPos);
567
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;
573}
574
575
576// #pragma mark Cookie fields existence tests
577
578
579bool
580BNetworkCookie::HasName() const
581{
582	return fName.Length() > 0;
583}
584
585
586bool
587BNetworkCookie::HasValue() const
588{
589	return fValue.Length() > 0;
590}
591
592
593bool
594BNetworkCookie::HasDomain() const
595{
596	return fDomain.Length() > 0;
597}
598
599
600bool
601BNetworkCookie::HasPath() const
602{
603	return fPath.Length() > 0;
604}
605
606
607bool
608BNetworkCookie::HasExpirationDate() const
609{
610	return !IsSessionCookie();
611}
612
613
614// #pragma mark Cookie delete test
615
616
617bool
618BNetworkCookie::ShouldDeleteAtExit() const
619{
620	return IsSessionCookie() || ShouldDeleteNow();
621}
622
623
624bool
625BNetworkCookie::ShouldDeleteNow() const
626{
627	if (HasExpirationDate())
628		return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
629
630	return false;
631}
632
633
634// #pragma mark BArchivable members
635
636
637status_t
638BNetworkCookie::Archive(BMessage* into, bool deep) const
639{
640	status_t error = BArchivable::Archive(into, deep);
641
642	if (error != B_OK)
643		return error;
644
645	error = into->AddString(kArchivedCookieName, fName);
646	if (error != B_OK)
647		return error;
648
649	error = into->AddString(kArchivedCookieValue, fValue);
650	if (error != B_OK)
651		return error;
652
653
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;
659	}
660
661	if (HasExpirationDate()) {
662		error = into->AddString(kArchivedCookieExpirationDate,
663			BHttpTime(fExpiration).ToString());
664		if (error != B_OK)
665			return error;
666	}
667
668	if (HasPath()) {
669		error = into->AddString(kArchivedCookiePath, fPath);
670		if (error != B_OK)
671			return error;
672	}
673
674	if (Secure()) {
675		error = into->AddBool(kArchivedCookieSecure, fSecure);
676		if (error != B_OK)
677			return error;
678	}
679
680	if (HttpOnly()) {
681		error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly);
682		if (error != B_OK)
683			return error;
684	}
685
686	if (IsHostOnly()) {
687		error = into->AddBool(kArchivedCookieHostOnly, true);
688		if (error != B_OK)
689			return error;
690	}
691
692	return B_OK;
693}
694
695
696/*static*/ BArchivable*
697BNetworkCookie::Instantiate(BMessage* archive)
698{
699	if (archive->HasString(kArchivedCookieName)
700		&& archive->HasString(kArchivedCookieValue))
701		return new(std::nothrow) BNetworkCookie(archive);
702
703	return NULL;
704}
705
706
707// #pragma mark Overloaded operators
708
709
710bool
711BNetworkCookie::operator==(const BNetworkCookie& other)
712{
713	// Equality : name and values equals
714	return fName == other.fName && fValue == other.fValue;
715}
716
717
718bool
719BNetworkCookie::operator!=(const BNetworkCookie& other)
720{
721	return !(*this == other);
722}
723
724
725void
726BNetworkCookie::_Reset()
727{
728	fInitStatus = false;
729
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;
737
738	fSessionCookie = true;
739	fHostOnly = true;
740
741	fRawCookieValid = false;
742	fRawFullCookieValid = false;
743	fExpirationStringValid = false;
744}
745
746
747int32
748skip_whitespace_forward(const BString& string, int32 index)
749{
750	while (index < string.Length() && (string[index] == ' '
751			|| string[index] == '\t'))
752		index++;
753	return index;
754}
755
756
757int32
758skip_whitespace_backward(const BString& string, int32 index)
759{
760	while (index >= 0 && (string[index] == ' ' || string[index] == '\t'))
761		index--;
762	return index;
763}
764
765
766int32
767BNetworkCookie::_ExtractNameValuePair(const BString& cookieString,
768	BString& name, BString& value, int32 index)
769{
770	// Find our name-value-pair and the delimiter.
771	int32 firstEquals = cookieString.FindFirst('=', index);
772	int32 nameValueEnd = cookieString.FindFirst(';', index);
773
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();
778
779	// If the name-value-pair lacks an equals, the parse should fail.
780	if (firstEquals == -1 || firstEquals > nameValueEnd)
781		return -1;
782
783	int32 first = skip_whitespace_forward(cookieString, index);
784	int32 last = skip_whitespace_backward(cookieString, firstEquals - 1);
785
786	// If we lack a name, fail to parse.
787	if (first > last)
788		return -1;
789
790	cookieString.CopyInto(name, first, last - first + 1);
791
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("");
798
799	return nameValueEnd;
800}
801
802
803int32
804BNetworkCookie::_ExtractAttributeValuePair(const BString& cookieString,
805	BString& attribute, BString& value, int32 index)
806{
807	// Find the end of our cookie-av.
808	int32 cookieAVEnd = cookieString.FindFirst(';', index);
809
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();
814
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;
820
821	int32 first = skip_whitespace_forward(cookieString, index);
822	int32 last = skip_whitespace_backward(cookieString, attributeNameEnd - 1);
823
824	if (first <= last)
825		cookieString.CopyInto(attribute, first, last - first + 1);
826	else
827		attribute.SetTo("");
828
829	if (attributeNameEnd == cookieAVEnd) {
830		value.SetTo("");
831		return cookieAVEnd;
832	}
833
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("");
840
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);
845	}
846
847	return cookieAVEnd;
848}
849
850
851BString
852BNetworkCookie::_DefaultPathForUrl(const BUrl& url)
853{
854	const BString& path = url.Path();
855	if (path.IsEmpty() || path.ByteAt(0) != '/')
856		return "";
857
858	int32 index = path.FindLast('/');
859	if (index == 0)
860		return "";
861
862	BString newPath = path;
863	newPath.Truncate(index);
864	return newPath;
865}
866