772 lines
16 KiB
C++
772 lines
16 KiB
C++
/*
|
|
* Copyright 2010-2013 Haiku Inc. All rights reserved.
|
|
* Distributed under the terms of the MIT License.
|
|
*
|
|
* Authors:
|
|
* Christophe Huriaux, [email protected]
|
|
* Hamish Morrison, [email protected]
|
|
*/
|
|
|
|
|
|
#include <new>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#include <Debug.h>
|
|
#include <HttpTime.h>
|
|
#include <NetworkCookie.h>
|
|
|
|
using BPrivate::BHttpTime;
|
|
|
|
static const char* kArchivedCookieName = "be:cookie.name";
|
|
static const char* kArchivedCookieValue = "be:cookie.value";
|
|
static const char* kArchivedCookieDomain = "be:cookie.domain";
|
|
static const char* kArchivedCookiePath = "be:cookie.path";
|
|
static const char* kArchivedCookieExpirationDate = "be:cookie.expirationdate";
|
|
static const char* kArchivedCookieSecure = "be:cookie.secure";
|
|
static const char* kArchivedCookieHttpOnly = "be:cookie.httponly";
|
|
static const char* kArchivedCookieHostOnly = "be:cookie.hostonly";
|
|
|
|
|
|
BNetworkCookie::BNetworkCookie(const char* name, const char* value,
|
|
const BUrl& url)
|
|
{
|
|
_Reset();
|
|
fName = name;
|
|
fValue = value;
|
|
|
|
SetDomain(url.Host());
|
|
SetPath(_DefaultPathForUrl(url));
|
|
}
|
|
|
|
|
|
BNetworkCookie::BNetworkCookie(const BString& cookieString, const BUrl& url)
|
|
{
|
|
_Reset();
|
|
fInitStatus = ParseCookieString(cookieString, url);
|
|
}
|
|
|
|
|
|
BNetworkCookie::BNetworkCookie(BMessage* archive)
|
|
{
|
|
_Reset();
|
|
|
|
archive->FindString(kArchivedCookieName, &fName);
|
|
archive->FindString(kArchivedCookieValue, &fValue);
|
|
|
|
archive->FindString(kArchivedCookieDomain, &fDomain);
|
|
archive->FindString(kArchivedCookiePath, &fPath);
|
|
archive->FindBool(kArchivedCookieSecure, &fSecure);
|
|
archive->FindBool(kArchivedCookieHttpOnly, &fHttpOnly);
|
|
archive->FindBool(kArchivedCookieHostOnly, &fHostOnly);
|
|
|
|
int32 expiration;
|
|
if (archive->FindInt32(kArchivedCookieExpirationDate, &expiration)
|
|
== B_OK) {
|
|
SetExpirationDate((time_t)expiration);
|
|
}
|
|
}
|
|
|
|
|
|
BNetworkCookie::BNetworkCookie()
|
|
{
|
|
_Reset();
|
|
}
|
|
|
|
|
|
BNetworkCookie::~BNetworkCookie()
|
|
{
|
|
}
|
|
|
|
|
|
// #pragma mark String to cookie fields
|
|
|
|
|
|
status_t
|
|
BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
|
|
{
|
|
_Reset();
|
|
|
|
// Set default values (these can be overriden later on)
|
|
SetPath(_DefaultPathForUrl(url));
|
|
SetDomain(url.Host());
|
|
fHostOnly = true;
|
|
|
|
BString name;
|
|
BString value;
|
|
int32 index = 0;
|
|
|
|
// Parse the name and value of the cookie
|
|
index = _ExtractNameValuePair(string, name, value, index);
|
|
if (index == -1) {
|
|
// The set-cookie-string is not valid
|
|
return B_BAD_DATA;
|
|
}
|
|
|
|
SetName(name);
|
|
SetValue(value);
|
|
|
|
// Note on error handling: even if there are parse errors, we will continue
|
|
// and try to parse as much from the cookie as we can.
|
|
status_t result = B_OK;
|
|
|
|
// Parse the remaining cookie attributes.
|
|
while (index < string.Length()) {
|
|
ASSERT(string[index] == ';');
|
|
index++;
|
|
|
|
index = _ExtractAttributeValuePair(string, name, value, index);
|
|
|
|
if (name.ICompare("secure") == 0)
|
|
SetSecure(true);
|
|
else if (name.ICompare("httponly") == 0)
|
|
SetHttpOnly(true);
|
|
|
|
// The following attributes require a value.
|
|
|
|
if (name.ICompare("max-age") == 0) {
|
|
if (value.IsEmpty()) {
|
|
result = B_BAD_VALUE;
|
|
continue;
|
|
}
|
|
// Validate the max-age value.
|
|
char* end = NULL;
|
|
long maxAge = strtol(value.String(), &end, 10);
|
|
if (*end == '\0')
|
|
SetMaxAge((int)maxAge);
|
|
else
|
|
SetMaxAge(-1); // cookie will expire immediately
|
|
} else if (name.ICompare("expires") == 0) {
|
|
if (value.IsEmpty()) {
|
|
result = B_BAD_VALUE;
|
|
continue;
|
|
}
|
|
BHttpTime date(value);
|
|
SetExpirationDate(date.Parse());
|
|
} else if (name.ICompare("domain") == 0) {
|
|
if (value.IsEmpty()) {
|
|
result = B_BAD_VALUE;
|
|
continue;
|
|
}
|
|
|
|
status_t domainResult = SetDomain(value);
|
|
// Do not reset the result to B_OK if something else already failed
|
|
if (result == B_OK)
|
|
result = domainResult;
|
|
} else if (name.ICompare("path") == 0) {
|
|
if (value.IsEmpty()) {
|
|
result = B_BAD_VALUE;
|
|
continue;
|
|
}
|
|
status_t pathResult = SetPath(value);
|
|
if (result == B_OK)
|
|
result = pathResult;
|
|
}
|
|
}
|
|
|
|
if (!IsValidForDomain(url.Host())) {
|
|
// Invalidate the cookie.
|
|
_Reset();
|
|
return B_NOT_ALLOWED;
|
|
}
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// #pragma mark Cookie fields modification
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetName(const BString& name)
|
|
{
|
|
fName = name;
|
|
fRawFullCookieValid = false;
|
|
fRawCookieValid = false;
|
|
return *this;
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetValue(const BString& value)
|
|
{
|
|
fValue = value;
|
|
fRawFullCookieValid = false;
|
|
fRawCookieValid = false;
|
|
return *this;
|
|
}
|
|
|
|
|
|
status_t
|
|
BNetworkCookie::SetPath(const BString& path)
|
|
{
|
|
if (path[0] != '/')
|
|
return B_BAD_DATA;
|
|
|
|
// TODO: canonicalize the path
|
|
fPath = path;
|
|
fRawFullCookieValid = false;
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
status_t
|
|
BNetworkCookie::SetDomain(const BString& domain)
|
|
{
|
|
// TODO: canonicalize the domain
|
|
BString newDomain = domain;
|
|
|
|
// RFC 2109 (legacy) support: domain string may start with a dot,
|
|
// meant to indicate the cookie should also be used for subdomains.
|
|
// RFC 6265 makes all cookies work for subdomains, unless the domain is
|
|
// not specified at all (in this case it has to exactly match the Url of
|
|
// the page that set the cookie). In any case, we don't need to handle
|
|
// dot-cookies specifically anymore, so just remove the extra dot.
|
|
if (newDomain[0] == '.')
|
|
newDomain.Remove(0, 1);
|
|
|
|
// check we're not trying to set a cookie on a TLD or empty domain
|
|
if (newDomain.FindLast('.') <= 0)
|
|
return B_BAD_DATA;
|
|
|
|
fDomain = newDomain.ToLower();
|
|
|
|
fHostOnly = false;
|
|
|
|
fRawFullCookieValid = false;
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetMaxAge(int32 maxAge)
|
|
{
|
|
BDateTime expiration = BDateTime::CurrentDateTime(B_GMT_TIME);
|
|
expiration.SetTime_t(expiration.Time_t() + maxAge);
|
|
return SetExpirationDate(expiration);
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetExpirationDate(time_t expireDate)
|
|
{
|
|
BDateTime expiration;
|
|
expiration.SetTime_t(expireDate);
|
|
return SetExpirationDate(expiration);
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetExpirationDate(BDateTime& expireDate)
|
|
{
|
|
if (!expireDate.IsValid()) {
|
|
fExpiration.SetTime_t(0);
|
|
fSessionCookie = true;
|
|
fExpirationStringValid = false;
|
|
fRawFullCookieValid = false;
|
|
} else {
|
|
fExpiration = expireDate;
|
|
fSessionCookie = false;
|
|
fExpirationStringValid = false;
|
|
fRawFullCookieValid = false;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetSecure(bool secure)
|
|
{
|
|
fSecure = secure;
|
|
fRawFullCookieValid = false;
|
|
return *this;
|
|
}
|
|
|
|
|
|
BNetworkCookie&
|
|
BNetworkCookie::SetHttpOnly(bool httpOnly)
|
|
{
|
|
fHttpOnly = httpOnly;
|
|
fRawFullCookieValid = false;
|
|
return *this;
|
|
}
|
|
|
|
|
|
// #pragma mark Cookie fields access
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::Name() const
|
|
{
|
|
return fName;
|
|
}
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::Value() const
|
|
{
|
|
return fValue;
|
|
}
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::Domain() const
|
|
{
|
|
return fDomain;
|
|
}
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::Path() const
|
|
{
|
|
return fPath;
|
|
}
|
|
|
|
|
|
time_t
|
|
BNetworkCookie::ExpirationDate() const
|
|
{
|
|
return fExpiration.Time_t();
|
|
}
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::ExpirationString() const
|
|
{
|
|
BHttpTime date(ExpirationDate());
|
|
|
|
if (!fExpirationStringValid) {
|
|
fExpirationString = date.ToString(BPrivate::B_HTTP_TIME_FORMAT_COOKIE);
|
|
fExpirationStringValid = true;
|
|
}
|
|
|
|
return fExpirationString;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::Secure() const
|
|
{
|
|
return fSecure;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HttpOnly() const
|
|
{
|
|
return fHttpOnly;
|
|
}
|
|
|
|
|
|
const BString&
|
|
BNetworkCookie::RawCookie(bool full) const
|
|
{
|
|
if (full && !fRawFullCookieValid) {
|
|
fRawFullCookie.Truncate(0);
|
|
fRawFullCookieValid = true;
|
|
|
|
fRawFullCookie << fName << "=" << fValue;
|
|
|
|
if (HasDomain())
|
|
fRawFullCookie << "; Domain=" << fDomain;
|
|
if (HasExpirationDate())
|
|
fRawFullCookie << "; Expires=" << ExpirationString();
|
|
if (HasPath())
|
|
fRawFullCookie << "; Path=" << fPath;
|
|
if (Secure())
|
|
fRawFullCookie << "; Secure";
|
|
if (HttpOnly())
|
|
fRawFullCookie << "; HttpOnly";
|
|
|
|
} else if (!full && !fRawCookieValid) {
|
|
fRawCookie.Truncate(0);
|
|
fRawCookieValid = true;
|
|
|
|
fRawCookie << fName << "=" << fValue;
|
|
}
|
|
|
|
return full ? fRawFullCookie : fRawCookie;
|
|
}
|
|
|
|
|
|
// #pragma mark Cookie test
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsHostOnly() const
|
|
{
|
|
return fHostOnly;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsSessionCookie() const
|
|
{
|
|
return fSessionCookie;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsValid() const
|
|
{
|
|
return fInitStatus == B_OK && HasName() && HasDomain();
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsValidForUrl(const BUrl& url) const
|
|
{
|
|
if (Secure() && url.Protocol() != "https")
|
|
return false;
|
|
|
|
return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsValidForDomain(const BString& domain) const
|
|
{
|
|
// TODO: canonicalize both domains
|
|
const BString& cookieDomain = Domain();
|
|
|
|
int32 difference = domain.Length() - cookieDomain.Length();
|
|
// If the cookie domain is longer than the domain string it cannot
|
|
// be valid.
|
|
if (difference < 0)
|
|
return false;
|
|
|
|
// If the cookie is host-only the domains must match exactly.
|
|
if (IsHostOnly())
|
|
return domain == cookieDomain;
|
|
|
|
// FIXME prevent supercookies with a domain of ".com" or similar
|
|
// This is NOT as straightforward as relying on the last dot in the domain.
|
|
// Here's a list of TLD:
|
|
// https://github.com/rsimoes/Mozilla-PublicSuffix/blob/master/effective_tld_names.dat
|
|
|
|
// FIXME do not do substring matching on IP addresses. The RFCs disallow it.
|
|
|
|
// Otherwise, the domains must match exactly, or the domain must have a dot
|
|
// character just before the common suffix.
|
|
const char* suffix = domain.String() + difference;
|
|
if (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0
|
|
|| domain[difference - 1] == '.'))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::IsValidForPath(const BString& path) const
|
|
{
|
|
const BString& cookiePath = Path();
|
|
BString normalizedPath = path;
|
|
|
|
int slashPos = normalizedPath.FindLast('/');
|
|
if (slashPos != normalizedPath.Length() - 1)
|
|
normalizedPath.Truncate(slashPos + 1);
|
|
|
|
if (normalizedPath.Length() < cookiePath.Length())
|
|
return false;
|
|
|
|
// The cookie path must be a prefix of the path string
|
|
return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
|
|
}
|
|
|
|
|
|
// #pragma mark Cookie fields existence tests
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HasName() const
|
|
{
|
|
return fName.Length() > 0;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HasValue() const
|
|
{
|
|
return fValue.Length() > 0;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HasDomain() const
|
|
{
|
|
return fDomain.Length() > 0;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HasPath() const
|
|
{
|
|
return fPath.Length() > 0;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::HasExpirationDate() const
|
|
{
|
|
return !IsSessionCookie();
|
|
}
|
|
|
|
|
|
// #pragma mark Cookie delete test
|
|
|
|
|
|
bool
|
|
BNetworkCookie::ShouldDeleteAtExit() const
|
|
{
|
|
return IsSessionCookie() || ShouldDeleteNow();
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::ShouldDeleteNow() const
|
|
{
|
|
if (HasExpirationDate())
|
|
return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// #pragma mark BArchivable members
|
|
|
|
|
|
status_t
|
|
BNetworkCookie::Archive(BMessage* into, bool deep) const
|
|
{
|
|
status_t error = BArchivable::Archive(into, deep);
|
|
|
|
if (error != B_OK)
|
|
return error;
|
|
|
|
error = into->AddString(kArchivedCookieName, fName);
|
|
if (error != B_OK)
|
|
return error;
|
|
|
|
error = into->AddString(kArchivedCookieValue, fValue);
|
|
if (error != B_OK)
|
|
return error;
|
|
|
|
|
|
// We add optional fields only if they're defined
|
|
if (HasDomain()) {
|
|
error = into->AddString(kArchivedCookieDomain, fDomain);
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
if (HasExpirationDate()) {
|
|
error = into->AddInt32(kArchivedCookieExpirationDate,
|
|
fExpiration.Time_t());
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
if (HasPath()) {
|
|
error = into->AddString(kArchivedCookiePath, fPath);
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
if (Secure()) {
|
|
error = into->AddBool(kArchivedCookieSecure, fSecure);
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
if (HttpOnly()) {
|
|
error = into->AddBool(kArchivedCookieHttpOnly, fHttpOnly);
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
if (IsHostOnly()) {
|
|
error = into->AddBool(kArchivedCookieHostOnly, true);
|
|
if (error != B_OK)
|
|
return error;
|
|
}
|
|
|
|
return B_OK;
|
|
}
|
|
|
|
|
|
/*static*/ BArchivable*
|
|
BNetworkCookie::Instantiate(BMessage* archive)
|
|
{
|
|
if (archive->HasString(kArchivedCookieName)
|
|
&& archive->HasString(kArchivedCookieValue))
|
|
return new(std::nothrow) BNetworkCookie(archive);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// #pragma mark Overloaded operators
|
|
|
|
|
|
bool
|
|
BNetworkCookie::operator==(const BNetworkCookie& other)
|
|
{
|
|
// Equality : name and values equals
|
|
return fName == other.fName && fValue == other.fValue;
|
|
}
|
|
|
|
|
|
bool
|
|
BNetworkCookie::operator!=(const BNetworkCookie& other)
|
|
{
|
|
return !(*this == other);
|
|
}
|
|
|
|
|
|
void
|
|
BNetworkCookie::_Reset()
|
|
{
|
|
fInitStatus = false;
|
|
|
|
fName.Truncate(0);
|
|
fValue.Truncate(0);
|
|
fDomain.Truncate(0);
|
|
fPath.Truncate(0);
|
|
fExpiration = BDateTime();
|
|
fSecure = false;
|
|
fHttpOnly = false;
|
|
|
|
fSessionCookie = true;
|
|
fHostOnly = true;
|
|
|
|
fRawCookieValid = false;
|
|
fRawFullCookieValid = false;
|
|
fExpirationStringValid = false;
|
|
}
|
|
|
|
|
|
int32
|
|
skip_whitespace_forward(const BString& string, int32 index)
|
|
{
|
|
while (index < string.Length() && (string[index] == ' '
|
|
|| string[index] == '\t'))
|
|
index++;
|
|
return index;
|
|
}
|
|
|
|
|
|
int32
|
|
skip_whitespace_backward(const BString& string, int32 index)
|
|
{
|
|
while (index >= 0 && (string[index] == ' ' || string[index] == '\t'))
|
|
index--;
|
|
return index;
|
|
}
|
|
|
|
|
|
int32
|
|
BNetworkCookie::_ExtractNameValuePair(const BString& cookieString,
|
|
BString& name, BString& value, int32 index)
|
|
{
|
|
// Find our name-value-pair and the delimiter.
|
|
int32 firstEquals = cookieString.FindFirst('=', index);
|
|
int32 nameValueEnd = cookieString.FindFirst(';', index);
|
|
|
|
// If the set-cookie-string lacks a semicolon, the name-value-pair
|
|
// is the whole string.
|
|
if (nameValueEnd == -1)
|
|
nameValueEnd = cookieString.Length();
|
|
|
|
// If the name-value-pair lacks an equals, the parse should fail.
|
|
if (firstEquals == -1 || firstEquals > nameValueEnd)
|
|
return -1;
|
|
|
|
int32 first = skip_whitespace_forward(cookieString, index);
|
|
int32 last = skip_whitespace_backward(cookieString, firstEquals - 1);
|
|
|
|
// If we lack a name, fail to parse.
|
|
if (first > last)
|
|
return -1;
|
|
|
|
cookieString.CopyInto(name, first, last - first + 1);
|
|
|
|
first = skip_whitespace_forward(cookieString, firstEquals + 1);
|
|
last = skip_whitespace_backward(cookieString, nameValueEnd - 1);
|
|
if (first <= last)
|
|
cookieString.CopyInto(value, first, last - first + 1);
|
|
else
|
|
value.SetTo("");
|
|
|
|
return nameValueEnd;
|
|
}
|
|
|
|
|
|
int32
|
|
BNetworkCookie::_ExtractAttributeValuePair(const BString& cookieString,
|
|
BString& attribute, BString& value, int32 index)
|
|
{
|
|
// Find the end of our cookie-av.
|
|
int32 cookieAVEnd = cookieString.FindFirst(';', index);
|
|
|
|
// If the unparsed-attributes lacks a semicolon, then the cookie-av is the
|
|
// whole string.
|
|
if (cookieAVEnd == -1)
|
|
cookieAVEnd = cookieString.Length();
|
|
|
|
int32 attributeNameEnd = cookieString.FindFirst('=', index);
|
|
// If the cookie-av has no equals, the attribute-name is the entire
|
|
// cookie-av and the attribute-value is empty.
|
|
if (attributeNameEnd == -1 || attributeNameEnd > cookieAVEnd)
|
|
attributeNameEnd = cookieAVEnd;
|
|
|
|
int32 first = skip_whitespace_forward(cookieString, index);
|
|
int32 last = skip_whitespace_backward(cookieString, attributeNameEnd - 1);
|
|
|
|
if (first <= last)
|
|
cookieString.CopyInto(attribute, first, last - first + 1);
|
|
else
|
|
attribute.SetTo("");
|
|
|
|
if (attributeNameEnd == cookieAVEnd) {
|
|
value.SetTo("");
|
|
return cookieAVEnd;
|
|
}
|
|
|
|
first = skip_whitespace_forward(cookieString, attributeNameEnd + 1);
|
|
last = skip_whitespace_backward(cookieString, cookieAVEnd - 1);
|
|
if (first <= last)
|
|
cookieString.CopyInto(value, first, last - first + 1);
|
|
else
|
|
value.SetTo("");
|
|
|
|
// values may (or may not) have quotes around them.
|
|
if (value[0] == '"' && value[value.Length() - 1] == '"') {
|
|
value.Remove(0, 1);
|
|
value.Remove(value.Length() - 1, 1);
|
|
}
|
|
|
|
return cookieAVEnd;
|
|
}
|
|
|
|
|
|
BString
|
|
BNetworkCookie::_DefaultPathForUrl(const BUrl& url)
|
|
{
|
|
const BString& path = url.Path();
|
|
if (path.IsEmpty() || path.ByteAt(0) != '/')
|
|
return "";
|
|
|
|
int32 index = path.FindLast('/');
|
|
if (index == 0)
|
|
return "";
|
|
|
|
BString newPath = path;
|
|
newPath.Truncate(index);
|
|
return newPath;
|
|
}
|