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
24using BPrivate::BHttpTime;
25
26
27static const char* kArchivedCookieName = "be:cookie.name";
28static const char* kArchivedCookieValue = "be:cookie.value";
29static const char* kArchivedCookieDomain = "be:cookie.domain";
30static const char* kArchivedCookiePath = "be:cookie.path";
31static const char* kArchivedCookieExpirationDate = "be:cookie.expirationdate";
32static const char* kArchivedCookieSecure = "be:cookie.secure";
33static const char* kArchivedCookieHttpOnly = "be:cookie.httponly";
34static const char* kArchivedCookieHostOnly = "be:cookie.hostonly";
35
36
37BNetworkCookie::BNetworkCookie(const char* name, const char* value,
38	const BUrl& url)
39{
40	_Reset();
41	fName = name;
42	fValue = value;
43
44	SetDomain(url.Host());
45
46	if (url.Protocol() == "file" && url.Host().Length() == 0) {
47		SetDomain("localhost");
48			// make sure cookies set from a file:// URL are stored somewhere.
49	}
50
51	SetPath(_DefaultPathForUrl(url));
52}
53
54
55BNetworkCookie::BNetworkCookie(const BString& cookieString, const BUrl& url)
56{
57	_Reset();
58	fInitStatus = ParseCookieString(cookieString, url);
59}
60
61
62BNetworkCookie::BNetworkCookie(BMessage* archive)
63{
64	_Reset();
65
66	archive->FindString(kArchivedCookieName, &fName);
67	archive->FindString(kArchivedCookieValue, &fValue);
68
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);
74
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);
86	}
87}
88
89
90BNetworkCookie::BNetworkCookie()
91{
92	_Reset();
93}
94
95
96BNetworkCookie::~BNetworkCookie()
97{
98}
99
100
101// #pragma mark String to cookie fields
102
103
104status_t
105BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
106{
107	_Reset();
108
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) {
114		fDomain = "localhost";
115			// make sure cookies set from a file:// URL are stored somewhere.
116			// not going through SetDomain as it requires at least one '.'
117			// in the domain (to avoid setting cookies on TLDs).
118	}
119
120	BString name;
121	BString value;
122	int32 index = 0;
123
124	// Parse the name and value of the cookie
125	index = _ExtractNameValuePair(string, name, value, index);
126	if (index == -1 || value.Length() > 4096) {
127		// The set-cookie-string is not valid
128		return B_BAD_DATA;
129	}
130
131	SetName(name);
132	SetValue(value);
133
134	// Note on error handling: even if there are parse errors, we will continue
135	// and try to parse as much from the cookie as we can.
136	status_t result = B_OK;
137
138	// Parse the remaining cookie attributes.
139	while (index < string.Length()) {
140		ASSERT(string[index] == ';');
141		index++;
142
143		index = _ExtractAttributeValuePair(string, name, value, index);
144
145		if (name.ICompare("secure") == 0)
146			SetSecure(true);
147		else if (name.ICompare("httponly") == 0)
148			SetHttpOnly(true);
149
150		// The following attributes require a value.
151
152		if (name.ICompare("max-age") == 0) {
153			if (value.IsEmpty()) {
154				result = B_BAD_VALUE;
155				continue;
156			}
157			// Validate the max-age value.
158			char* end = NULL;
159			errno = 0;
160			long maxAge = strtol(value.String(), &end, 10);
161			if (*end == '\0')
162				SetMaxAge((int)maxAge);
163			else if (errno == ERANGE && maxAge == LONG_MAX)
164				SetMaxAge(INT_MAX);
165			else
166				SetMaxAge(-1); // cookie will expire immediately
167		} else if (name.ICompare("expires") == 0) {
168			if (value.IsEmpty()) {
169				// Will be a session cookie.
170				continue;
171			}
172			BDateTime parsed = BHttpTime(value).Parse();
173			SetExpirationDate(parsed);
174		} else if (name.ICompare("domain") == 0) {
175			if (value.IsEmpty()) {
176				result = B_BAD_VALUE;
177				continue;
178			}
179
180			status_t domainResult = SetDomain(value);
181			// Do not reset the result to B_OK if something else already failed
182			if (result == B_OK)
183				result = domainResult;
184		} else if (name.ICompare("path") == 0) {
185			if (value.IsEmpty()) {
186				result = B_BAD_VALUE;
187				continue;
188			}
189			status_t pathResult = SetPath(value);
190			if (result == B_OK)
191				result = pathResult;
192		}
193	}
194
195	if (!_CanBeSetFromUrl(url))
196		result = B_NOT_ALLOWED;
197
198	if (result != B_OK)
199		_Reset();
200
201	return result;
202}
203
204
205// #pragma mark Cookie fields modification
206
207
208BNetworkCookie&
209BNetworkCookie::SetName(const BString& name)
210{
211	fName = name;
212	fRawFullCookieValid = false;
213	fRawCookieValid = false;
214	return *this;
215}
216
217
218BNetworkCookie&
219BNetworkCookie::SetValue(const BString& value)
220{
221	fValue = value;
222	fRawFullCookieValid = false;
223	fRawCookieValid = false;
224	return *this;
225}
226
227
228status_t
229BNetworkCookie::SetPath(const BString& to)
230{
231	fPath.Truncate(0);
232	fRawFullCookieValid = false;
233
234	// Limit the path to 4096 characters to not let the cookie jar grow huge.
235	if (to[0] != '/' || to.Length() > 4096)
236		return B_BAD_DATA;
237
238	// Check that there aren't any "." or ".." segments in the path.
239	if (to.EndsWith("/.") || to.EndsWith("/.."))
240		return B_BAD_DATA;
241	if (to.FindFirst("/../") >= 0 || to.FindFirst("/./") >= 0)
242		return B_BAD_DATA;
243
244	fPath = to;
245	return B_OK;
246}
247
248
249status_t
250BNetworkCookie::SetDomain(const BString& domain)
251{
252	// TODO: canonicalize the domain
253	BString newDomain = domain;
254
255	// RFC 2109 (legacy) support: domain string may start with a dot,
256	// meant to indicate the cookie should also be used for subdomains.
257	// RFC 6265 makes all cookies work for subdomains, unless the domain is
258	// not specified at all (in this case it has to exactly match the Url of
259	// the page that set the cookie). In any case, we don't need to handle
260	// dot-cookies specifically anymore, so just remove the extra dot.
261	if (newDomain[0] == '.')
262		newDomain.Remove(0, 1);
263
264	// check we're not trying to set a cookie on a TLD or empty domain
265	if (newDomain.FindLast('.') <= 0)
266		return B_BAD_DATA;
267
268	fDomain = newDomain.ToLower();
269
270	fHostOnly = false;
271
272	fRawFullCookieValid = false;
273	return B_OK;
274}
275
276
277BNetworkCookie&
278BNetworkCookie::SetMaxAge(int32 maxAge)
279{
280	BDateTime expiration = BDateTime::CurrentDateTime(B_LOCAL_TIME);
281
282	// Compute the expiration date (watch out for overflows)
283	int64_t date = expiration.Time_t();
284	date += (int64_t)maxAge;
285	if (date > INT_MAX)
286		date = INT_MAX;
287
288	expiration.SetTime_t(date);
289
290	return SetExpirationDate(expiration);
291}
292
293
294BNetworkCookie&
295BNetworkCookie::SetExpirationDate(time_t expireDate)
296{
297	BDateTime expiration;
298	expiration.SetTime_t(expireDate);
299	return SetExpirationDate(expiration);
300}
301
302
303BNetworkCookie&
304BNetworkCookie::SetExpirationDate(BDateTime& expireDate)
305{
306	if (!expireDate.IsValid()) {
307		fExpiration.SetTime_t(0);
308		fSessionCookie = true;
309	} else {
310		fExpiration = expireDate;
311		fSessionCookie = false;
312	}
313
314	fExpirationStringValid = false;
315	fRawFullCookieValid = false;
316
317	return *this;
318}
319
320
321BNetworkCookie&
322BNetworkCookie::SetSecure(bool secure)
323{
324	fSecure = secure;
325	fRawFullCookieValid = false;
326	return *this;
327}
328
329
330BNetworkCookie&
331BNetworkCookie::SetHttpOnly(bool httpOnly)
332{
333	fHttpOnly = httpOnly;
334	fRawFullCookieValid = false;
335	return *this;
336}
337
338
339// #pragma mark Cookie fields access
340
341
342const BString&
343BNetworkCookie::Name() const
344{
345	return fName;
346}
347
348
349const BString&
350BNetworkCookie::Value() const
351{
352	return fValue;
353}
354
355
356const BString&
357BNetworkCookie::Domain() const
358{
359	return fDomain;
360}
361
362
363const BString&
364BNetworkCookie::Path() const
365{
366	return fPath;
367}
368
369
370time_t
371BNetworkCookie::ExpirationDate() const
372{
373	return fExpiration.Time_t();
374}
375
376
377const BString&
378BNetworkCookie::ExpirationString() const
379{
380	BHttpTime date(fExpiration);
381
382	if (!fExpirationStringValid) {
383		fExpirationString = date.ToString(BPrivate::B_HTTP_TIME_FORMAT_COOKIE);
384		fExpirationStringValid = true;
385	}
386
387	return fExpirationString;
388}
389
390
391bool
392BNetworkCookie::Secure() const
393{
394	return fSecure;
395}
396
397
398bool
399BNetworkCookie::HttpOnly() const
400{
401	return fHttpOnly;
402}
403
404
405const BString&
406BNetworkCookie::RawCookie(bool full) const
407{
408	if (!fRawCookieValid) {
409		fRawCookie.Truncate(0);
410		fRawCookieValid = true;
411
412		fRawCookie << fName << "=" << fValue;
413	}
414
415	if (!full)
416		return fRawCookie;
417
418	if (!fRawFullCookieValid) {
419		fRawFullCookie = fRawCookie;
420		fRawFullCookieValid = true;
421
422		if (HasDomain())
423			fRawFullCookie << "; Domain=" << fDomain;
424		if (HasExpirationDate())
425			fRawFullCookie << "; Expires=" << ExpirationString();
426		if (HasPath())
427			fRawFullCookie << "; Path=" << fPath;
428		if (Secure())
429			fRawFullCookie << "; Secure";
430		if (HttpOnly())
431			fRawFullCookie << "; HttpOnly";
432
433	}
434
435	return fRawFullCookie;
436}
437
438
439// #pragma mark Cookie test
440
441
442bool
443BNetworkCookie::IsHostOnly() const
444{
445	return fHostOnly;
446}
447
448
449bool
450BNetworkCookie::IsSessionCookie() const
451{
452	return fSessionCookie;
453}
454
455
456bool
457BNetworkCookie::IsValid() const
458{
459	return fInitStatus == B_OK && HasName() && HasDomain();
460}
461
462
463bool
464BNetworkCookie::IsValidForUrl(const BUrl& url) const
465{
466	if (Secure() && url.Protocol() != "https")
467		return false;
468
469	if (url.Protocol() == "file")
470		return Domain() == "localhost" && IsValidForPath(url.Path());
471
472	return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
473}
474
475
476bool
477BNetworkCookie::IsValidForDomain(const BString& domain) const
478{
479	// TODO: canonicalize both domains
480	const BString& cookieDomain = Domain();
481
482	int32 difference = domain.Length() - cookieDomain.Length();
483	// If the cookie domain is longer than the domain string it cannot
484	// be valid.
485	if (difference < 0)
486		return false;
487
488	// If the cookie is host-only the domains must match exactly.
489	if (IsHostOnly())
490		return domain == cookieDomain;
491
492	// FIXME do not do substring matching on IP addresses. The RFCs disallow it.
493
494	// Otherwise, the domains must match exactly, or the domain must have a dot
495	// character just before the common suffix.
496	const char* suffix = domain.String() + difference;
497	return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0
498		|| domain[difference - 1] == '.'));
499}
500
501
502bool
503BNetworkCookie::IsValidForPath(const BString& path) const
504{
505	const BString& cookiePath = Path();
506	BString normalizedPath = path;
507	int slashPos = normalizedPath.FindLast('/');
508	if (slashPos != normalizedPath.Length() - 1)
509		normalizedPath.Truncate(slashPos + 1);
510
511	if (normalizedPath.Length() < cookiePath.Length())
512		return false;
513
514	// The cookie path must be a prefix of the path string
515	return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
516}
517
518
519bool
520BNetworkCookie::_CanBeSetFromUrl(const BUrl& url) const
521{
522	if (url.Protocol() == "file")
523		return Domain() == "localhost" && _CanBeSetFromPath(url.Path());
524
525	return _CanBeSetFromDomain(url.Host()) && _CanBeSetFromPath(url.Path());
526}
527
528
529bool
530BNetworkCookie::_CanBeSetFromDomain(const BString& domain) const
531{
532	// TODO: canonicalize both domains
533	const BString& cookieDomain = Domain();
534
535	int32 difference = domain.Length() - cookieDomain.Length();
536	if (difference < 0) {
537		// Setting a cookie on a subdomain is allowed.
538		const char* suffix = cookieDomain.String() + difference;
539		return (strcmp(suffix, domain.String()) == 0 && (difference == 0
540			|| cookieDomain[difference - 1] == '.'));
541	}
542
543	// If the cookie is host-only the domains must match exactly.
544	if (IsHostOnly())
545		return domain == cookieDomain;
546
547	// FIXME prevent supercookies with a domain of ".com" or similar
548	// This is NOT as straightforward as relying on the last dot in the domain.
549	// Here's a list of TLD:
550	// https://github.com/rsimoes/Mozilla-PublicSuffix/blob/master/effective_tld_names.dat
551
552	// FIXME do not do substring matching on IP addresses. The RFCs disallow it.
553
554	// Otherwise, the domains must match exactly, or the domain must have a dot
555	// character just before the common suffix.
556	const char* suffix = domain.String() + difference;
557	return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0
558		|| domain[difference - 1] == '.'));
559}
560
561
562bool
563BNetworkCookie::_CanBeSetFromPath(const BString& path) const
564{
565	BString normalizedPath = path;
566	int slashPos = normalizedPath.FindLast('/');
567	normalizedPath.Truncate(slashPos);
568
569	if (Path().Compare(normalizedPath, normalizedPath.Length()) == 0)
570		return true;
571	else if (normalizedPath.Compare(Path(), Path().Length()) == 0)
572		return true;
573	return false;
574}
575
576
577// #pragma mark Cookie fields existence tests
578
579
580bool
581BNetworkCookie::HasName() const
582{
583	return fName.Length() > 0;
584}
585
586
587bool
588BNetworkCookie::HasValue() const
589{
590	return fValue.Length() > 0;
591}
592
593
594bool
595BNetworkCookie::HasDomain() const
596{
597	return fDomain.Length() > 0;
598}
599
600
601bool
602BNetworkCookie::HasPath() const
603{
604	return fPath.Length() > 0;
605}
606
607
608bool
609BNetworkCookie::HasExpirationDate() const
610{
611	return !IsSessionCookie();
612}
613
614
615// #pragma mark Cookie delete test
616
617
618bool
619BNetworkCookie::ShouldDeleteAtExit() const
620{
621	return IsSessionCookie() || ShouldDeleteNow();
622}
623
624
625bool
626BNetworkCookie::ShouldDeleteNow() const
627{
628	if (HasExpirationDate())
629		return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
630
631	return false;
632}
633
634
635// #pragma mark BArchivable members
636
637
638status_t
639BNetworkCookie::Archive(BMessage* into, bool deep) const
640{
641	status_t error = BArchivable::Archive(into, deep);
642
643	if (error != B_OK)
644		return error;
645
646	error = into->AddString(kArchivedCookieName, fName);
647	if (error != B_OK)
648		return error;
649
650	error = into->AddString(kArchivedCookieValue, fValue);
651	if (error != B_OK)
652		return error;
653
654
655	// We add optional fields only if they're defined
656	if (HasDomain()) {
657		error = into->AddString(kArchivedCookieDomain, fDomain);
658		if (error != B_OK)
659			return error;
660	}
661
662	if (HasExpirationDate()) {
663		error = into->AddString(kArchivedCookieExpirationDate,
664			BHttpTime(fExpiration).ToString());
665		if (error != B_OK)
666			return error;
667	}
668
669	if (HasPath()) {
670		error = into->AddString(kArchivedCookiePath, fPath);
671		if (error != B_OK)
672			return error;
673	}
674
675	if (Secure()) {
676		error = into->AddBool(kArchivedCookieSecure, fSecure);
677		if (error != B_OK)
678			return error;
679	}
680
681	if (HttpOnly()) {
682		error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly);
683		if (error != B_OK)
684			return error;
685	}
686
687	if (IsHostOnly()) {
688		error = into->AddBool(kArchivedCookieHostOnly, true);
689		if (error != B_OK)
690			return error;
691	}
692
693	return B_OK;
694}
695
696
697/*static*/ BArchivable*
698BNetworkCookie::Instantiate(BMessage* archive)
699{
700	if (archive->HasString(kArchivedCookieName)
701		&& archive->HasString(kArchivedCookieValue))
702		return new(std::nothrow) BNetworkCookie(archive);
703
704	return NULL;
705}
706
707
708// #pragma mark Overloaded operators
709
710
711bool
712BNetworkCookie::operator==(const BNetworkCookie& other)
713{
714	// Equality : name and values equals
715	return fName == other.fName && fValue == other.fValue;
716}
717
718
719bool
720BNetworkCookie::operator!=(const BNetworkCookie& other)
721{
722	return !(*this == other);
723}
724
725
726void
727BNetworkCookie::_Reset()
728{
729	fInitStatus = false;
730
731	fName.Truncate(0);
732	fValue.Truncate(0);
733	fDomain.Truncate(0);
734	fPath.Truncate(0);
735	fExpiration = BDateTime();
736	fSecure = false;
737	fHttpOnly = false;
738
739	fSessionCookie = true;
740	fHostOnly = true;
741
742	fRawCookieValid = false;
743	fRawFullCookieValid = false;
744	fExpirationStringValid = false;
745}
746
747
748int32
749skip_whitespace_forward(const BString& string, int32 index)
750{
751	while (index < string.Length() && (string[index] == ' '
752			|| string[index] == '\t'))
753		index++;
754	return index;
755}
756
757
758int32
759skip_whitespace_backward(const BString& string, int32 index)
760{
761	while (index >= 0 && (string[index] == ' ' || string[index] == '\t'))
762		index--;
763	return index;
764}
765
766
767int32
768BNetworkCookie::_ExtractNameValuePair(const BString& cookieString,
769	BString& name, BString& value, int32 index)
770{
771	// Find our name-value-pair and the delimiter.
772	int32 firstEquals = cookieString.FindFirst('=', index);
773	int32 nameValueEnd = cookieString.FindFirst(';', index);
774
775	// If the set-cookie-string lacks a semicolon, the name-value-pair
776	// is the whole string.
777	if (nameValueEnd == -1)
778		nameValueEnd = cookieString.Length();
779
780	// If the name-value-pair lacks an equals, the parse should fail.
781	if (firstEquals == -1 || firstEquals > nameValueEnd)
782		return -1;
783
784	int32 first = skip_whitespace_forward(cookieString, index);
785	int32 last = skip_whitespace_backward(cookieString, firstEquals - 1);
786
787	// If we lack a name, fail to parse.
788	if (first > last)
789		return -1;
790
791	cookieString.CopyInto(name, first, last - first + 1);
792
793	first = skip_whitespace_forward(cookieString, firstEquals + 1);
794	last = skip_whitespace_backward(cookieString, nameValueEnd - 1);
795	if (first <= last)
796		cookieString.CopyInto(value, first, last - first + 1);
797	else
798		value.SetTo("");
799
800	return nameValueEnd;
801}
802
803
804int32
805BNetworkCookie::_ExtractAttributeValuePair(const BString& cookieString,
806	BString& attribute, BString& value, int32 index)
807{
808	// Find the end of our cookie-av.
809	int32 cookieAVEnd = cookieString.FindFirst(';', index);
810
811	// If the unparsed-attributes lacks a semicolon, then the cookie-av is the
812	// whole string.
813	if (cookieAVEnd == -1)
814		cookieAVEnd = cookieString.Length();
815
816	int32 attributeNameEnd = cookieString.FindFirst('=', index);
817	// If the cookie-av has no equals, the attribute-name is the entire
818	// cookie-av and the attribute-value is empty.
819	if (attributeNameEnd == -1 || attributeNameEnd > cookieAVEnd)
820		attributeNameEnd = cookieAVEnd;
821
822	int32 first = skip_whitespace_forward(cookieString, index);
823	int32 last = skip_whitespace_backward(cookieString, attributeNameEnd - 1);
824
825	if (first <= last)
826		cookieString.CopyInto(attribute, first, last - first + 1);
827	else
828		attribute.SetTo("");
829
830	if (attributeNameEnd == cookieAVEnd) {
831		value.SetTo("");
832		return cookieAVEnd;
833	}
834
835	first = skip_whitespace_forward(cookieString, attributeNameEnd + 1);
836	last = skip_whitespace_backward(cookieString, cookieAVEnd - 1);
837	if (first <= last)
838		cookieString.CopyInto(value, first, last - first + 1);
839	else
840		value.SetTo("");
841
842	// values may (or may not) have quotes around them.
843	if (value[0] == '"' && value[value.Length() - 1] == '"') {
844		value.Remove(0, 1);
845		value.Remove(value.Length() - 1, 1);
846	}
847
848	return cookieAVEnd;
849}
850
851
852BString
853BNetworkCookie::_DefaultPathForUrl(const BUrl& url)
854{
855	const BString& path = url.Path();
856	if (path.IsEmpty() || path.ByteAt(0) != '/')
857		return "";
858
859	int32 index = path.FindLast('/');
860	if (index == 0)
861		return "";
862
863	BString newPath = path;
864	newPath.Truncate(index);
865	return newPath;
866}
867