* Copyright 2010-2014 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Adrien Destugues, pulkomandy@pulkomandy.tk
* Christophe Huriaux, c.huriaux@gmail.com
* Hamish Morrison, hamishm53@gmail.com
*/
#include <new>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Debug.h>
#include <HttpTime.h>
#include <NetworkCookie.h>
using namespace BPrivate::Network;
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());
if (url.Protocol() == "file" && url.Host().Length() == 0) {
SetDomain("localhost");
}
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);
BString expirationString;
int32 expiration;
if (archive->FindString(kArchivedCookieExpirationDate, &expirationString)
== B_OK) {
BDateTime time = BHttpTime(expirationString).Parse();
SetExpirationDate(time);
} else if (archive->FindInt32(kArchivedCookieExpirationDate, &expiration)
== B_OK) {
SetExpirationDate((time_t)expiration);
}
}
BNetworkCookie::BNetworkCookie()
{
_Reset();
}
BNetworkCookie::~BNetworkCookie()
{
}
status_t
BNetworkCookie::ParseCookieString(const BString& string, const BUrl& url)
{
_Reset();
SetPath(_DefaultPathForUrl(url));
SetDomain(url.Host());
fHostOnly = true;
if (url.Protocol() == "file" && url.Host().Length() == 0) {
fDomain = "localhost";
}
BString name;
BString value;
int32 index = 0;
index = _ExtractNameValuePair(string, name, value, index);
if (index == -1 || value.Length() > 4096) {
return B_BAD_DATA;
}
SetName(name);
SetValue(value);
status_t result = B_OK;
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);
if (name.ICompare("max-age") == 0) {
if (value.IsEmpty()) {
result = B_BAD_VALUE;
continue;
}
char* end = NULL;
errno = 0;
long maxAge = strtol(value.String(), &end, 10);
if (*end == '\0')
SetMaxAge((int)maxAge);
else if (errno == ERANGE && maxAge == LONG_MAX)
SetMaxAge(INT_MAX);
else
SetMaxAge(-1);
} else if (name.ICompare("expires") == 0) {
if (value.IsEmpty()) {
continue;
}
BDateTime parsed = BHttpTime(value).Parse();
SetExpirationDate(parsed);
} else if (name.ICompare("domain") == 0) {
if (value.IsEmpty()) {
result = B_BAD_VALUE;
continue;
}
status_t domainResult = SetDomain(value);
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 (!_CanBeSetFromUrl(url))
result = B_NOT_ALLOWED;
if (result != B_OK)
_Reset();
return result;
}
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& to)
{
fPath.Truncate(0);
fRawFullCookieValid = false;
if (to[0] != '/' || to.Length() > 4096)
return B_BAD_DATA;
if (to.EndsWith("/.") || to.EndsWith("/.."))
return B_BAD_DATA;
if (to.FindFirst("/../") >= 0 || to.FindFirst("/./") >= 0)
return B_BAD_DATA;
fPath = to;
return B_OK;
}
status_t
BNetworkCookie::SetDomain(const BString& domain)
{
BString newDomain = domain;
if (newDomain[0] == '.')
newDomain.Remove(0, 1);
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_LOCAL_TIME);
int64_t date = expiration.Time_t();
date += (int64_t)maxAge;
if (date > INT_MAX)
date = INT_MAX;
expiration.SetTime_t(date);
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;
} 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;
}
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(fExpiration);
if (!fExpirationStringValid) {
fExpirationString = date.ToString(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 (!fRawCookieValid) {
fRawCookie.Truncate(0);
fRawCookieValid = true;
fRawCookie << fName << "=" << fValue;
}
if (!full)
return fRawCookie;
if (!fRawFullCookieValid) {
fRawFullCookie = fRawCookie;
fRawFullCookieValid = true;
if (HasDomain())
fRawFullCookie << "; Domain=" << fDomain;
if (HasExpirationDate())
fRawFullCookie << "; Expires=" << ExpirationString();
if (HasPath())
fRawFullCookie << "; Path=" << fPath;
if (Secure())
fRawFullCookie << "; Secure";
if (HttpOnly())
fRawFullCookie << "; HttpOnly";
}
return fRawFullCookie;
}
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;
if (url.Protocol() == "file")
return Domain() == "localhost" && IsValidForPath(url.Path());
return IsValidForDomain(url.Host()) && IsValidForPath(url.Path());
}
bool
BNetworkCookie::IsValidForDomain(const BString& domain) const
{
const BString& cookieDomain = Domain();
int32 difference = domain.Length() - cookieDomain.Length();
if (difference < 0)
return false;
if (IsHostOnly())
return domain == cookieDomain;
const char* suffix = domain.String() + difference;
return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0
|| domain[difference - 1] == '.'));
}
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;
return normalizedPath.Compare(cookiePath, cookiePath.Length()) == 0;
}
bool
BNetworkCookie::_CanBeSetFromUrl(const BUrl& url) const
{
if (url.Protocol() == "file")
return Domain() == "localhost" && _CanBeSetFromPath(url.Path());
return _CanBeSetFromDomain(url.Host()) && _CanBeSetFromPath(url.Path());
}
bool
BNetworkCookie::_CanBeSetFromDomain(const BString& domain) const
{
const BString& cookieDomain = Domain();
int32 difference = domain.Length() - cookieDomain.Length();
if (difference < 0) {
const char* suffix = cookieDomain.String() + difference;
return (strcmp(suffix, domain.String()) == 0 && (difference == 0
|| cookieDomain[difference - 1] == '.'));
}
if (IsHostOnly())
return domain == cookieDomain;
const char* suffix = domain.String() + difference;
return (strcmp(suffix, cookieDomain.String()) == 0 && (difference == 0
|| domain[difference - 1] == '.'));
}
bool
BNetworkCookie::_CanBeSetFromPath(const BString& path) const
{
BString normalizedPath = path;
int slashPos = normalizedPath.FindLast('/');
normalizedPath.Truncate(slashPos);
if (Path().Compare(normalizedPath, normalizedPath.Length()) == 0)
return true;
else if (normalizedPath.Compare(Path(), Path().Length()) == 0)
return true;
return false;
}
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();
}
bool
BNetworkCookie::ShouldDeleteAtExit() const
{
return IsSessionCookie() || ShouldDeleteNow();
}
bool
BNetworkCookie::ShouldDeleteNow() const
{
if (HasExpirationDate())
return (BDateTime::CurrentDateTime(B_GMT_TIME) > fExpiration);
return false;
}
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;
if (HasDomain()) {
error = into->AddString(kArchivedCookieDomain, fDomain);
if (error != B_OK)
return error;
}
if (HasExpirationDate()) {
error = into->AddString(kArchivedCookieExpirationDate,
BHttpTime(fExpiration).ToString());
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;
}
BArchivable*
BNetworkCookie::Instantiate(BMessage* archive)
{
if (archive->HasString(kArchivedCookieName)
&& archive->HasString(kArchivedCookieValue))
return new(std::nothrow) BNetworkCookie(archive);
return NULL;
}
bool
BNetworkCookie::operator==(const BNetworkCookie& other)
{
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)
{
int32 firstEquals = cookieString.FindFirst('=', index);
int32 nameValueEnd = cookieString.FindFirst(';', index);
if (nameValueEnd == -1)
nameValueEnd = cookieString.Length();
if (firstEquals == -1 || firstEquals > nameValueEnd)
return -1;
int32 first = skip_whitespace_forward(cookieString, index);
int32 last = skip_whitespace_backward(cookieString, firstEquals - 1);
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)
{
int32 cookieAVEnd = cookieString.FindFirst(';', index);
if (cookieAVEnd == -1)
cookieAVEnd = cookieString.Length();
int32 attributeNameEnd = cookieString.FindFirst('=', index);
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("");
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;
}