/* * Copyright 2010-2014 Haiku Inc. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: * Christophe Huriaux, c.huriaux@gmail.com * Hamish Morrison, hamishm53@gmail.com */ #include #include #include #include #include #include #include "NetworkCookieJarPrivate.h" using namespace BPrivate::Network; // #define TRACE_COOKIE #ifdef TRACE_COOKIE # define TRACE(x...) printf(x) #else # define TRACE(x...) ; #endif const char* kArchivedCookieMessageName = "be:cookie"; BNetworkCookieJar::BNetworkCookieJar() : fCookieHashMap(new(std::nothrow) PrivateHashMap()) { } BNetworkCookieJar::BNetworkCookieJar(const BNetworkCookieJar& other) : fCookieHashMap(new(std::nothrow) PrivateHashMap()) { *this = other; } BNetworkCookieJar::BNetworkCookieJar(const BNetworkCookieList& otherList) : fCookieHashMap(new(std::nothrow) PrivateHashMap()) { AddCookies(otherList); } BNetworkCookieJar::BNetworkCookieJar(BMessage* archive) : fCookieHashMap(new(std::nothrow) PrivateHashMap()) { BMessage extractedCookie; for (int32 i = 0; archive->FindMessage(kArchivedCookieMessageName, i, &extractedCookie) == B_OK; i++) { BNetworkCookie* heapCookie = new(std::nothrow) BNetworkCookie(&extractedCookie); if (heapCookie == NULL) break; if (AddCookie(heapCookie) != B_OK) { delete heapCookie; continue; } } } BNetworkCookieJar::~BNetworkCookieJar() { for (Iterator it = GetIterator(); it.Next() != NULL;) delete it.Remove(); fCookieHashMap->Lock(); PrivateHashMap::Iterator it = fCookieHashMap->GetIterator(); while (it.HasNext()) { BNetworkCookieList* list = it.Next().value; list->LockForWriting(); delete list; } delete fCookieHashMap; } // #pragma mark Add cookie to cookie jar status_t BNetworkCookieJar::AddCookie(const BNetworkCookie& cookie) { BNetworkCookie* heapCookie = new(std::nothrow) BNetworkCookie(cookie); if (heapCookie == NULL) return B_NO_MEMORY; status_t result = AddCookie(heapCookie); if (result != B_OK) delete heapCookie; return result; } status_t BNetworkCookieJar::AddCookie(const BString& cookie, const BUrl& referrer) { BNetworkCookie* heapCookie = new(std::nothrow) BNetworkCookie(cookie, referrer); if (heapCookie == NULL) return B_NO_MEMORY; status_t result = AddCookie(heapCookie); if (result != B_OK) delete heapCookie; return result; } status_t BNetworkCookieJar::AddCookie(BNetworkCookie* cookie) { if (fCookieHashMap == NULL) return B_NO_MEMORY; if (cookie == NULL || !cookie->IsValid()) return B_BAD_VALUE; HashString key(cookie->Domain()); if (!fCookieHashMap->Lock()) return B_ERROR; // Get the cookies for the requested domain, or create a new list if there // isn't one yet. BNetworkCookieList* list = fCookieHashMap->Get(key); if (list == NULL) { list = new(std::nothrow) BNetworkCookieList(); if (list == NULL) { fCookieHashMap->Unlock(); return B_NO_MEMORY; } if (fCookieHashMap->Put(key, list) != B_OK) { fCookieHashMap->Unlock(); delete list; return B_NO_MEMORY; } } if (list->LockForWriting() != B_OK) { fCookieHashMap->Unlock(); return B_ERROR; } fCookieHashMap->Unlock(); // Remove any cookie with the same key as the one we're trying to add (it // replaces/updates them) for (int32 i = 0; i < list->CountItems(); i++) { const BNetworkCookie* c = list->ItemAt(i); if (c->Name() == cookie->Name() && c->Path() == cookie->Path()) { list->RemoveItemAt(i); break; } } // If the cookie has an expiration date in the past, stop here: we // effectively deleted a cookie. if (cookie->ShouldDeleteNow()) { TRACE("Remove cookie: %s\n", cookie->RawCookie(true).String()); delete cookie; } else { // Make sure the cookie has cached the raw string and expiration date // string, so it is now actually immutable. This way we can safely // read the cookie data from multiple threads without any locking. const BString& raw = cookie->RawCookie(true); (void)raw; TRACE("Add cookie: %s\n", raw.String()); // Keep the list sorted by path length (longest first). This makes sure // that cookies for most specific paths are returned first when // iterating the cookie jar. int32 i; for (i = 0; i < list->CountItems(); i++) { const BNetworkCookie* current = list->ItemAt(i); if (current->Path().Length() < cookie->Path().Length()) break; } list->AddItem(cookie, i); } list->Unlock(); return B_OK; } status_t BNetworkCookieJar::AddCookies(const BNetworkCookieList& cookies) { for (int32 i = 0; i < cookies.CountItems(); i++) { const BNetworkCookie* cookiePtr = cookies.ItemAt(i); // Using AddCookie by reference in order to avoid multiple // cookie jar share the same cookie pointers status_t result = AddCookie(*cookiePtr); if (result != B_OK) return result; } return B_OK; } // #pragma mark Purge useless cookies uint32 BNetworkCookieJar::DeleteOutdatedCookies() { int32 deleteCount = 0; const BNetworkCookie* cookiePtr; for (Iterator it = GetIterator(); (cookiePtr = it.Next()) != NULL;) { if (cookiePtr->ShouldDeleteNow()) { delete it.Remove(); deleteCount++; } } return deleteCount; } uint32 BNetworkCookieJar::PurgeForExit() { int32 deleteCount = 0; const BNetworkCookie* cookiePtr; for (Iterator it = GetIterator(); (cookiePtr = it.Next()) != NULL;) { if (cookiePtr->ShouldDeleteAtExit()) { delete it.Remove(); deleteCount++; } } return deleteCount; } // #pragma mark BArchivable interface status_t BNetworkCookieJar::Archive(BMessage* into, bool deep) const { status_t error = BArchivable::Archive(into, deep); if (error == B_OK) { const BNetworkCookie* cookiePtr; for (Iterator it = GetIterator(); (cookiePtr = it.Next()) != NULL;) { BMessage subArchive; error = cookiePtr->Archive(&subArchive, deep); if (error != B_OK) return error; error = into->AddMessage(kArchivedCookieMessageName, &subArchive); if (error != B_OK) return error; } } return error; } BArchivable* BNetworkCookieJar::Instantiate(BMessage* archive) { if (archive->HasMessage(kArchivedCookieMessageName)) return new(std::nothrow) BNetworkCookieJar(archive); return NULL; } // #pragma mark BFlattenable interface bool BNetworkCookieJar::IsFixedSize() const { // Flattened size vary return false; } type_code BNetworkCookieJar::TypeCode() const { // TODO: Add a B_COOKIEJAR_TYPE return B_ANY_TYPE; } ssize_t BNetworkCookieJar::FlattenedSize() const { _DoFlatten(); return fFlattened.Length() + 1; } status_t BNetworkCookieJar::Flatten(void* buffer, ssize_t size) const { if (FlattenedSize() > size) return B_ERROR; fFlattened.CopyInto(reinterpret_cast(buffer), 0, fFlattened.Length()); reinterpret_cast(buffer)[fFlattened.Length()] = 0; return B_OK; } bool BNetworkCookieJar::AllowsTypeCode(type_code) const { // TODO return false; } status_t BNetworkCookieJar::Unflatten(type_code, const void* buffer, ssize_t size) { BString flattenedCookies; flattenedCookies.SetTo(reinterpret_cast(buffer), size); while (flattenedCookies.Length() > 0) { BNetworkCookie tempCookie; BString tempCookieLine; int32 endOfLine = flattenedCookies.FindFirst('\n', 0); if (endOfLine == -1) tempCookieLine = flattenedCookies; else { flattenedCookies.MoveInto(tempCookieLine, 0, endOfLine); flattenedCookies.Remove(0, 1); } if (tempCookieLine.Length() != 0 && tempCookieLine[0] != '#') { for (int32 field = 0; field < 7; field++) { BString tempString; int32 endOfField = tempCookieLine.FindFirst('\t', 0); if (endOfField == -1) tempString = tempCookieLine; else { tempCookieLine.MoveInto(tempString, 0, endOfField); tempCookieLine.Remove(0, 1); } switch (field) { case 0: tempCookie.SetDomain(tempString); break; case 1: // TODO: Useless field ATM break; case 2: tempCookie.SetPath(tempString); break; case 3: tempCookie.SetSecure(tempString == "TRUE"); break; case 4: tempCookie.SetExpirationDate(atoi(tempString)); break; case 5: tempCookie.SetName(tempString); break; case 6: tempCookie.SetValue(tempString); break; } // switch } // for loop AddCookie(tempCookie); } } return B_OK; } BNetworkCookieJar& BNetworkCookieJar::operator=(const BNetworkCookieJar& other) { if (&other == this) return *this; for (Iterator it = GetIterator(); it.Next() != NULL;) delete it.Remove(); BArchivable::operator=(other); BFlattenable::operator=(other); fFlattened = other.fFlattened; delete fCookieHashMap; fCookieHashMap = new(std::nothrow) PrivateHashMap(); for (Iterator it = other.GetIterator(); it.HasNext();) { const BNetworkCookie* cookie = it.Next(); AddCookie(*cookie); // Pass by reference so the cookie is copied. } return *this; } // #pragma mark Iterators BNetworkCookieJar::Iterator BNetworkCookieJar::GetIterator() const { return BNetworkCookieJar::Iterator(this); } BNetworkCookieJar::UrlIterator BNetworkCookieJar::GetUrlIterator(const BUrl& url) const { if (!url.HasPath()) { BUrl copy(url); copy.SetPath("/"); return BNetworkCookieJar::UrlIterator(this, copy); } return BNetworkCookieJar::UrlIterator(this, url); } void BNetworkCookieJar::_DoFlatten() const { fFlattened.Truncate(0); const BNetworkCookie* cookiePtr; for (Iterator it = GetIterator(); (cookiePtr = it.Next()) != NULL;) { fFlattened << cookiePtr->Domain() << '\t' << "TRUE" << '\t' << cookiePtr->Path() << '\t' << (cookiePtr->Secure()?"TRUE":"FALSE") << '\t' << (int32)cookiePtr->ExpirationDate() << '\t' << cookiePtr->Name() << '\t' << cookiePtr->Value() << '\n'; } } // #pragma mark Iterator BNetworkCookieJar::Iterator::Iterator(const Iterator& other) : fCookieJar(other.fCookieJar), fIterator(NULL), fLastList(NULL), fList(NULL), fElement(NULL), fLastElement(NULL), fIndex(0) { fIterator = new(std::nothrow) PrivateIterator( fCookieJar->fCookieHashMap->GetIterator()); _FindNext(); } BNetworkCookieJar::Iterator::Iterator(const BNetworkCookieJar* cookieJar) : fCookieJar(const_cast(cookieJar)), fIterator(NULL), fLastList(NULL), fList(NULL), fElement(NULL), fLastElement(NULL), fIndex(0) { fIterator = new(std::nothrow) PrivateIterator( fCookieJar->fCookieHashMap->GetIterator()); // Locate first cookie _FindNext(); } BNetworkCookieJar::Iterator::~Iterator() { if (fList != NULL) fList->Unlock(); if (fLastList != NULL) fLastList->Unlock(); delete fIterator; } BNetworkCookieJar::Iterator& BNetworkCookieJar::Iterator::operator=(const Iterator& other) { if (this == &other) return *this; delete fIterator; if (fList != NULL) fList->Unlock(); fCookieJar = other.fCookieJar; fIterator = NULL; fLastList = NULL; fList = NULL; fElement = NULL; fLastElement = NULL; fIndex = 0; fIterator = new(std::nothrow) PrivateIterator( fCookieJar->fCookieHashMap->GetIterator()); _FindNext(); return *this; } bool BNetworkCookieJar::Iterator::HasNext() const { return fElement; } const BNetworkCookie* BNetworkCookieJar::Iterator::Next() { if (!fElement) return NULL; const BNetworkCookie* result = fElement; _FindNext(); return result; } const BNetworkCookie* BNetworkCookieJar::Iterator::NextDomain() { if (!fElement) return NULL; const BNetworkCookie* result = fElement; if (!fIterator->fCookieMapIterator.HasNext()) { fElement = NULL; return result; } if (fList != NULL) fList->Unlock(); if (fCookieJar->fCookieHashMap->Lock()) { fList = fIterator->fCookieMapIterator.Next().value; fList->LockForReading(); while (fList->CountItems() == 0 && fIterator->fCookieMapIterator.HasNext()) { // Empty list. Skip it fList->Unlock(); fList = fIterator->fCookieMapIterator.Next().value; fList->LockForReading(); } fCookieJar->fCookieHashMap->Unlock(); } fIndex = 0; fElement = fList->ItemAt(fIndex); return result; } const BNetworkCookie* BNetworkCookieJar::Iterator::Remove() { if (!fLastElement) return NULL; const BNetworkCookie* result = fLastElement; if (fIndex == 0) { if (fLastList && fCookieJar->fCookieHashMap->Lock()) { // We are on the first item of fList, so we need to remove the // last of fLastList fLastList->Unlock(); if (fLastList->LockForWriting() == B_OK) { fLastList->RemoveItemAt(fLastList->CountItems() - 1); // TODO if the list became empty, we could remove it from the // map, but this can be a problem if other iterators are still // referencing it. Is there a safe place and locking pattern // where we can do that? fLastList->Unlock(); fLastList->LockForReading(); } fCookieJar->fCookieHashMap->Unlock(); } } else { fIndex--; if (fCookieJar->fCookieHashMap->Lock()) { // Switch to a write lock fList->Unlock(); if (fList->LockForWriting() == B_OK) { fList->RemoveItemAt(fIndex); fList->Unlock(); } fList->LockForReading(); fCookieJar->fCookieHashMap->Unlock(); } } fLastElement = NULL; return result; } void BNetworkCookieJar::Iterator::_FindNext() { fLastElement = fElement; fIndex++; if (fList && fIndex < fList->CountItems()) { // Get an element from the current list fElement = fList->ItemAt(fIndex); return; } if (fIterator == NULL || !fIterator->fCookieMapIterator.HasNext()) { // We are done iterating fElement = NULL; return; } // Get an element from the next list if (fLastList != NULL) { fLastList->Unlock(); } fLastList = fList; if (fCookieJar->fCookieHashMap->Lock()) { fList = (fIterator->fCookieMapIterator.Next().value); fList->LockForReading(); while (fList->CountItems() == 0 && fIterator->fCookieMapIterator.HasNext()) { // Empty list. Skip it fList->Unlock(); fList = fIterator->fCookieMapIterator.Next().value; fList->LockForReading(); } fCookieJar->fCookieHashMap->Unlock(); } fIndex = 0; fElement = fList->ItemAt(fIndex); } // #pragma mark URL Iterator BNetworkCookieJar::UrlIterator::UrlIterator(const UrlIterator& other) : fCookieJar(other.fCookieJar), fIterator(NULL), fList(NULL), fLastList(NULL), fElement(NULL), fLastElement(NULL), fIndex(0), fLastIndex(0), fUrl(other.fUrl) { _Initialize(); } BNetworkCookieJar::UrlIterator::UrlIterator(const BNetworkCookieJar* cookieJar, const BUrl& url) : fCookieJar(const_cast(cookieJar)), fIterator(NULL), fList(NULL), fLastList(NULL), fElement(NULL), fLastElement(NULL), fIndex(0), fLastIndex(0), fUrl(url) { _Initialize(); } BNetworkCookieJar::UrlIterator::~UrlIterator() { if (fList != NULL) fList->Unlock(); if (fLastList != NULL) fLastList->Unlock(); delete fIterator; } bool BNetworkCookieJar::UrlIterator::HasNext() const { return fElement; } const BNetworkCookie* BNetworkCookieJar::UrlIterator::Next() { if (!fElement) return NULL; const BNetworkCookie* result = fElement; _FindNext(); return result; } const BNetworkCookie* BNetworkCookieJar::UrlIterator::Remove() { if (!fLastElement) return NULL; const BNetworkCookie* result = fLastElement; if (fCookieJar->fCookieHashMap->Lock()) { fLastList->Unlock(); if (fLastList->LockForWriting() == B_OK) { fLastList->RemoveItemAt(fLastIndex); if (fLastList->CountItems() == 0) { fCookieJar->fCookieHashMap->Remove(fIterator->fCookieMapIterator); delete fLastList; fLastList = NULL; } else { fLastList->Unlock(); fLastList->LockForReading(); } } fCookieJar->fCookieHashMap->Unlock(); } fLastElement = NULL; return result; } BNetworkCookieJar::UrlIterator& BNetworkCookieJar::UrlIterator::operator=( const BNetworkCookieJar::UrlIterator& other) { if (this == &other) return *this; // Teardown if (fList) fList->Unlock(); delete fIterator; // Init fCookieJar = other.fCookieJar; fIterator = NULL; fList = NULL; fLastList = NULL; fElement = NULL; fLastElement = NULL; fIndex = 0; fLastIndex = 0; fUrl = other.fUrl; _Initialize(); return *this; } void BNetworkCookieJar::UrlIterator::_Initialize() { BString domain = fUrl.Host(); if (!domain.Length()) { if (fUrl.Protocol() == "file") domain = "localhost"; else return; } fIterator = new(std::nothrow) PrivateIterator( fCookieJar->fCookieHashMap->GetIterator()); if (fIterator != NULL) { // Prepending a dot since _FindNext is going to call _SupDomain() domain.Prepend("."); fIterator->fKey.SetTo(domain, domain.Length()); _FindNext(); } } bool BNetworkCookieJar::UrlIterator::_SuperDomain() { BString domain(fIterator->fKey.GetString()); // Makes a copy of the characters from the key. This is important, // because HashString doesn't like SetTo to be called with a substring // of its original string (use-after-free + memcpy overwrite). int32 firstDot = domain.FindFirst('.'); if (firstDot < 0) return false; const char* nextDot = domain.String() + firstDot; fIterator->fKey.SetTo(nextDot + 1); return true; } void BNetworkCookieJar::UrlIterator::_FindNext() { fLastIndex = fIndex; fLastElement = fElement; if (fLastList != NULL) fLastList->Unlock(); fLastList = fList; if (fCookieJar->fCookieHashMap->Lock()) { if (fLastList) fLastList->LockForReading(); while (!_FindPath()) { if (!_SuperDomain()) { fElement = NULL; fCookieJar->fCookieHashMap->Unlock(); return; } _FindDomain(); } fCookieJar->fCookieHashMap->Unlock(); } } void BNetworkCookieJar::UrlIterator::_FindDomain() { if (fList != NULL) fList->Unlock(); if (fCookieJar->fCookieHashMap->Lock()) { fList = fCookieJar->fCookieHashMap->Get(fIterator->fKey); if (fList == NULL) fElement = NULL; else { fList->LockForReading(); } fCookieJar->fCookieHashMap->Unlock(); } fIndex = -1; } bool BNetworkCookieJar::UrlIterator::_FindPath() { fIndex++; while (fList && fIndex < fList->CountItems()) { fElement = fList->ItemAt(fIndex); if (fElement->IsValidForPath(fUrl.Path())) return true; fIndex++; } return false; } // #pragma mark - BNetworkCookieList BNetworkCookieList::BNetworkCookieList() { pthread_rwlock_init(&fLock, NULL); } BNetworkCookieList::~BNetworkCookieList() { // Note: this is expected to be called with the write lock held. pthread_rwlock_destroy(&fLock); } status_t BNetworkCookieList::LockForReading() { return pthread_rwlock_rdlock(&fLock); } status_t BNetworkCookieList::LockForWriting() { return pthread_rwlock_wrlock(&fLock); } status_t BNetworkCookieList::Unlock() { return pthread_rwlock_unlock(&fLock); }