/* Copyright (c) 2006-2010, Linden Research, Inc. * * LLQtWebKit Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in GPL-license.txt in this distribution, or online at * http://secondlifegrid.net/technology-programs/license-virtual-world/viewerlicensing/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file FLOSS-exception.txt in this software distribution, or * online at * http://secondlifegrid.net/technology-programs/license-virtual-world/viewerlicensing/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. */ #include "networkcookiejar.h" #include "networkcookiejar_p.h" #include "twoleveldomains_p.h" //#define NETWORKCOOKIEJAR_DEBUG #ifndef QT_NO_DEBUG // ^ Prevent being left on in a released product by accident // qDebug any cookies that are rejected for further inspection #define NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES #include #endif #include #include #if defined(NETWORKCOOKIEJAR_DEBUG) #include #endif NetworkCookieJar::NetworkCookieJar(QObject *parent) : QNetworkCookieJar(parent) { d = new NetworkCookieJarPrivate; } NetworkCookieJar::~NetworkCookieJar() { delete d; } static QStringList splitHost(const QString &host) { QStringList parts = host.split(QLatin1Char('.'), QString::KeepEmptyParts); // Remove empty components that are on the start and end while (!parts.isEmpty() && parts.last().isEmpty()) parts.removeLast(); while (!parts.isEmpty() && parts.first().isEmpty()) parts.removeFirst(); return parts; } inline static bool shorterPaths(const QNetworkCookie &c1, const QNetworkCookie &c2) { return c2.path().length() < c1.path().length(); } QList NetworkCookieJar::cookiesForUrl(const QUrl &url) const { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << url; #endif // Generate split host QString host = url.host(); if (url.scheme().toLower() == QLatin1String("file")) host = QLatin1String("localhost"); QStringList urlHost = splitHost(host); // Get all the cookies for url QList cookies = d->tree.find(urlHost); if (urlHost.count() > 2) { int top = 2; if (d->matchesBlacklist(urlHost.last())) top = 3; urlHost.removeFirst(); while (urlHost.count() >= top) { cookies += d->tree.find(urlHost); urlHost.removeFirst(); } } // Prevent doing anything expensive in the common case where // there are no cookies to check if (cookies.isEmpty()) return cookies; QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC); const QString urlPath = d->urlPath(url); const bool isSecure = url.scheme().toLower() == QLatin1String("https"); QList::iterator i = cookies.begin(); for (; i != cookies.end();) { if (!d->matchingPath(*i, urlPath)) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << __FUNCTION__ << "Ignoring cookie, path does not match" << *i << urlPath; #endif i = cookies.erase(i); continue; } if (!isSecure && i->isSecure()) { i = cookies.erase(i); #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << __FUNCTION__ << "Ignoring cookie, security mismatch" << *i << !isSecure; #endif continue; } if (!i->isSessionCookie() && now > i->expirationDate()) { // remove now (expensive short term) because there will // probably be many more cookiesForUrl calls for this host d->tree.remove(splitHost(i->domain()), *i); #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << __FUNCTION__ << "Ignoring cookie, expiration issue" << *i << now; #endif i = cookies.erase(i); continue; } ++i; } // shorter paths should go first qSort(cookies.begin(), cookies.end(), shorterPaths); #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "returning" << cookies.count(); qDebug() << cookies; #endif return cookies; } static const qint32 NetworkCookieJarMagic = 0xae; QByteArray NetworkCookieJar::saveState () const { int version = 1; QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << qint32(NetworkCookieJarMagic); stream << qint32(version); stream << d->tree; return data; } bool NetworkCookieJar::restoreState(const QByteArray &state) { int version = 1; QByteArray sd = state; QDataStream stream(&sd, QIODevice::ReadOnly); if (stream.atEnd()) return false; qint32 marker; qint32 v; stream >> marker; stream >> v; if (marker != NetworkCookieJarMagic || v != version) return false; stream >> d->tree; if (stream.status() != QDataStream::Ok) { d->tree.clear(); return false; } return true; } /*! Remove any session cookies or cookies that have expired. */ void NetworkCookieJar::endSession() { const QList cookies = d->tree.all(); QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC); QList::const_iterator i = cookies.constBegin(); for (; i != cookies.constEnd();) { if (i->isSessionCookie() || (!i->isSessionCookie() && now > i->expirationDate())) { d->tree.remove(splitHost(i->domain()), *i); } ++i; } } static const int maxCookiePathLength = 1024; bool NetworkCookieJar::setCookiesFromUrl(const QList &cookieList, const QUrl &url) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << url; qDebug() << cookieList; #endif QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC); bool changed = false; QString fullUrlPath = url.path(); QString defaultPath = fullUrlPath.mid(0, fullUrlPath.lastIndexOf(QLatin1Char('/')) + 1); if (defaultPath.isEmpty()) defaultPath = QLatin1Char('/'); QString urlPath = d->urlPath(url); foreach (QNetworkCookie cookie, cookieList) { if (cookie.path().length() > maxCookiePathLength) continue; bool alreadyDead = !cookie.isSessionCookie() && cookie.expirationDate() < now; if (cookie.path().isEmpty()) { cookie.setPath(defaultPath); } // Matching the behavior of Firefox, no path checking is done when setting cookies // Safari does something even odder, when that paths don't match it keeps // the cookie, but changes the paths to the default path #if 0 else if (!d->matchingPath(cookie, urlPath)) { #ifdef NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "Blocked cookie because: path doesn't match: " << cookie << url; #endif continue; } #endif if (cookie.domain().isEmpty()) { QString host = url.host().toLower(); if (host.isEmpty()) continue; cookie.setDomain(host); } else if (!d->matchingDomain(cookie, url)) { #ifdef NETWORKCOOKIEJAR_LOGREJECTEDCOOKIES qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "Blocked cookie because: domain doesn't match: " << cookie << url; #endif continue; } // replace/remove existing cookies removeCookie(cookie); // Notify derived class onCookieSetFromURL(cookie, url, alreadyDead); if (alreadyDead) continue; changed = true; d->tree.insert(splitHost(cookie.domain()), cookie); } return changed; } QList NetworkCookieJar::allCookies() const { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__; #endif return d->tree.all(); } void NetworkCookieJar::clearCookies() { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__; #endif d->tree.clear(); } void NetworkCookieJar::setCookies(const QList &cookieList) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << cookieList.count(); #endif QDateTime now = QDateTime::currentDateTime().toTimeSpec(Qt::UTC); foreach (const QNetworkCookie &cookie, cookieList) { // If a matching cookie is already in the list, remove it. removeCookie(cookie); if(!cookie.isSessionCookie() && cookie.expirationDate() < now) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "removing cookie: " << cookie; #endif // This cookie has expired -- don't re-add it } else { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "adding cookie: " << cookie; #endif // this cookie has not expired -- save it d->tree.insert(splitHost(cookie.domain()), cookie); } } } void NetworkCookieJar::setAllCookies(const QList &cookieList) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << cookieList.count(); #endif clearCookies(); setCookies(cookieList); } void NetworkCookieJar::removeCookie(const QNetworkCookie &cookie) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "removing cookie: " << cookie; #endif // If a cookie with the matching domain, path, and name exists in the cookiejar, remove it. QString domain = cookie.domain(); Q_ASSERT(!domain.isEmpty()); QStringList urlHost = splitHost(domain); QList cookies = d->tree.find(urlHost); QList::const_iterator it = cookies.constBegin(); for (; it != cookies.constEnd(); ++it) { if (cookie.name() == it->name() && domain == it->domain() && cookie.path() == it->path()) { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "found matching cookie: " << *it; #endif d->tree.remove(urlHost, *it); break; } } } void NetworkCookieJar::dump() { #if defined(NETWORKCOOKIEJAR_DEBUG) qDebug() << "NetworkCookieJar::" << __FUNCTION__ << "dumping all cookies: "; QList cookies = allCookies(); foreach (const QNetworkCookie &cookie, cookies) { qDebug() << " " << cookie; } #endif } QString NetworkCookieJarPrivate::urlPath(const QUrl &url) const { QString urlPath = url.path(); if (!urlPath.endsWith(QLatin1Char('/'))) urlPath += QLatin1Char('/'); return urlPath; } bool NetworkCookieJarPrivate::matchingPath(const QNetworkCookie &cookie, const QString &urlPath) const { QString cookiePath = cookie.path(); if (!cookiePath.endsWith(QLatin1Char('/'))) cookiePath += QLatin1Char('/'); return urlPath.startsWith(cookiePath); } bool NetworkCookieJarPrivate::matchesBlacklist(const QString &string) const { if (!setSecondLevelDomain) { // Alternatively to save a little bit of ram we could just // use bsearch on twoLevelDomains in place for (int j = 0; twoLevelDomains[j]; ++j) secondLevelDomains += QLatin1String(twoLevelDomains[j]); setSecondLevelDomain = true; } QStringList::const_iterator i = qBinaryFind(secondLevelDomains.constBegin(), secondLevelDomains.constEnd(), string); return (i != secondLevelDomains.constEnd()); } bool NetworkCookieJarPrivate::matchingDomain(const QNetworkCookie &cookie, const QUrl &url) const { QString domain = cookie.domain().simplified().toLower(); domain.remove(QLatin1Char(' ')); QStringList parts = splitHost(domain); if (parts.isEmpty()) return false; // When there is only one part only file://localhost/ is accepted if (parts.count() == 1) { QString s = parts.first(); if (parts.first() != QLatin1String("localhost")) return false; if (url.scheme().toLower() == QLatin1String("file")) return true; } // Check for blacklist if (parts.count() == 2 && matchesBlacklist(parts.last())) return false; QStringList urlParts = url.host().toLower().split(QLatin1Char('.'), QString::SkipEmptyParts); if (urlParts.isEmpty()) return false; while (urlParts.count() > parts.count()) urlParts.removeFirst(); for (int j = 0; j < urlParts.count(); ++j) { if (urlParts.at(j) != parts.at(j)) { return false; } } return true; } void NetworkCookieJar::setSecondLevelDomains(const QStringList &secondLevelDomains) { d->setSecondLevelDomain = true; d->secondLevelDomains = secondLevelDomains; qSort(d->secondLevelDomains); } void NetworkCookieJar::onCookieSetFromURL(const QNetworkCookie &cookie, const QUrl &url, bool already_dead) { Q_UNUSED(cookie); Q_UNUSED(url); Q_UNUSED(already_dead); // Derived classes can use this to track cookie changes. }