root/src/kits/network/libnetservices/NetworkCookieJar.cpp
/*
 * 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 <new>
#include <stdio.h>

#include <HashMap.h>
#include <HashString.h>
#include <Message.h>
#include <NetworkCookieJar.h>

#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<char*>(buffer), 0,
                fFlattened.Length());
        reinterpret_cast<char*>(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<const char*>(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<BNetworkCookieJar*>(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<BNetworkCookieJar*>(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);
}