diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | ce4a32fe52ef09d8f5ff1dd22c001110902b60a2 (patch) | |
tree | 5ac38a06f3dde268dc7927dc155896926aaf7012 /kioslave/http | |
download | tdelibs-ce4a32fe52ef09d8f5ff1dd22c001110902b60a2.tar.gz tdelibs-ce4a32fe52ef09d8f5ff1dd22c001110902b60a2.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdelibs@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kioslave/http')
43 files changed, 15727 insertions, 0 deletions
diff --git a/kioslave/http/Makefile.am b/kioslave/http/Makefile.am new file mode 100644 index 000000000..a29e06e9e --- /dev/null +++ b/kioslave/http/Makefile.am @@ -0,0 +1,31 @@ +# $Id$ +# Makefile.am of kdebase/kioslave/http + +SUBDIRS = kcookiejar + +INCLUDES= -I$(top_srcdir)/interfaces -I$(top_srcdir)/kio/httpfilter -I$(top_srcdir)/kdecore/network $(all_includes) $(GSSAPI_INCS) +AM_LDFLAGS = $(all_libraries) $(GSSAPI_RPATH) + +####### Files + +bin_PROGRAMS= +lib_LTLIBRARIES= +kdeinit_LTLIBRARIES = kio_http_cache_cleaner.la +kde_module_LTLIBRARIES = kio_http.la + +kio_http_la_SOURCES = http.cc +kio_http_la_METASOURCES = AUTO +kio_http_la_LIBADD = $(LIB_KIO) $(top_builddir)/kio/httpfilter/libhttpfilter.la $(top_builddir)/kio/misc/kntlm/libkntlm.la +kio_http_la_LDFLAGS = $(all_libraries) $(GSSAPI_RPATH) -module $(KDE_PLUGIN) $(GSSAPI_LIBS) + +kio_http_cache_cleaner_la_SOURCES = http_cache_cleaner.cpp +kio_http_cache_cleaner_la_LIBADD = $(LIB_KIO) +kio_http_cache_cleaner_la_LDFLAGS = -module -avoid-version + +noinst_HEADERS = http.h + +kdelnkdir = $(kde_servicesdir) +kdelnk_DATA = http_cache_cleaner.desktop http.protocol https.protocol \ + webdav.protocol webdavs.protocol + +include $(top_srcdir)/admin/Doxyfile.am diff --git a/kioslave/http/README.http_cache_cleaner b/kioslave/http/README.http_cache_cleaner new file mode 100644 index 000000000..7714bfba6 --- /dev/null +++ b/kioslave/http/README.http_cache_cleaner @@ -0,0 +1,20 @@ +khttpcache README +================= + +khttpcache checks the HTTP Cache of a user +and throws out expired entries. + +TODO: + +* Skip entries which end in .new and are younger than +30 minutes / delte entries which end in .new and are +older than 30 minutes. + +* Let kio_http fill in expire dates other than 0. + +DONE: + +* Start khttpcache from kio_http if the file "cleaned" +is older than 30(?) minutes. + +* Accept command line parameteres diff --git a/kioslave/http/README.webdav b/kioslave/http/README.webdav new file mode 100644 index 000000000..c7ee900bb --- /dev/null +++ b/kioslave/http/README.webdav @@ -0,0 +1,184 @@ +This document describes how to add support for extended webdav features (locking, +properties etc.) to your webdav-aware application. +Author: Hamish Rodda, rodda@kde.org +Version: 0.3 + +Compatable with (tested on): +Apache + mod_dav version 1 and 2 +Zope +Silverstream webdav server + +Applications supporting extended webdav features + (include name and contact email, in case the interface has to change): +[none currently] + +Much of the info here is elaborated by rfc #2518; the rest can be understood by reading +davPropStat() in http.cc, specifically the setMetaData() calls. + +Extended information is transferred via kio's metadata system... + +=== MISCELLANEOUS === +Display Names (names suitable for presentation to the user) are passed as the metadata +element davDisplayName. + +Source template locations (href, usually an absolute URL w/o host info) +are passed as element davSource. + +Content languages are passed as element davContentLanguage. + +Extra webdav headers are passed as metadata element davHeader + +For doing a webdav SEARCH, use listDir() and set the metadata element +davSearchQuery to the search query. The root element of this query should be like +<d:basicsearch> or <d:sql>. + +For doing a generic webdav action, call a special request, with +the following data: +int, value 7 (WEBDAV generic) +KURL url +int method - the HTTP/WEBDAV method to call +Send the xml request and receive the xml response in the usual way. + +=== CREATING A LOCK === +To create a lock, call a special request, with the following data: + +int, value 5 (LOCK request) +KURL url - the location of the resource to lock +QString scope - the scope of the lock, currently "exclusive" or "shared" +QString type - the type of the lock, currently only "write" +QString owner (optional) - owner contact details (url) + +Additionally, the lock timeout requested from the server may be altered from the default +of Infinity by setting the metadata "davTimeout" to the number of seconds, or 0 for +infinity. + +=== REMOVING A LOCK === +To remove a lock, call a special request, with the following data: + +int, value 5 (LOCK request) +KURL url - the location of the resource to unlock + +metadata required: +davLockToken - the lock token to remove + +and, of course, any other lock information as below required for the operation +to suceed. + +=== SETTING LOCK INFORMATION === +To provide lock data so that urls can be accessed, you need to pass the following metadata: +davLockCount: (uint) the number of locks you are providing +davLockToken%1: (string) the token +(optional) davLockURL%1: (string) the absolute URL specified by the lock token +(optional) davLockNot%1: (value ignored) the presence of this meta key negates the lock + (ie. requires the lock to not be set) + +Example data: +============= +davLockCount: 2 +davLockToken1: opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76A +davLockNot1: (value ignored) +davLockToken2: opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76B +davLockURL2: http://www.foo.bar/container2/ + + +=== RECEIVING LOCK INFORMATION === +For each file, stat/listdir always returns two pieces of information: + +davSupportedLockCount: (uint) the number of lock types discovered for this resource. +davLockCount: (uint) the number of locks discovered on this resource. + +for each count, additional information is returned: + +=================== +Information about the locks on a resource: + +davLockCount: %1 (the number of locks to be described, as below) +*** Required items *** +davLockScope%1 - The scope of this lock. May be exclusive, shared, or a custom type. +davLockType%1 - The type of the lock. +davLockDepth%1 - The depth to which this lock applies + (0=only this resource, 1=this collection, infinity=applies recursively) + +*** Optional items *** +davLockOwner%1 - The owner of this lock. +davLockTimeout%1 - The timeout parameter. Possibilities: see section 9.8, rfc #2518 +davLockToken%1 - The token which iden + +=================== +Information about the lock types supported by the resource + +davSupportedLockCount: %1 (the number of locks types to be described, as below) + +davSupportedLockScope%1 - The scope of the lock (exclusive, shared, other custom type) +davSupportedLockType%1 - The type of the lock (webdav 1.0 supports only the "write" type) +=================== + +Example Metadata which would be supplied if the response was the example XML below: + +davSupportedLockCount: 2 +davLockCount: 2 +davLockScope1: exclusive +davLockType1: write +davLockDepth1: 0 +davLockOwner1: Jane Smith +davLockTimeout1: infinite +davLockToken1: opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76A +davLockScope2: shared +davLockType2: write +davLockDepth2: 1 +davLockOwner2: John Doe +davLockToken2: opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76B +davSupportedLockScope1: exclusive +davSupportedLockType1: write +davSupportedLockScope2: shared +davSupportedLockType2: write + + +(example XML:) + + <?xml version="1.0" encoding="utf-8" ?> + <D:multistatus xmlns:D='DAV:'> + <D:response> + <D:href>http://www.foo.bar/container/</D:href> + <D:propstat> + <D:prop> + <D:lockdiscovery> + <D:activelock> + <D:locktype><D:write/></D:locktype> + <D:lockscope><D:exclusive/></D:lockscope> + <D:depth>0</D:depth> + <D:owner>Jane Smith</D:owner> + <D:timeout>Infinite</D:timeout> + <D:locktoken> + <D:href> + opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76A + </D:href> + </D:locktoken> + </D:activelock> + <D:activelock> + <D:locktype><D:write/></D:locktype> + <D:lockscope><D:shared/></D:lockscope> + <D:depth>1</D:depth> + <D:owner>John Doe</D:owner> + <D:locktoken> + <D:href> + opaquelocktoken:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76B + </D:href> + </D:locktoken> + </D:activelock> + </D:lockdiscovery> + <D:supportedlock> + <D:lockentry> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + <D:lockentry> + <D:lockscope><D:shared/></D:lockscope> + <D:locktype><D:write/></D:locktype> + </D:lockentry> + </D:supportedlock> + </D:prop> + <D:status>HTTP/1.1 200 OK</D:status> + </D:propstat> + </D:response> + </D:multistatus> diff --git a/kioslave/http/THOUGHTS b/kioslave/http/THOUGHTS new file mode 100644 index 000000000..9715b5c2f --- /dev/null +++ b/kioslave/http/THOUGHTS @@ -0,0 +1,28 @@ +Here's a few ideas for those with blistered hands and nothing better to +do: + +SSL certificate verification: +We do establish SSL connections, but we never actually verify a +certificate! + +HTTP/1.1 Persistant Connections: +The header often specifies the timeout value used for connections. +Close the connection ourselves when the timeout has expired. That way +we don't loose time sending stuff to an already closed connection. + +Rating(s) support. http://www.w3.org/PICS +This might involve an external program to parse the labels, and something +to configure access. + +WebDAV support. MSIE5 calls it web folders support, and a similar +approach would probably be a good idea. Perhaps with an exists() +function.. one could tell if an http url was part of a WebDAV collection.. +and this could be used for some kind of integration with kfile... to +provide seamless integration. Uhm, also, this might entail an external +program (xml parser and such). + +"Friendly" error messages. How often have you seen a useless 404 message? +Again something I first notied in MSIE5, and that would be some sort of +translation of what an error really means. Yes this would have to be +i18n'd and easily turned off. But this could also be extended to all the +slaves (ftp, pop3, etc, etc). diff --git a/kioslave/http/TODO b/kioslave/http/TODO new file mode 100644 index 000000000..6484f0284 --- /dev/null +++ b/kioslave/http/TODO @@ -0,0 +1,45 @@ +The following is a list of items that are currently missing or partially implemented +in kio_http: + +- HTTP/1.1 Persistant Connections: +The header often specifies the timeout value used for connections. +Close the connection ourselves when the timeout has expired. That way +we don't loose time sending stuff to an already closed connection. + +- HTTP/1.1 Pipelining support +This more of an optimization of the http io-slave that is intended to make it +faster while using as few resources as possible. Work on this is currently +being done to add this support for KDE 3.x version. + +- WebDAV support: +The majority of the work for this is done, see README.webdav. GUI integration +into konqueror as a konqueror part would be nice, to add GUI support for +features such as locking. + +- Rating(s) support. http://www.w3.org/PICS: +This might involve an external program to parse the labels, and something to +configure access accordingly. There is only some basic things that need to be +added to kio_http to support this. The majority of the work has to be done at the +application level. A khtml plugin in kdeaddons to do this might be a nice idea. + +- P3P support: +This can also be implemented as a plugin to konqueror and does +not need any speical support in HTTP except perhaps sending a +flag that indicates that the web page provides some P3P information. +This is something that can be added as a plugin to kdeaddons. + + +Things that do not require programming +============================ + +- "Friendly" error message html page. +We currently support the sending of error messages, but this is only done if the server +sends back nicely formatted error messages. We do not have fall back HTML pages that +describe these error messages in a non-technical manner! This of course also means that +we will certainly need to have these files translated. + + +Maintainers +Waldo Bastian <bastian@kde.org> +Dawit Alemayehu <adawit@kde.org> +WebDAV support: Hamish Rodda <rodda@kde.org> diff --git a/kioslave/http/configure.in.bot b/kioslave/http/configure.in.bot new file mode 100644 index 000000000..56d051424 --- /dev/null +++ b/kioslave/http/configure.in.bot @@ -0,0 +1,10 @@ +dnl put here things which have to be done as very last part of configure + +if test "x$with_gssapi" = xNOTFOUND; then + echo "" + echo "You're missing GSSAPI/Kerberos." + echo "KDE can use GSSAPI/Kerberos to authenticate on certain secure websites." + echo "GSSAPI/Kerberos authentication is typically used on intranets." + echo "" + all_tests=bad +fi diff --git a/kioslave/http/configure.in.in b/kioslave/http/configure.in.in new file mode 100644 index 000000000..14f79ddc6 --- /dev/null +++ b/kioslave/http/configure.in.in @@ -0,0 +1,110 @@ +AC_MSG_CHECKING(whether to enable GSSAPI support) +AC_ARG_WITH(gssapi, +[ --with-gssapi=PATH Set path for GSSAPI files [default=check]], +[ case "$withval" in + yes) + with_gssapi=CHECK + ;; + esac ], +[ with_gssapi=CHECK ] +)dnl + +if test "x$with_gssapi" = "xCHECK" ; then + with_gssapi=NOTFOUND + KDE_FIND_PATH(krb5-config, KRB5_CONFIG, [${prefix}/bin ${exec_prefix}/bin /usr/bin /usr/local/bin /opt/local/bin /usr/lib/mit/bin], [ + AC_MSG_WARN([Could not find krb5-config]) + ]) + + if test -n "$KRB5_CONFIG"; then + kde_save_cflags="$CFLAGS" + unset CFLAGS + GSSAPI_INCS="`$KRB5_CONFIG --cflags gssapi`" + GSSAPI_LIBS="`$KRB5_CONFIG --libs gssapi`" + CFLAGS="$kde_save_cflags" + if test "$USE_RPATH" = yes; then + for args in $GSSAPI_LIBS; do + case $args in + -L/usr/lib) ;; + -L*) + GSSAPI_RPATH="$GSSAPI_RPATH $args" + ;; + esac + done + GSSAPI_RPATH=`echo $GSSAPI_RPATH | sed -e "s/-L/-R/g"` + fi + gssapi_incdir="$GSSAPI_INCS" + gssapi_libdir="$GSSAPI_LIBS" + with_gssapi=FOUND + if $KRB5_CONFIG --vendor | grep "Massachusetts" > /dev/null; then + gssapi_flavor=MIT + else + gssapi_flavor=HEIMDAL + fi + else + search_incs="$kde_includes /usr/include /usr/local/include" + AC_FIND_FILE(gssapi.h, $search_incs, gssapi_incdir) + if test -r $gssapi_incdir/gssapi.h ; then + test "x$gssapi_incdir" != "x/usr/include" && GSSAPI_INCS="-I$gssapi_incdir" + with_gssapi=FOUND + fi + if test $with_gssapi = FOUND ; then + with_gssapi=NOTFOUND + for ext in la so sl a dylib ; do + AC_FIND_FILE(libgssapi.$ext, $kde_libraries /usr/lib /usr/local/lib, + gssapi_libdir) + if test -r $gssapi_libdir/libgssapi.$ext ; then + if test "x$gssapi_libdir" != "x/usr/lib" ; then + GSSAPI_LIBS="-L$gssapi_libdir " + test "$USE_RPATH" = yes && GSSAPI_RPATH="-R $gssapi_libdir" + fi + GSSAPI_LIBS="${GSSAPI_LIBS}-lgssapi -lkrb5 -lasn1 -lcrypto -lroken -lcrypt ${LIBRESOLV}" + with_gssapi=FOUND + gssapi_flavor=HEIMDAL + break + fi + done + fi + fi +fi + +case "$with_gssapi" in +no) AC_MSG_RESULT(no) ;; +framework) + GSSAPI_LIBS="-Xlinker -framework -Xlinker Kerberos" + AC_DEFINE_UNQUOTED(HAVE_LIBGSSAPI, 1, [Define if you have GSSAPI libraries]) + GSSAPI_SUBDIR="gssapi" + AC_MSG_RESULT(Apple framework) + ;; +NOTFOUND) AC_MSG_RESULT(searched but not found) ;; +*) + if test "x$with_gssapi" = "xFOUND" ; then + msg="incs=$gssapi_incdir libs=$gssapi_libdir" + else + msg="$with_gssapi" + GSSAPI_ROOT="$with_gssapi" + if test "x$GSSAPI_ROOT" != "x/usr" ; then + GSSAPI_INCS="-I${GSSAPI_ROOT}/include" + GSSAPI_LIBS="-L${GSSAPI_ROOT}/lib " + if test "$USE_RPATH" = "yes" ; then + GSSAPI_RPATH="-R ${GSSAPI_ROOT}/lib" + fi + fi + if test -f ${GSSAPI_ROOT}/include/gssapi/gssapi.h ; then + gssapi_flavor=MIT + GSSAPI_LIBS="${GSSAPI_LIBS}-lgssapi_krb5 -lkrb5 -lk5crypto -lcom_err ${LIBRESOLV}" + else + gssapi_flavor=HEIMDAL + GSSAPI_LIBS="${GSSAPI_LIBS}-lgssapi -lkrb5 -lasn1 -lcrypto -lroken -lcrypt ${LIBRESOLV}" + fi + fi + if test "x$gssapi_flavor" = "xMIT" ; then + AC_DEFINE_UNQUOTED(GSSAPI_MIT, 1, [Define if you have the MIT Kerberos libraries]) + fi + AC_DEFINE_UNQUOTED(HAVE_LIBGSSAPI, 1, [Define if you have GSSAPI libraries]) + AC_MSG_RESULT($msg) + ;; +esac + +AC_SUBST(GSSAPI_INCS) +AC_SUBST(GSSAPI_LIBS) +AC_SUBST(GSSAPI_RPATH) diff --git a/kioslave/http/http.cc b/kioslave/http/http.cc new file mode 100644 index 000000000..5d9fa2eb7 --- /dev/null +++ b/kioslave/http/http.cc @@ -0,0 +1,6095 @@ +/* + Copyright (C) 2000-2003 Waldo Bastian <bastian@kde.org> + Copyright (C) 2000-2002 George Staikos <staikos@kde.org> + Copyright (C) 2000-2002 Dawit Alemayehu <adawit@kde.org> + Copyright (C) 2001,2002 Hamish Rodda <rodda@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License (LGPL) as published by the Free Software Foundation; + either version 2 of the License, or (at your option) any later + version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <config.h> + +#include <errno.h> +#include <fcntl.h> +#include <utime.h> +#include <stdlib.h> +#include <signal.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <netinet/in.h> // Required for AIX +#include <netinet/tcp.h> +#include <unistd.h> // must be explicitly included for MacOSX + +/* +#include <netdb.h> +#include <sys/time.h> +#include <sys/wait.h> +*/ + +#include <qdom.h> +#include <qfile.h> +#include <qregexp.h> +#include <qdatetime.h> +#include <qstringlist.h> + +#include <kurl.h> +#include <kidna.h> +#include <ksocks.h> +#include <kdebug.h> +#include <klocale.h> +#include <kconfig.h> +#include <kextsock.h> +#include <kservice.h> +#include <krfcdate.h> +#include <kmdcodec.h> +#include <kinstance.h> +#include <kresolver.h> +#include <kmimemagic.h> +#include <dcopclient.h> +#include <kdatastream.h> +#include <kapplication.h> +#include <kstandarddirs.h> +#include <kstringhandler.h> +#include <kremoteencoding.h> + +#include "kio/ioslave_defaults.h" +#include "kio/http_slave_defaults.h" + +#include "httpfilter.h" +#include "http.h" + +#ifdef HAVE_LIBGSSAPI +#ifdef GSSAPI_MIT +#include <gssapi/gssapi.h> +#else +#include <gssapi.h> +#endif /* GSSAPI_MIT */ + +// Catch uncompatible crap (BR86019) +#if defined(GSS_RFC_COMPLIANT_OIDS) && (GSS_RFC_COMPLIANT_OIDS == 0) +#include <gssapi/gssapi_generic.h> +#define GSS_C_NT_HOSTBASED_SERVICE gss_nt_service_name +#endif + +#endif /* HAVE_LIBGSSAPI */ + +#include <misc/kntlm/kntlm.h> + +using namespace KIO; + +extern "C" { + KDE_EXPORT int kdemain(int argc, char **argv); +} + +int kdemain( int argc, char **argv ) +{ + KLocale::setMainCatalogue("kdelibs"); + KInstance instance( "kio_http" ); + ( void ) KGlobal::locale(); + + if (argc != 4) + { + fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n"); + exit(-1); + } + + HTTPProtocol slave(argv[1], argv[2], argv[3]); + slave.dispatchLoop(); + return 0; +} + +/*********************************** Generic utility functions ********************/ + +static char * trimLead (char *orig_string) +{ + while (*orig_string == ' ') + orig_string++; + return orig_string; +} + +static bool isCrossDomainRequest( const QString& fqdn, const QString& originURL ) +{ + if (originURL == "true") // Backwards compatibility + return true; + + KURL url ( originURL ); + + // Document Origin domain + QString a = url.host(); + + // Current request domain + QString b = fqdn; + + if (a == b) + return false; + + QStringList l1 = QStringList::split('.', a); + QStringList l2 = QStringList::split('.', b); + + while(l1.count() > l2.count()) + l1.pop_front(); + + while(l2.count() > l1.count()) + l2.pop_front(); + + while(l2.count() >= 2) + { + if (l1 == l2) + return false; + + l1.pop_front(); + l2.pop_front(); + } + + return true; +} + +/* + Eliminates any custom header that could potentically alter the request +*/ +static QString sanitizeCustomHTTPHeader(const QString& _header) +{ + QString sanitizedHeaders; + QStringList headers = QStringList::split(QRegExp("[\r\n]"), _header); + + for(QStringList::Iterator it = headers.begin(); it != headers.end(); ++it) + { + QString header = (*it).lower(); + // Do not allow Request line to be specified and ignore + // the other HTTP headers. + if (header.find(':') == -1 || + header.startsWith("host") || + header.startsWith("via")) + continue; + + sanitizedHeaders += (*it); + sanitizedHeaders += "\r\n"; + } + + return sanitizedHeaders.stripWhiteSpace(); +} + + +#define NO_SIZE ((KIO::filesize_t) -1) + +#ifdef HAVE_STRTOLL +#define STRTOLL strtoll +#else +#define STRTOLL strtol +#endif + + +/************************************** HTTPProtocol **********************************************/ + +HTTPProtocol::HTTPProtocol( const QCString &protocol, const QCString &pool, + const QCString &app ) + :TCPSlaveBase( 0, protocol , pool, app, + (protocol == "https" || protocol == "webdavs") ) +{ + m_requestQueue.setAutoDelete(true); + + m_bBusy = false; + m_bFirstRequest = false; + m_bProxyAuthValid = false; + + m_iSize = NO_SIZE; + m_lineBufUnget = 0; + + m_protocol = protocol; + + m_maxCacheAge = DEFAULT_MAX_CACHE_AGE; + m_maxCacheSize = DEFAULT_MAX_CACHE_SIZE / 2; + m_remoteConnTimeout = DEFAULT_CONNECT_TIMEOUT; + m_remoteRespTimeout = DEFAULT_RESPONSE_TIMEOUT; + m_proxyConnTimeout = DEFAULT_PROXY_CONNECT_TIMEOUT; + + m_pid = getpid(); + + setMultipleAuthCaching( true ); + reparseConfiguration(); +} + +HTTPProtocol::~HTTPProtocol() +{ + httpClose(false); +} + +void HTTPProtocol::reparseConfiguration() +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::reparseConfiguration" << endl; + + m_strProxyRealm = QString::null; + m_strProxyAuthorization = QString::null; + ProxyAuthentication = AUTH_None; + m_bUseProxy = false; + + if (m_protocol == "https" || m_protocol == "webdavs") + m_iDefaultPort = DEFAULT_HTTPS_PORT; + else if (m_protocol == "ftp") + m_iDefaultPort = DEFAULT_FTP_PORT; + else + m_iDefaultPort = DEFAULT_HTTP_PORT; +} + +void HTTPProtocol::resetConnectionSettings() +{ + m_bEOF = false; + m_bError = false; + m_lineCount = 0; + m_iWWWAuthCount = 0; + m_lineCountUnget = 0; + m_iProxyAuthCount = 0; + +} + +void HTTPProtocol::resetResponseSettings() +{ + m_bRedirect = false; + m_redirectLocation = KURL(); + m_bChunked = false; + m_iSize = NO_SIZE; + + m_responseHeader.clear(); + m_qContentEncodings.clear(); + m_qTransferEncodings.clear(); + m_sContentMD5 = QString::null; + m_strMimeType = QString::null; + + setMetaData("request-id", m_request.id); +} + +void HTTPProtocol::resetSessionSettings() +{ + // Do not reset the URL on redirection if the proxy + // URL, username or password has not changed! + KURL proxy ( config()->readEntry("UseProxy") ); + + if ( m_strProxyRealm.isEmpty() || !proxy.isValid() || + m_proxyURL.host() != proxy.host() || + (!proxy.user().isNull() && proxy.user() != m_proxyURL.user()) || + (!proxy.pass().isNull() && proxy.pass() != m_proxyURL.pass()) ) + { + m_bProxyAuthValid = false; + m_proxyURL = proxy; + m_bUseProxy = m_proxyURL.isValid(); + + kdDebug(7113) << "(" << m_pid << ") Using proxy: " << m_bUseProxy << + " URL: " << m_proxyURL.url() << + " Realm: " << m_strProxyRealm << endl; + } + + m_bPersistentProxyConnection = config()->readBoolEntry("PersistentProxyConnection", false); + kdDebug(7113) << "(" << m_pid << ") Enable Persistent Proxy Connection: " + << m_bPersistentProxyConnection << endl; + + m_request.bUseCookiejar = config()->readBoolEntry("Cookies"); + m_request.bUseCache = config()->readBoolEntry("UseCache", true); + m_request.bErrorPage = config()->readBoolEntry("errorPage", true); + m_request.bNoAuth = config()->readBoolEntry("no-auth"); + m_strCacheDir = config()->readPathEntry("CacheDir"); + m_maxCacheAge = config()->readNumEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE); + m_request.window = config()->readEntry("window-id"); + + kdDebug(7113) << "(" << m_pid << ") Window Id = " << m_request.window << endl; + kdDebug(7113) << "(" << m_pid << ") ssl_was_in_use = " + << metaData ("ssl_was_in_use") << endl; + + m_request.referrer = QString::null; + if ( config()->readBoolEntry("SendReferrer", true) && + (m_protocol == "https" || m_protocol == "webdavs" || + metaData ("ssl_was_in_use") != "TRUE" ) ) + { + KURL referrerURL ( metaData("referrer") ); + if (referrerURL.isValid()) + { + // Sanitize + QString protocol = referrerURL.protocol(); + if (protocol.startsWith("webdav")) + { + protocol.replace(0, 6, "http"); + referrerURL.setProtocol(protocol); + } + + if (protocol.startsWith("http")) + { + referrerURL.setRef(QString::null); + referrerURL.setUser(QString::null); + referrerURL.setPass(QString::null); + m_request.referrer = referrerURL.url(); + } + } + } + + if ( config()->readBoolEntry("SendLanguageSettings", true) ) + { + m_request.charsets = config()->readEntry( "Charsets", "iso-8859-1" ); + + if ( !m_request.charsets.isEmpty() ) + m_request.charsets += DEFAULT_PARTIAL_CHARSET_HEADER; + + m_request.languages = config()->readEntry( "Languages", DEFAULT_LANGUAGE_HEADER ); + } + else + { + m_request.charsets = QString::null; + m_request.languages = QString::null; + } + + // Adjust the offset value based on the "resume" meta-data. + QString resumeOffset = metaData("resume"); + if ( !resumeOffset.isEmpty() ) + m_request.offset = resumeOffset.toInt(); // TODO: Convert to 64 bit + else + m_request.offset = 0; + + m_request.disablePassDlg = config()->readBoolEntry("DisablePassDlg", false); + m_request.allowCompressedPage = config()->readBoolEntry("AllowCompressedPage", true); + m_request.id = metaData("request-id"); + + // Store user agent for this host. + if ( config()->readBoolEntry("SendUserAgent", true) ) + m_request.userAgent = metaData("UserAgent"); + else + m_request.userAgent = QString::null; + + // Deal with cache cleaning. + // TODO: Find a smarter way to deal with cleaning the + // cache ? + if ( m_request.bUseCache ) + cleanCache(); + + // Deal with HTTP tunneling + if ( m_bIsSSL && m_bUseProxy && m_proxyURL.protocol() != "https" && + m_proxyURL.protocol() != "webdavs") + { + m_bNeedTunnel = true; + setRealHost( m_request.hostname ); + kdDebug(7113) << "(" << m_pid << ") SSL tunnel: Setting real hostname to: " + << m_request.hostname << endl; + } + else + { + m_bNeedTunnel = false; + setRealHost( QString::null); + } + + m_responseCode = 0; + m_prevResponseCode = 0; + + m_strRealm = QString::null; + m_strAuthorization = QString::null; + Authentication = AUTH_None; + + // Obtain the proxy and remote server timeout values + m_proxyConnTimeout = proxyConnectTimeout(); + m_remoteConnTimeout = connectTimeout(); + m_remoteRespTimeout = responseTimeout(); + + // Set the SSL meta-data here... + setSSLMetaData(); + + // Bounce back the actual referrer sent + setMetaData("referrer", m_request.referrer); + + // Follow HTTP/1.1 spec and enable keep-alive by default + // unless the remote side tells us otherwise or we determine + // the persistent link has been terminated by the remote end. + m_bKeepAlive = true; + m_keepAliveTimeout = 0; + m_bUnauthorized = false; + + // A single request can require multiple exchanges with the remote + // server due to authentication challenges or SSL tunneling. + // m_bFirstRequest is a flag that indicates whether we are + // still processing the first request. This is important because we + // should not force a close of a keep-alive connection in the middle + // of the first request. + // m_bFirstRequest is set to "true" whenever a new connection is + // made in httpOpenConnection() + m_bFirstRequest = false; +} + +void HTTPProtocol::setHost( const QString& host, int port, + const QString& user, const QString& pass ) +{ + // Reset the webdav-capable flags for this host + if ( m_request.hostname != host ) + m_davHostOk = m_davHostUnsupported = false; + + // is it an IPv6 address? + if (host.find(':') == -1) + { + m_request.hostname = host; + m_request.encoded_hostname = KIDNA::toAscii(host); + } + else + { + m_request.hostname = host; + int pos = host.find('%'); + if (pos == -1) + m_request.encoded_hostname = '[' + host + ']'; + else + // don't send the scope-id in IPv6 addresses to the server + m_request.encoded_hostname = '[' + host.left(pos) + ']'; + } + m_request.port = (port == 0) ? m_iDefaultPort : port; + m_request.user = user; + m_request.passwd = pass; + + m_bIsTunneled = false; + + kdDebug(7113) << "(" << m_pid << ") Hostname is now: " << m_request.hostname << + " (" << m_request.encoded_hostname << ")" <<endl; +} + +bool HTTPProtocol::checkRequestURL( const KURL& u ) +{ + kdDebug (7113) << "(" << m_pid << ") HTTPProtocol::checkRequestURL: " << u.url() << endl; + + m_request.url = u; + + if (m_request.hostname.isEmpty()) + { + error( KIO::ERR_UNKNOWN_HOST, i18n("No host specified.")); + return false; + } + + if (u.path().isEmpty()) + { + KURL newUrl(u); + newUrl.setPath("/"); + redirection(newUrl); + finished(); + return false; + } + + if ( m_protocol != u.protocol().latin1() ) + { + short unsigned int oldDefaultPort = m_iDefaultPort; + m_protocol = u.protocol().latin1(); + reparseConfiguration(); + if ( m_iDefaultPort != oldDefaultPort && + m_request.port == oldDefaultPort ) + m_request.port = m_iDefaultPort; + } + + resetSessionSettings(); + return true; +} + +void HTTPProtocol::retrieveContent( bool dataInternal /* = false */ ) +{ + kdDebug (7113) << "(" << m_pid << ") HTTPProtocol::retrieveContent " << endl; + if ( !retrieveHeader( false ) ) + { + if ( m_bError ) + return; + } + else + { + if ( !readBody( dataInternal ) && m_bError ) + return; + } + + httpClose(m_bKeepAlive); + + // if data is required internally, don't finish, + // it is processed before we finish() + if ( !dataInternal ) + { + if ((m_responseCode == 204) && + ((m_request.method == HTTP_GET) || (m_request.method == HTTP_POST))) + error(ERR_NO_CONTENT, ""); + else + finished(); + } +} + +bool HTTPProtocol::retrieveHeader( bool close_connection ) +{ + kdDebug (7113) << "(" << m_pid << ") HTTPProtocol::retrieveHeader " << endl; + while ( 1 ) + { + if (!httpOpen()) + return false; + + resetResponseSettings(); + if (!readHeader()) + { + if ( m_bError ) + return false; + + if (m_bIsTunneled) + { + kdDebug(7113) << "(" << m_pid << ") Re-establishing SSL tunnel..." << endl; + httpCloseConnection(); + } + } + else + { + // Do not save authorization if the current response code is + // 4xx (client error) or 5xx (server error). + kdDebug(7113) << "(" << m_pid << ") Previous Response: " + << m_prevResponseCode << endl; + kdDebug(7113) << "(" << m_pid << ") Current Response: " + << m_responseCode << endl; + + if (isSSLTunnelEnabled() && m_bIsSSL && !m_bUnauthorized && !m_bError) + { + // If there is no error, disable tunneling + if ( m_responseCode < 400 ) + { + kdDebug(7113) << "(" << m_pid << ") Unset tunneling flag!" << endl; + setEnableSSLTunnel( false ); + m_bIsTunneled = true; + // Reset the CONNECT response code... + m_responseCode = m_prevResponseCode; + continue; + } + else + { + if ( !m_request.bErrorPage ) + { + kdDebug(7113) << "(" << m_pid << ") Sending an error message!" << endl; + error( ERR_UNKNOWN_PROXY_HOST, m_proxyURL.host() ); + return false; + } + + kdDebug(7113) << "(" << m_pid << ") Sending an error page!" << endl; + } + } + + if (m_responseCode < 400 && (m_prevResponseCode == 401 || + m_prevResponseCode == 407)) + saveAuthorization(); + break; + } + } + + // Clear of the temporary POST buffer if it is not empty... + if (!m_bufPOST.isEmpty()) + { + m_bufPOST.resize(0); + kdDebug(7113) << "(" << m_pid << ") HTTP::retreiveHeader: Cleared POST " + "buffer..." << endl; + } + + if ( close_connection ) + { + httpClose(m_bKeepAlive); + finished(); + } + + return true; +} + +void HTTPProtocol::stat(const KURL& url) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::stat " << url.prettyURL() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + if ( m_protocol != "webdav" && m_protocol != "webdavs" ) + { + QString statSide = metaData(QString::fromLatin1("statSide")); + if ( statSide != "source" ) + { + // When uploading we assume the file doesn't exit + error( ERR_DOES_NOT_EXIST, url.prettyURL() ); + return; + } + + // When downloading we assume it exists + UDSEntry entry; + UDSAtom atom; + atom.m_uds = KIO::UDS_NAME; + atom.m_str = url.fileName(); + entry.append( atom ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = S_IFREG; // a file + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = S_IRUSR | S_IRGRP | S_IROTH; // readable by everybody + entry.append( atom ); + + statEntry( entry ); + finished(); + return; + } + + davStatList( url ); +} + +void HTTPProtocol::listDir( const KURL& url ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::listDir " << url.url() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + if (!url.protocol().startsWith("webdav")) { + error(ERR_UNSUPPORTED_ACTION, url.prettyURL()); + return; + } + + davStatList( url, false ); +} + +void HTTPProtocol::davSetRequest( const QCString& requestXML ) +{ + // insert the document into the POST buffer, kill trailing zero byte + m_bufPOST = requestXML; + + if (m_bufPOST.size()) + m_bufPOST.truncate( m_bufPOST.size() - 1 ); +} + +void HTTPProtocol::davStatList( const KURL& url, bool stat ) +{ + UDSEntry entry; + UDSAtom atom; + + // check to make sure this host supports WebDAV + if ( !davHostOk() ) + return; + + // Maybe it's a disguised SEARCH... + QString query = metaData("davSearchQuery"); + if ( !query.isEmpty() ) + { + QCString request = "<?xml version=\"1.0\"?>\r\n"; + request.append( "<D:searchrequest xmlns:D=\"DAV:\">\r\n" ); + request.append( query.utf8() ); + request.append( "</D:searchrequest>\r\n" ); + + davSetRequest( request ); + } else { + // We are only after certain features... + QCString request; + request = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + "<D:propfind xmlns:D=\"DAV:\">"; + + // insert additional XML request from the davRequestResponse metadata + if ( hasMetaData( "davRequestResponse" ) ) + request += metaData( "davRequestResponse" ).utf8(); + else { + // No special request, ask for default properties + request += "<D:prop>" + "<D:creationdate/>" + "<D:getcontentlength/>" + "<D:displayname/>" + "<D:source/>" + "<D:getcontentlanguage/>" + "<D:getcontenttype/>" + "<D:executable/>" + "<D:getlastmodified/>" + "<D:getetag/>" + "<D:supportedlock/>" + "<D:lockdiscovery/>" + "<D:resourcetype/>" + "</D:prop>"; + } + request += "</D:propfind>"; + + davSetRequest( request ); + } + + // WebDAV Stat or List... + m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + m_request.davData.depth = stat ? 0 : 1; + if (!stat) + m_request.url.adjustPath(+1); + + retrieveContent( true ); + + // Has a redirection already been called? If so, we're done. + if (m_bRedirect) { + finished(); + return; + } + + QDomDocument multiResponse; + multiResponse.setContent( m_bufWebDavData, true ); + + bool hasResponse = false; + + for ( QDomNode n = multiResponse.documentElement().firstChild(); + !n.isNull(); n = n.nextSibling()) + { + QDomElement thisResponse = n.toElement(); + if (thisResponse.isNull()) + continue; + + hasResponse = true; + + QDomElement href = thisResponse.namedItem( "href" ).toElement(); + if ( !href.isNull() ) + { + entry.clear(); + + QString urlStr = href.text(); + int encoding = remoteEncoding()->encodingMib(); + if ((encoding == 106) && (!KStringHandler::isUtf8(KURL::decode_string(urlStr, 4).latin1()))) + encoding = 4; // Use latin1 if the file is not actually utf-8 + + KURL thisURL ( urlStr, encoding ); + + atom.m_uds = KIO::UDS_NAME; + + if ( thisURL.isValid() ) { + // don't list the base dir of a listDir() + if ( !stat && thisURL.path(+1).length() == url.path(+1).length() ) + continue; + + atom.m_str = thisURL.fileName(); + } else { + // This is a relative URL. + atom.m_str = href.text(); + } + + entry.append( atom ); + + QDomNodeList propstats = thisResponse.elementsByTagName( "propstat" ); + + davParsePropstats( propstats, entry ); + + if ( stat ) + { + // return an item + statEntry( entry ); + finished(); + return; + } + else + { + listEntry( entry, false ); + } + } + else + { + kdDebug(7113) << "Error: no URL contained in response to PROPFIND on " + << url.prettyURL() << endl; + } + } + + if ( stat || !hasResponse ) + { + error( ERR_DOES_NOT_EXIST, url.prettyURL() ); + } + else + { + listEntry( entry, true ); + finished(); + } +} + +void HTTPProtocol::davGeneric( const KURL& url, KIO::HTTP_METHOD method ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::davGeneric " << url.url() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + // check to make sure this host supports WebDAV + if ( !davHostOk() ) + return; + + // WebDAV method + m_request.method = method; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveContent( false ); +} + +int HTTPProtocol::codeFromResponse( const QString& response ) +{ + int firstSpace = response.find( ' ' ); + int secondSpace = response.find( ' ', firstSpace + 1 ); + return response.mid( firstSpace + 1, secondSpace - firstSpace - 1 ).toInt(); +} + +void HTTPProtocol::davParsePropstats( const QDomNodeList& propstats, UDSEntry& entry ) +{ + QString mimeType; + UDSAtom atom; + bool foundExecutable = false; + bool isDirectory = false; + uint lockCount = 0; + uint supportedLockCount = 0; + + for ( uint i = 0; i < propstats.count(); i++) + { + QDomElement propstat = propstats.item(i).toElement(); + + QDomElement status = propstat.namedItem( "status" ).toElement(); + if ( status.isNull() ) + { + // error, no status code in this propstat + kdDebug(7113) << "Error, no status code in this propstat" << endl; + return; + } + + int code = codeFromResponse( status.text() ); + + if ( code != 200 ) + { + kdDebug(7113) << "Warning: status code " << code << " (this may mean that some properties are unavailable" << endl; + continue; + } + + QDomElement prop = propstat.namedItem( "prop" ).toElement(); + if ( prop.isNull() ) + { + kdDebug(7113) << "Error: no prop segment in this propstat." << endl; + return; + } + + if ( hasMetaData( "davRequestResponse" ) ) + { + atom.m_uds = KIO::UDS_XML_PROPERTIES; + QDomDocument doc; + doc.appendChild(prop); + atom.m_str = doc.toString(); + entry.append( atom ); + } + + for ( QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling() ) + { + QDomElement property = n.toElement(); + if (property.isNull()) + continue; + + if ( property.namespaceURI() != "DAV:" ) + { + // break out - we're only interested in properties from the DAV namespace + continue; + } + + if ( property.tagName() == "creationdate" ) + { + // Resource creation date. Should be is ISO 8601 format. + atom.m_uds = KIO::UDS_CREATION_TIME; + atom.m_long = parseDateTime( property.text(), property.attribute("dt") ); + entry.append( atom ); + } + else if ( property.tagName() == "getcontentlength" ) + { + // Content length (file size) + atom.m_uds = KIO::UDS_SIZE; + atom.m_long = property.text().toULong(); + entry.append( atom ); + } + else if ( property.tagName() == "displayname" ) + { + // Name suitable for presentation to the user + setMetaData( "davDisplayName", property.text() ); + } + else if ( property.tagName() == "source" ) + { + // Source template location + QDomElement source = property.namedItem( "link" ).toElement() + .namedItem( "dst" ).toElement(); + if ( !source.isNull() ) + setMetaData( "davSource", source.text() ); + } + else if ( property.tagName() == "getcontentlanguage" ) + { + // equiv. to Content-Language header on a GET + setMetaData( "davContentLanguage", property.text() ); + } + else if ( property.tagName() == "getcontenttype" ) + { + // Content type (mime type) + // This may require adjustments for other server-side webdav implementations + // (tested with Apache + mod_dav 1.0.3) + if ( property.text() == "httpd/unix-directory" ) + { + isDirectory = true; + } + else + { + mimeType = property.text(); + } + } + else if ( property.tagName() == "executable" ) + { + // File executable status + if ( property.text() == "T" ) + foundExecutable = true; + + } + else if ( property.tagName() == "getlastmodified" ) + { + // Last modification date + atom.m_uds = KIO::UDS_MODIFICATION_TIME; + atom.m_long = parseDateTime( property.text(), property.attribute("dt") ); + entry.append( atom ); + + } + else if ( property.tagName() == "getetag" ) + { + // Entity tag + setMetaData( "davEntityTag", property.text() ); + } + else if ( property.tagName() == "supportedlock" ) + { + // Supported locking specifications + for ( QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling() ) + { + QDomElement lockEntry = n2.toElement(); + if ( lockEntry.tagName() == "lockentry" ) + { + QDomElement lockScope = lockEntry.namedItem( "lockscope" ).toElement(); + QDomElement lockType = lockEntry.namedItem( "locktype" ).toElement(); + if ( !lockScope.isNull() && !lockType.isNull() ) + { + // Lock type was properly specified + supportedLockCount++; + QString scope = lockScope.firstChild().toElement().tagName(); + QString type = lockType.firstChild().toElement().tagName(); + + setMetaData( QString("davSupportedLockScope%1").arg(supportedLockCount), scope ); + setMetaData( QString("davSupportedLockType%1").arg(supportedLockCount), type ); + } + } + } + } + else if ( property.tagName() == "lockdiscovery" ) + { + // Lists the available locks + davParseActiveLocks( property.elementsByTagName( "activelock" ), lockCount ); + } + else if ( property.tagName() == "resourcetype" ) + { + // Resource type. "Specifies the nature of the resource." + if ( !property.namedItem( "collection" ).toElement().isNull() ) + { + // This is a collection (directory) + isDirectory = true; + } + } + else + { + kdDebug(7113) << "Found unknown webdav property: " << property.tagName() << endl; + } + } + } + + setMetaData( "davLockCount", QString("%1").arg(lockCount) ); + setMetaData( "davSupportedLockCount", QString("%1").arg(supportedLockCount) ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = isDirectory ? S_IFDIR : S_IFREG; + entry.append( atom ); + + if ( foundExecutable || isDirectory ) + { + // File was executable, or is a directory. + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = 0700; + entry.append(atom); + } + else + { + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = 0600; + entry.append(atom); + } + + if ( !isDirectory && !mimeType.isEmpty() ) + { + atom.m_uds = KIO::UDS_MIME_TYPE; + atom.m_str = mimeType; + entry.append( atom ); + } +} + +void HTTPProtocol::davParseActiveLocks( const QDomNodeList& activeLocks, + uint& lockCount ) +{ + for ( uint i = 0; i < activeLocks.count(); i++ ) + { + QDomElement activeLock = activeLocks.item(i).toElement(); + + lockCount++; + // required + QDomElement lockScope = activeLock.namedItem( "lockscope" ).toElement(); + QDomElement lockType = activeLock.namedItem( "locktype" ).toElement(); + QDomElement lockDepth = activeLock.namedItem( "depth" ).toElement(); + // optional + QDomElement lockOwner = activeLock.namedItem( "owner" ).toElement(); + QDomElement lockTimeout = activeLock.namedItem( "timeout" ).toElement(); + QDomElement lockToken = activeLock.namedItem( "locktoken" ).toElement(); + + if ( !lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull() ) + { + // lock was properly specified + lockCount++; + QString scope = lockScope.firstChild().toElement().tagName(); + QString type = lockType.firstChild().toElement().tagName(); + QString depth = lockDepth.text(); + + setMetaData( QString("davLockScope%1").arg( lockCount ), scope ); + setMetaData( QString("davLockType%1").arg( lockCount ), type ); + setMetaData( QString("davLockDepth%1").arg( lockCount ), depth ); + + if ( !lockOwner.isNull() ) + setMetaData( QString("davLockOwner%1").arg( lockCount ), lockOwner.text() ); + + if ( !lockTimeout.isNull() ) + setMetaData( QString("davLockTimeout%1").arg( lockCount ), lockTimeout.text() ); + + if ( !lockToken.isNull() ) + { + QDomElement tokenVal = lockScope.namedItem( "href" ).toElement(); + if ( !tokenVal.isNull() ) + setMetaData( QString("davLockToken%1").arg( lockCount ), tokenVal.text() ); + } + } + } +} + +long HTTPProtocol::parseDateTime( const QString& input, const QString& type ) +{ + if ( type == "dateTime.tz" ) + { + return KRFCDate::parseDateISO8601( input ); + } + else if ( type == "dateTime.rfc1123" ) + { + return KRFCDate::parseDate( input ); + } + + // format not advertised... try to parse anyway + time_t time = KRFCDate::parseDate( input ); + if ( time != 0 ) + return time; + + return KRFCDate::parseDateISO8601( input ); +} + +QString HTTPProtocol::davProcessLocks() +{ + if ( hasMetaData( "davLockCount" ) ) + { + QString response("If:"); + int numLocks; + numLocks = metaData( "davLockCount" ).toInt(); + bool bracketsOpen = false; + for ( int i = 0; i < numLocks; i++ ) + { + if ( hasMetaData( QString("davLockToken%1").arg(i) ) ) + { + if ( hasMetaData( QString("davLockURL%1").arg(i) ) ) + { + if ( bracketsOpen ) + { + response += ")"; + bracketsOpen = false; + } + response += " <" + metaData( QString("davLockURL%1").arg(i) ) + ">"; + } + + if ( !bracketsOpen ) + { + response += " ("; + bracketsOpen = true; + } + else + { + response += " "; + } + + if ( hasMetaData( QString("davLockNot%1").arg(i) ) ) + response += "Not "; + + response += "<" + metaData( QString("davLockToken%1").arg(i) ) + ">"; + } + } + + if ( bracketsOpen ) + response += ")"; + + response += "\r\n"; + return response; + } + + return QString::null; +} + +bool HTTPProtocol::davHostOk() +{ + // FIXME needs to be reworked. Switched off for now. + return true; + + // cached? + if ( m_davHostOk ) + { + kdDebug(7113) << "(" << m_pid << ") " << k_funcinfo << " true" << endl; + return true; + } + else if ( m_davHostUnsupported ) + { + kdDebug(7113) << "(" << m_pid << ") " << k_funcinfo << " false" << endl; + davError( -2 ); + return false; + } + + m_request.method = HTTP_OPTIONS; + + // query the server's capabilities generally, not for a specific URL + m_request.path = "*"; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + // clear davVersions variable, which holds the response to the DAV: header + m_davCapabilities.clear(); + + retrieveHeader(false); + + if (m_davCapabilities.count()) + { + for (uint i = 0; i < m_davCapabilities.count(); i++) + { + bool ok; + uint verNo = m_davCapabilities[i].toUInt(&ok); + if (ok && verNo > 0 && verNo < 3) + { + m_davHostOk = true; + kdDebug(7113) << "Server supports DAV version " << verNo << "." << endl; + } + } + + if ( m_davHostOk ) + return true; + } + + m_davHostUnsupported = true; + davError( -2 ); + return false; +} + +// This function is for closing retrieveHeader( false ); requests +// Required because there may or may not be further info expected +void HTTPProtocol::davFinished() +{ + // TODO: Check with the DAV extension developers + httpClose(m_bKeepAlive); + finished(); +} + +void HTTPProtocol::mkdir( const KURL& url, int ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::mkdir " << url.url() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = DAV_MKCOL; + m_request.path = url.path(); + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + + if ( m_responseCode == 201 ) + davFinished(); + else + davError(); +} + +void HTTPProtocol::get( const KURL& url ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::get " << url.url() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = HTTP_GET; + m_request.path = url.path(); + m_request.query = url.query(); + + QString tmp = metaData("cache"); + if (!tmp.isEmpty()) + m_request.cache = parseCacheControl(tmp); + else + m_request.cache = DEFAULT_CACHE_CONTROL; + + m_request.passwd = url.pass(); + m_request.user = url.user(); + m_request.doProxy = m_bUseProxy; + + retrieveContent(); +} + +void HTTPProtocol::put( const KURL &url, int, bool overwrite, bool) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::put " << url.prettyURL() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + // Webdav hosts are capable of observing overwrite == false + if (!overwrite && m_protocol.left(6) == "webdav") { + // check to make sure this host supports WebDAV + if ( !davHostOk() ) + return; + + QCString request; + request = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + "<D:propfind xmlns:D=\"DAV:\"><D:prop>" + "<D:creationdate/>" + "<D:getcontentlength/>" + "<D:displayname/>" + "<D:resourcetype/>" + "</D:prop></D:propfind>"; + + davSetRequest( request ); + + // WebDAV Stat or List... + m_request.method = DAV_PROPFIND; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + m_request.davData.depth = 0; + + retrieveContent(true); + + if (m_responseCode == 207) { + error(ERR_FILE_ALREADY_EXIST, QString::null); + return; + } + + m_bError = false; + } + + m_request.method = HTTP_PUT; + m_request.path = url.path(); + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::put error = " << m_bError << endl; + if (m_bError) + return; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::put responseCode = " << m_responseCode << endl; + + httpClose(false); // Always close connection. + + if ( (m_responseCode >= 200) && (m_responseCode < 300) ) + finished(); + else + httpError(); +} + +void HTTPProtocol::copy( const KURL& src, const KURL& dest, int, bool overwrite ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::copy " << src.prettyURL() + << " -> " << dest.prettyURL() << endl; + + if ( !checkRequestURL( dest ) || !checkRequestURL( src ) ) + return; + + // destination has to be "http(s)://..." + KURL newDest = dest; + if (newDest.protocol() == "webdavs") + newDest.setProtocol("https"); + else + newDest.setProtocol("http"); + + m_request.method = DAV_COPY; + m_request.path = src.path(); + m_request.davData.desturl = newDest.url(); + m_request.davData.overwrite = overwrite; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + + // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion + if ( m_responseCode == 201 || m_responseCode == 204 ) + davFinished(); + else + davError(); +} + +void HTTPProtocol::rename( const KURL& src, const KURL& dest, bool overwrite ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::rename " << src.prettyURL() + << " -> " << dest.prettyURL() << endl; + + if ( !checkRequestURL( dest ) || !checkRequestURL( src ) ) + return; + + // destination has to be "http://..." + KURL newDest = dest; + if (newDest.protocol() == "webdavs") + newDest.setProtocol("https"); + else + newDest.setProtocol("http"); + + m_request.method = DAV_MOVE; + m_request.path = src.path(); + m_request.davData.desturl = newDest.url(); + m_request.davData.overwrite = overwrite; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + + if ( m_responseCode == 301 ) + { + // Work around strict Apache-2 WebDAV implementation which refuses to cooperate + // with webdav://host/directory, instead requiring webdav://host/directory/ + // (strangely enough it accepts Destination: without a trailing slash) + + if (m_redirectLocation.protocol() == "https") + m_redirectLocation.setProtocol("webdavs"); + else + m_redirectLocation.setProtocol("webdav"); + + if ( !checkRequestURL( m_redirectLocation ) ) + return; + + m_request.method = DAV_MOVE; + m_request.path = m_redirectLocation.path(); + m_request.davData.desturl = newDest.url(); + m_request.davData.overwrite = overwrite; + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + } + + if ( m_responseCode == 201 ) + davFinished(); + else + davError(); +} + +void HTTPProtocol::del( const KURL& url, bool ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::del " << url.prettyURL() + << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = HTTP_DELETE; + m_request.path = url.path(); + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveHeader( false ); + + // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content + // on successful completion + if ( m_responseCode == 200 || m_responseCode == 204 ) + davFinished(); + else + davError(); +} + +void HTTPProtocol::post( const KURL& url ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::post " + << url.prettyURL() << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = HTTP_POST; + m_request.path = url.path(); + m_request.query = url.query(); + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveContent(); +} + +void HTTPProtocol::davLock( const KURL& url, const QString& scope, + const QString& type, const QString& owner ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::davLock " + << url.prettyURL() << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = DAV_LOCK; + m_request.path = url.path(); + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + /* Create appropriate lock XML request. */ + QDomDocument lockReq; + + QDomElement lockInfo = lockReq.createElementNS( "DAV:", "lockinfo" ); + lockReq.appendChild( lockInfo ); + + QDomElement lockScope = lockReq.createElement( "lockscope" ); + lockInfo.appendChild( lockScope ); + + lockScope.appendChild( lockReq.createElement( scope ) ); + + QDomElement lockType = lockReq.createElement( "locktype" ); + lockInfo.appendChild( lockType ); + + lockType.appendChild( lockReq.createElement( type ) ); + + if ( !owner.isNull() ) { + QDomElement ownerElement = lockReq.createElement( "owner" ); + lockReq.appendChild( ownerElement ); + + QDomElement ownerHref = lockReq.createElement( "href" ); + ownerElement.appendChild( ownerHref ); + + ownerHref.appendChild( lockReq.createTextNode( owner ) ); + } + + // insert the document into the POST buffer + m_bufPOST = lockReq.toCString(); + + retrieveContent( true ); + + if ( m_responseCode == 200 ) { + // success + QDomDocument multiResponse; + multiResponse.setContent( m_bufWebDavData, true ); + + QDomElement prop = multiResponse.documentElement().namedItem( "prop" ).toElement(); + + QDomElement lockdiscovery = prop.namedItem( "lockdiscovery" ).toElement(); + + uint lockCount = 0; + davParseActiveLocks( lockdiscovery.elementsByTagName( "activelock" ), lockCount ); + + setMetaData( "davLockCount", QString("%1").arg( lockCount ) ); + + finished(); + + } else + davError(); +} + +void HTTPProtocol::davUnlock( const KURL& url ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::davUnlock " + << url.prettyURL() << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = DAV_UNLOCK; + m_request.path = url.path(); + m_request.query = QString::null; + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + retrieveContent( true ); + + if ( m_responseCode == 200 ) + finished(); + else + davError(); +} + +QString HTTPProtocol::davError( int code /* = -1 */, QString url ) +{ + bool callError = false; + if ( code == -1 ) { + code = m_responseCode; + callError = true; + } + if ( code == -2 ) { + callError = true; + } + + if ( !url.isNull() ) + url = m_request.url.url(); + + QString action, errorString; + KIO::Error kError; + + // for 412 Precondition Failed + QString ow = i18n( "Otherwise, the request would have succeeded." ); + + switch ( m_request.method ) { + case DAV_PROPFIND: + action = i18n( "retrieve property values" ); + break; + case DAV_PROPPATCH: + action = i18n( "set property values" ); + break; + case DAV_MKCOL: + action = i18n( "create the requested folder" ); + break; + case DAV_COPY: + action = i18n( "copy the specified file or folder" ); + break; + case DAV_MOVE: + action = i18n( "move the specified file or folder" ); + break; + case DAV_SEARCH: + action = i18n( "search in the specified folder" ); + break; + case DAV_LOCK: + action = i18n( "lock the specified file or folder" ); + break; + case DAV_UNLOCK: + action = i18n( "unlock the specified file or folder" ); + break; + case HTTP_DELETE: + action = i18n( "delete the specified file or folder" ); + break; + case HTTP_OPTIONS: + action = i18n( "query the server's capabilities" ); + break; + case HTTP_GET: + action = i18n( "retrieve the contents of the specified file or folder" ); + break; + case HTTP_PUT: + case HTTP_POST: + case HTTP_HEAD: + default: + // this should not happen, this function is for webdav errors only + Q_ASSERT(0); + } + + // default error message if the following code fails + kError = ERR_INTERNAL; + errorString = i18n("An unexpected error (%1) occurred while attempting to %2.") + .arg( code ).arg( action ); + + switch ( code ) + { + case -2: + // internal error: OPTIONS request did not specify DAV compliance + kError = ERR_UNSUPPORTED_PROTOCOL; + errorString = i18n("The server does not support the WebDAV protocol."); + break; + case 207: + // 207 Multi-status + { + // our error info is in the returned XML document. + // retrieve the XML document + + // there was an error retrieving the XML document. + // ironic, eh? + if ( !readBody( true ) && m_bError ) + return QString::null; + + QStringList errors; + QDomDocument multiResponse; + + multiResponse.setContent( m_bufWebDavData, true ); + + QDomElement multistatus = multiResponse.documentElement().namedItem( "multistatus" ).toElement(); + + QDomNodeList responses = multistatus.elementsByTagName( "response" ); + + for (uint i = 0; i < responses.count(); i++) + { + int errCode; + QString errUrl; + + QDomElement response = responses.item(i).toElement(); + QDomElement code = response.namedItem( "status" ).toElement(); + + if ( !code.isNull() ) + { + errCode = codeFromResponse( code.text() ); + QDomElement href = response.namedItem( "href" ).toElement(); + if ( !href.isNull() ) + errUrl = href.text(); + errors << davError( errCode, errUrl ); + } + } + + //kError = ERR_SLAVE_DEFINED; + errorString = i18n("An error occurred while attempting to %1, %2. A " + "summary of the reasons is below.<ul>").arg( action ).arg( url ); + + for ( QStringList::Iterator it = errors.begin(); it != errors.end(); ++it ) + errorString += "<li>" + *it + "</li>"; + + errorString += "</ul>"; + } + case 403: + case 500: // hack: Apache mod_dav returns this instead of 403 (!) + // 403 Forbidden + kError = ERR_ACCESS_DENIED; + errorString = i18n("Access was denied while attempting to %1.").arg( action ); + break; + case 405: + // 405 Method Not Allowed + if ( m_request.method == DAV_MKCOL ) + { + kError = ERR_DIR_ALREADY_EXIST; + errorString = i18n("The specified folder already exists."); + } + break; + case 409: + // 409 Conflict + kError = ERR_ACCESS_DENIED; + errorString = i18n("A resource cannot be created at the destination " + "until one or more intermediate collections (folders) " + "have been created."); + break; + case 412: + // 412 Precondition failed + if ( m_request.method == DAV_COPY || m_request.method == DAV_MOVE ) + { + kError = ERR_ACCESS_DENIED; + errorString = i18n("The server was unable to maintain the liveness of " + "the properties listed in the propertybehavior XML " + "element or you attempted to overwrite a file while " + "requesting that files are not overwritten. %1") + .arg( ow ); + + } + else if ( m_request.method == DAV_LOCK ) + { + kError = ERR_ACCESS_DENIED; + errorString = i18n("The requested lock could not be granted. %1").arg( ow ); + } + break; + case 415: + // 415 Unsupported Media Type + kError = ERR_ACCESS_DENIED; + errorString = i18n("The server does not support the request type of the body."); + break; + case 423: + // 423 Locked + kError = ERR_ACCESS_DENIED; + errorString = i18n("Unable to %1 because the resource is locked.").arg( action ); + break; + case 425: + // 424 Failed Dependency + errorString = i18n("This action was prevented by another error."); + break; + case 502: + // 502 Bad Gateway + if ( m_request.method == DAV_COPY || m_request.method == DAV_MOVE ) + { + kError = ERR_WRITE_ACCESS_DENIED; + errorString = i18n("Unable to %1 because the destination server refuses " + "to accept the file or folder.").arg( action ); + } + break; + case 507: + // 507 Insufficient Storage + kError = ERR_DISK_FULL; + errorString = i18n("The destination resource does not have sufficient space " + "to record the state of the resource after the execution " + "of this method."); + break; + } + + // if ( kError != ERR_SLAVE_DEFINED ) + //errorString += " (" + url + ")"; + + if ( callError ) + error( ERR_SLAVE_DEFINED, errorString ); + + return errorString; +} + +void HTTPProtocol::httpError() +{ + QString action, errorString; + KIO::Error kError; + + switch ( m_request.method ) { + case HTTP_PUT: + action = i18n( "upload %1" ).arg(m_request.url.prettyURL()); + break; + default: + // this should not happen, this function is for http errors only + Q_ASSERT(0); + } + + // default error message if the following code fails + kError = ERR_INTERNAL; + errorString = i18n("An unexpected error (%1) occurred while attempting to %2.") + .arg( m_responseCode ).arg( action ); + + switch ( m_responseCode ) + { + case 403: + case 405: + case 500: // hack: Apache mod_dav returns this instead of 403 (!) + // 403 Forbidden + // 405 Method Not Allowed + kError = ERR_ACCESS_DENIED; + errorString = i18n("Access was denied while attempting to %1.").arg( action ); + break; + case 409: + // 409 Conflict + kError = ERR_ACCESS_DENIED; + errorString = i18n("A resource cannot be created at the destination " + "until one or more intermediate collections (folders) " + "have been created."); + break; + case 423: + // 423 Locked + kError = ERR_ACCESS_DENIED; + errorString = i18n("Unable to %1 because the resource is locked.").arg( action ); + break; + case 502: + // 502 Bad Gateway + kError = ERR_WRITE_ACCESS_DENIED; + errorString = i18n("Unable to %1 because the destination server refuses " + "to accept the file or folder.").arg( action ); + break; + case 507: + // 507 Insufficient Storage + kError = ERR_DISK_FULL; + errorString = i18n("The destination resource does not have sufficient space " + "to record the state of the resource after the execution " + "of this method."); + break; + } + + // if ( kError != ERR_SLAVE_DEFINED ) + //errorString += " (" + url + ")"; + + error( ERR_SLAVE_DEFINED, errorString ); +} + +bool HTTPProtocol::isOffline(const KURL &url) +{ + const int NetWorkStatusUnknown = 1; + const int NetWorkStatusOnline = 8; + QCString replyType; + QByteArray params; + QByteArray reply; + + QDataStream stream(params, IO_WriteOnly); + stream << url.url(); + + if ( dcopClient()->call( "kded", "networkstatus", "status(QString)", + params, replyType, reply ) && (replyType == "int") ) + { + int result; + QDataStream stream2( reply, IO_ReadOnly ); + stream2 >> result; + kdDebug(7113) << "(" << m_pid << ") networkstatus status = " << result << endl; + return (result != NetWorkStatusUnknown) && (result != NetWorkStatusOnline); + } + kdDebug(7113) << "(" << m_pid << ") networkstatus <unreachable>" << endl; + return false; // On error, assume we are online +} + +void HTTPProtocol::multiGet(const QByteArray &data) +{ + QDataStream stream(data, IO_ReadOnly); + Q_UINT32 n; + stream >> n; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtcool::multiGet n = " << n << endl; + + HTTPRequest saveRequest; + if (m_bBusy) + saveRequest = m_request; + +// m_requestQueue.clear(); + for(unsigned i = 0; i < n; i++) + { + KURL url; + stream >> url >> mIncomingMetaData; + + if ( !checkRequestURL( url ) ) + continue; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::multi_get " << url.url() << endl; + + m_request.method = HTTP_GET; + m_request.path = url.path(); + m_request.query = url.query(); + QString tmp = metaData("cache"); + if (!tmp.isEmpty()) + m_request.cache = parseCacheControl(tmp); + else + m_request.cache = DEFAULT_CACHE_CONTROL; + + m_request.passwd = url.pass(); + m_request.user = url.user(); + m_request.doProxy = m_bUseProxy; + + HTTPRequest *newRequest = new HTTPRequest(m_request); + m_requestQueue.append(newRequest); + } + + if (m_bBusy) + m_request = saveRequest; + + if (!m_bBusy) + { + m_bBusy = true; + while(!m_requestQueue.isEmpty()) + { + HTTPRequest *request = m_requestQueue.take(0); + m_request = *request; + delete request; + retrieveContent(); + } + m_bBusy = false; + } +} + +ssize_t HTTPProtocol::write (const void *_buf, size_t nbytes) +{ + int bytes_sent = 0; + const char* buf = static_cast<const char*>(_buf); + while ( nbytes > 0 ) + { + int n = TCPSlaveBase::write(buf, nbytes); + + if ( n <= 0 ) + { + // remote side closed connection ? + if ( n == 0 ) + break; + // a valid exception(s) occurred, let's retry... + if (n < 0 && ((errno == EINTR) || (errno == EAGAIN))) + continue; + // some other error occurred ? + return -1; + } + + nbytes -= n; + buf += n; + bytes_sent += n; + } + + return bytes_sent; +} + +void HTTPProtocol::setRewindMarker() +{ + m_rewindCount = 0; +} + +void HTTPProtocol::rewind() +{ + m_linePtrUnget = m_rewindBuf, + m_lineCountUnget = m_rewindCount; + m_rewindCount = 0; +} + + +char *HTTPProtocol::gets (char *s, int size) +{ + int len=0; + char *buf=s; + char mybuf[2]={0,0}; + + while (len < size) + { + read(mybuf, 1); + if (m_bEOF) + break; + + if (m_rewindCount < sizeof(m_rewindBuf)) + m_rewindBuf[m_rewindCount++] = *mybuf; + + if (*mybuf == '\r') // Ignore! + continue; + + if ((*mybuf == '\n') || !*mybuf) + break; + + *buf++ = *mybuf; + len++; + } + + *buf=0; + return s; +} + +ssize_t HTTPProtocol::read (void *b, size_t nbytes) +{ + ssize_t ret = 0; + + if (m_lineCountUnget > 0) + { + ret = ( nbytes < m_lineCountUnget ? nbytes : m_lineCountUnget ); + m_lineCountUnget -= ret; + memcpy(b, m_linePtrUnget, ret); + m_linePtrUnget += ret; + + return ret; + } + + if (m_lineCount > 0) + { + ret = ( nbytes < m_lineCount ? nbytes : m_lineCount ); + m_lineCount -= ret; + memcpy(b, m_linePtr, ret); + m_linePtr += ret; + return ret; + } + + if (nbytes == 1) + { + ret = read(m_lineBuf, 1024); // Read into buffer + m_linePtr = m_lineBuf; + if (ret <= 0) + { + m_lineCount = 0; + return ret; + } + m_lineCount = ret; + return read(b, 1); // Read from buffer + } + + do + { + ret = TCPSlaveBase::read( b, nbytes); + if (ret == 0) + m_bEOF = true; + + } while ((ret == -1) && (errno == EAGAIN || errno == EINTR)); + + return ret; +} + +void HTTPProtocol::httpCheckConnection() +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpCheckConnection: " << + " Socket status: " << m_iSock << + " Keep Alive: " << m_bKeepAlive << + " First: " << m_bFirstRequest << endl; + + if ( !m_bFirstRequest && (m_iSock != -1) ) + { + bool closeDown = false; + if ( !isConnectionValid()) + { + kdDebug(7113) << "(" << m_pid << ") Connection lost!" << endl; + closeDown = true; + } + else if ( m_request.method != HTTP_GET ) + { + closeDown = true; + } + else if ( !m_state.doProxy && !m_request.doProxy ) + { + if (m_state.hostname != m_request.hostname || + m_state.port != m_request.port || + m_state.user != m_request.user || + m_state.passwd != m_request.passwd) + closeDown = true; + } + else + { + // Keep the connection to the proxy. + if ( !(m_request.doProxy && m_state.doProxy) ) + closeDown = true; + } + + if (closeDown) + httpCloseConnection(); + } + + // Let's update our current state + m_state.hostname = m_request.hostname; + m_state.encoded_hostname = m_request.encoded_hostname; + m_state.port = m_request.port; + m_state.user = m_request.user; + m_state.passwd = m_request.passwd; + m_state.doProxy = m_request.doProxy; +} + +bool HTTPProtocol::httpOpenConnection() +{ + int errCode; + QString errMsg; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpOpenConnection" << endl; + + setBlockConnection( true ); + // kio_http uses its own proxying: + KSocks::self()->disableSocks(); + + if ( m_state.doProxy ) + { + QString proxy_host = m_proxyURL.host(); + int proxy_port = m_proxyURL.port(); + + kdDebug(7113) << "(" << m_pid << ") Connecting to proxy server: " + << proxy_host << ", port: " << proxy_port << endl; + + infoMessage( i18n("Connecting to %1...").arg(m_state.hostname) ); + + setConnectTimeout( m_proxyConnTimeout ); + + if ( !connectToHost(proxy_host, proxy_port, false) ) + { + if (userAborted()) { + error(ERR_NO_CONTENT, ""); + return false; + } + + switch ( connectResult() ) + { + case IO_LookupError: + errMsg = proxy_host; + errCode = ERR_UNKNOWN_PROXY_HOST; + break; + case IO_TimeOutError: + errMsg = i18n("Proxy %1 at port %2").arg(proxy_host).arg(proxy_port); + errCode = ERR_SERVER_TIMEOUT; + break; + default: + errMsg = i18n("Proxy %1 at port %2").arg(proxy_host).arg(proxy_port); + errCode = ERR_COULD_NOT_CONNECT; + } + error( errCode, errMsg ); + return false; + } + } + else + { + // Apparently we don't want a proxy. let's just connect directly + setConnectTimeout(m_remoteConnTimeout); + + if ( !connectToHost(m_state.hostname, m_state.port, false ) ) + { + if (userAborted()) { + error(ERR_NO_CONTENT, ""); + return false; + } + + switch ( connectResult() ) + { + case IO_LookupError: + errMsg = m_state.hostname; + errCode = ERR_UNKNOWN_HOST; + break; + case IO_TimeOutError: + errMsg = i18n("Connection was to %1 at port %2").arg(m_state.hostname).arg(m_state.port); + errCode = ERR_SERVER_TIMEOUT; + break; + default: + errCode = ERR_COULD_NOT_CONNECT; + if (m_state.port != m_iDefaultPort) + errMsg = i18n("%1 (port %2)").arg(m_state.hostname).arg(m_state.port); + else + errMsg = m_state.hostname; + } + error( errCode, errMsg ); + return false; + } + } + + // Set our special socket option!! + int on = 1; + (void) setsockopt( m_iSock, IPPROTO_TCP, TCP_NODELAY, (char*)&on, sizeof(on) ); + + m_bFirstRequest = true; + + connected(); + return true; +} + + +/** + * This function is responsible for opening up the connection to the remote + * HTTP server and sending the header. If this requires special + * authentication or other such fun stuff, then it will handle it. This + * function will NOT receive anything from the server, however. This is in + * contrast to previous incarnations of 'httpOpen'. + * + * The reason for the change is due to one small fact: some requests require + * data to be sent in addition to the header (POST requests) and there is no + * way for this function to get that data. This function is called in the + * slotPut() or slotGet() functions which, in turn, are called (indirectly) as + * a result of a KIOJob::put() or KIOJob::get(). It is those latter functions + * which are responsible for starting up this ioslave in the first place. + * This means that 'httpOpen' is called (essentially) as soon as the ioslave + * is created -- BEFORE any data gets to this slave. + * + * The basic process now is this: + * + * 1) Open up the socket and port + * 2) Format our request/header + * 3) Send the header to the remote server + */ +bool HTTPProtocol::httpOpen() +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpOpen" << endl; + + // Cannot have an https request without the m_bIsSSL being set! This can + // only happen if TCPSlaveBase::InitializeSSL() function failed in which it + // means the current installation does not support SSL... + if ( (m_protocol == "https" || m_protocol == "webdavs") && !m_bIsSSL ) + { + error( ERR_UNSUPPORTED_PROTOCOL, m_protocol ); + return false; + } + + m_request.fcache = 0; + m_request.bCachedRead = false; + m_request.bCachedWrite = false; + m_request.bMustRevalidate = false; + m_request.expireDate = 0; + m_request.creationDate = 0; + + if (m_request.bUseCache) + { + m_request.fcache = checkCacheEntry( ); + + bool bCacheOnly = (m_request.cache == KIO::CC_CacheOnly); + bool bOffline = isOffline(m_request.doProxy ? m_proxyURL : m_request.url); + if (bOffline && (m_request.cache != KIO::CC_Reload)) + m_request.cache = KIO::CC_CacheOnly; + + if (m_request.cache == CC_Reload && m_request.fcache) + { + if (m_request.fcache) + fclose(m_request.fcache); + m_request.fcache = 0; + } + if ((m_request.cache == KIO::CC_CacheOnly) || (m_request.cache == KIO::CC_Cache)) + m_request.bMustRevalidate = false; + + m_request.bCachedWrite = true; + + if (m_request.fcache && !m_request.bMustRevalidate) + { + // Cache entry is OK. + m_request.bCachedRead = true; // Cache hit. + return true; + } + else if (!m_request.fcache) + { + m_request.bMustRevalidate = false; // Cache miss + } + else + { + // Conditional cache hit. (Validate) + } + + if (bCacheOnly) + { + error( ERR_DOES_NOT_EXIST, m_request.url.url() ); + return false; + } + if (bOffline) + { + error( ERR_COULD_NOT_CONNECT, m_request.url.url() ); + return false; + } + } + + QString header; + QString davHeader; + + bool moreData = false; + bool davData = false; + + // Clear out per-connection settings... + resetConnectionSettings (); + + // Check the validity of the current connection, if one exists. + httpCheckConnection(); + + if ( !m_bIsTunneled && m_bNeedTunnel ) + { + setEnableSSLTunnel( true ); + // We send a HTTP 1.0 header since some proxies refuse HTTP 1.1 and we don't + // need any HTTP 1.1 capabilities for CONNECT - Waba + header = QString("CONNECT %1:%2 HTTP/1.0" + "\r\n").arg( m_request.encoded_hostname).arg(m_request.port); + + // Identify who you are to the proxy server! + if (!m_request.userAgent.isEmpty()) + header += "User-Agent: " + m_request.userAgent + "\r\n"; + + /* Add hostname information */ + header += "Host: " + m_state.encoded_hostname; + + if (m_state.port != m_iDefaultPort) + header += QString(":%1").arg(m_state.port); + header += "\r\n"; + + header += proxyAuthenticationHeader(); + } + else + { + // Determine if this is a POST or GET method + switch (m_request.method) + { + case HTTP_GET: + header = "GET "; + break; + case HTTP_PUT: + header = "PUT "; + moreData = true; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case HTTP_POST: + header = "POST "; + moreData = true; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case HTTP_HEAD: + header = "HEAD "; + break; + case HTTP_DELETE: + header = "DELETE "; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case HTTP_OPTIONS: + header = "OPTIONS "; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_PROPFIND: + header = "PROPFIND "; + davData = true; + davHeader = "Depth: "; + if ( hasMetaData( "davDepth" ) ) + { + kdDebug(7113) << "Reading DAV depth from metadata: " << metaData( "davDepth" ) << endl; + davHeader += metaData( "davDepth" ); + } + else + { + if ( m_request.davData.depth == 2 ) + davHeader += "infinity"; + else + davHeader += QString("%1").arg( m_request.davData.depth ); + } + davHeader += "\r\n"; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_PROPPATCH: + header = "PROPPATCH "; + davData = true; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_MKCOL: + header = "MKCOL "; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_COPY: + case DAV_MOVE: + header = ( m_request.method == DAV_COPY ) ? "COPY " : "MOVE "; + davHeader = "Destination: " + m_request.davData.desturl; + // infinity depth means copy recursively + // (optional for copy -> but is the desired action) + davHeader += "\r\nDepth: infinity\r\nOverwrite: "; + davHeader += m_request.davData.overwrite ? "T" : "F"; + davHeader += "\r\n"; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_LOCK: + header = "LOCK "; + davHeader = "Timeout: "; + { + uint timeout = 0; + if ( hasMetaData( "davTimeout" ) ) + timeout = metaData( "davTimeout" ).toUInt(); + if ( timeout == 0 ) + davHeader += "Infinite"; + else + davHeader += QString("Seconds-%1").arg(timeout); + } + davHeader += "\r\n"; + m_request.bCachedWrite = false; // Do not put any result in the cache + davData = true; + break; + case DAV_UNLOCK: + header = "UNLOCK "; + davHeader = "Lock-token: " + metaData("davLockToken") + "\r\n"; + m_request.bCachedWrite = false; // Do not put any result in the cache + break; + case DAV_SEARCH: + header = "SEARCH "; + davData = true; + m_request.bCachedWrite = false; + break; + case DAV_SUBSCRIBE: + header = "SUBSCRIBE "; + m_request.bCachedWrite = false; + break; + case DAV_UNSUBSCRIBE: + header = "UNSUBSCRIBE "; + m_request.bCachedWrite = false; + break; + case DAV_POLL: + header = "POLL "; + m_request.bCachedWrite = false; + break; + default: + error (ERR_UNSUPPORTED_ACTION, QString::null); + return false; + } + // DAV_POLL; DAV_NOTIFY + + // format the URI + if (m_state.doProxy && !m_bIsTunneled) + { + KURL u; + + if (m_protocol == "webdav") + u.setProtocol( "http" ); + else if (m_protocol == "webdavs" ) + u.setProtocol( "https" ); + else + u.setProtocol( m_protocol ); + + // For all protocols other than the once handled by this io-slave + // append the username. This fixes a long standing bug of ftp io-slave + // logging in anonymously in proxied connections even when the username + // is explicitly specified. + if (m_protocol != "http" && m_protocol != "https" && + !m_state.user.isEmpty()) + u.setUser (m_state.user); + + u.setHost( m_state.hostname ); + if (m_state.port != m_iDefaultPort) + u.setPort( m_state.port ); + u.setEncodedPathAndQuery( m_request.url.encodedPathAndQuery(0,true) ); + header += u.url(); + } + else + { + header += m_request.url.encodedPathAndQuery(0, true); + } + + header += " HTTP/1.1\r\n"; /* start header */ + + if (!m_request.userAgent.isEmpty()) + { + header += "User-Agent: "; + header += m_request.userAgent; + header += "\r\n"; + } + + if (!m_request.referrer.isEmpty()) + { + header += "Referer: "; //Don't try to correct spelling! + header += m_request.referrer; + header += "\r\n"; + } + + if ( m_request.offset > 0 ) + { + header += QString("Range: bytes=%1-\r\n").arg(KIO::number(m_request.offset)); + kdDebug(7103) << "kio_http : Range = " << KIO::number(m_request.offset) << endl; + } + + if ( m_request.cache == CC_Reload ) + { + /* No caching for reload */ + header += "Pragma: no-cache\r\n"; /* for HTTP/1.0 caches */ + header += "Cache-control: no-cache\r\n"; /* for HTTP >=1.1 caches */ + } + + if (m_request.bMustRevalidate) + { + /* conditional get */ + if (!m_request.etag.isEmpty()) + header += "If-None-Match: "+m_request.etag+"\r\n"; + if (!m_request.lastModified.isEmpty()) + header += "If-Modified-Since: "+m_request.lastModified+"\r\n"; + } + + header += "Accept: "; + QString acceptHeader = metaData("accept"); + if (!acceptHeader.isEmpty()) + header += acceptHeader; + else + header += DEFAULT_ACCEPT_HEADER; + header += "\r\n"; + +#ifdef DO_GZIP + if (m_request.allowCompressedPage) + header += "Accept-Encoding: x-gzip, x-deflate, gzip, deflate\r\n"; +#endif + + if (!m_request.charsets.isEmpty()) + header += "Accept-Charset: " + m_request.charsets + "\r\n"; + + if (!m_request.languages.isEmpty()) + header += "Accept-Language: " + m_request.languages + "\r\n"; + + + /* support for virtual hosts and required by HTTP 1.1 */ + header += "Host: " + m_state.encoded_hostname; + + if (m_state.port != m_iDefaultPort) + header += QString(":%1").arg(m_state.port); + header += "\r\n"; + + QString cookieStr; + QString cookieMode = metaData("cookies").lower(); + if (cookieMode == "none") + { + m_request.cookieMode = HTTPRequest::CookiesNone; + } + else if (cookieMode == "manual") + { + m_request.cookieMode = HTTPRequest::CookiesManual; + cookieStr = metaData("setcookies"); + } + else + { + m_request.cookieMode = HTTPRequest::CookiesAuto; + if (m_request.bUseCookiejar) + cookieStr = findCookies( m_request.url.url()); + } + + if (!cookieStr.isEmpty()) + header += cookieStr + "\r\n"; + + QString customHeader = metaData( "customHTTPHeader" ); + if (!customHeader.isEmpty()) + { + header += sanitizeCustomHTTPHeader(customHeader); + header += "\r\n"; + } + + if (m_request.method == HTTP_POST) + { + header += metaData("content-type"); + header += "\r\n"; + } + + // Only check for a cached copy if the previous + // response was NOT a 401 or 407. + // no caching for Negotiate auth. + if ( !m_request.bNoAuth && m_responseCode != 401 && m_responseCode != 407 && Authentication != AUTH_Negotiate ) + { + kdDebug(7113) << "(" << m_pid << ") Calling checkCachedAuthentication " << endl; + AuthInfo info; + info.url = m_request.url; + info.verifyPath = true; + if ( !m_request.user.isEmpty() ) + info.username = m_request.user; + if ( checkCachedAuthentication( info ) && !info.digestInfo.isEmpty() ) + { + Authentication = info.digestInfo.startsWith("Basic") ? AUTH_Basic : info.digestInfo.startsWith("NTLM") ? AUTH_NTLM : info.digestInfo.startsWith("Negotiate") ? AUTH_Negotiate : AUTH_Digest ; + m_state.user = info.username; + m_state.passwd = info.password; + m_strRealm = info.realmValue; + if ( Authentication != AUTH_NTLM && Authentication != AUTH_Negotiate ) // don't use the cached challenge + m_strAuthorization = info.digestInfo; + } + } + else + { + kdDebug(7113) << "(" << m_pid << ") Not calling checkCachedAuthentication " << endl; + } + + switch ( Authentication ) + { + case AUTH_Basic: + header += createBasicAuth(); + break; + case AUTH_Digest: + header += createDigestAuth(); + break; +#ifdef HAVE_LIBGSSAPI + case AUTH_Negotiate: + header += createNegotiateAuth(); + break; +#endif + case AUTH_NTLM: + header += createNTLMAuth(); + break; + case AUTH_None: + default: + break; + } + + /********* Only for debugging purpose *********/ + if ( Authentication != AUTH_None ) + { + kdDebug(7113) << "(" << m_pid << ") Using Authentication: " << endl; + kdDebug(7113) << "(" << m_pid << ") HOST= " << m_state.hostname << endl; + kdDebug(7113) << "(" << m_pid << ") PORT= " << m_state.port << endl; + kdDebug(7113) << "(" << m_pid << ") USER= " << m_state.user << endl; + kdDebug(7113) << "(" << m_pid << ") PASSWORD= [protected]" << endl; + kdDebug(7113) << "(" << m_pid << ") REALM= " << m_strRealm << endl; + kdDebug(7113) << "(" << m_pid << ") EXTRA= " << m_strAuthorization << endl; + } + + // Do we need to authorize to the proxy server ? + if ( m_state.doProxy && !m_bIsTunneled ) + header += proxyAuthenticationHeader(); + + // Support old HTTP/1.0 style keep-alive header for compatability + // purposes as well as performance improvements while giving end + // users the ability to disable this feature proxy servers that + // don't not support such feature, e.g. junkbuster proxy server. + if (!m_bUseProxy || m_bPersistentProxyConnection || m_bIsTunneled) + header += "Connection: Keep-Alive\r\n"; + else + header += "Connection: close\r\n"; + + if ( m_protocol == "webdav" || m_protocol == "webdavs" ) + { + header += davProcessLocks(); + + // add extra webdav headers, if supplied + QString davExtraHeader = metaData("davHeader"); + if ( !davExtraHeader.isEmpty() ) + davHeader += davExtraHeader; + + // Set content type of webdav data + if (davData) + davHeader += "Content-Type: text/xml; charset=utf-8\r\n"; + + // add extra header elements for WebDAV + if ( !davHeader.isNull() ) + header += davHeader; + } + } + + kdDebug(7103) << "(" << m_pid << ") ============ Sending Header:" << endl; + + QStringList headerOutput = QStringList::split("\r\n", header); + QStringList::Iterator it = headerOutput.begin(); + + for (; it != headerOutput.end(); it++) + kdDebug(7103) << "(" << m_pid << ") " << (*it) << endl; + + if ( !moreData && !davData) + header += "\r\n"; /* end header */ + + // Now that we have our formatted header, let's send it! + // Create a new connection to the remote machine if we do + // not already have one... + if ( m_iSock == -1) + { + if (!httpOpenConnection()) + return false; + } + + // Send the data to the remote machine... + bool sendOk = (write(header.latin1(), header.length()) == (ssize_t) header.length()); + if (!sendOk) + { + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpOpen: " + "Connection broken! (" << m_state.hostname << ")" << endl; + + // With a Keep-Alive connection this can happen. + // Just reestablish the connection. + if (m_bKeepAlive) + { + httpCloseConnection(); + return true; // Try again + } + + if (!sendOk) + { + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpOpen: sendOk==false." + " Connnection broken !" << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + } + + bool res = true; + + if ( moreData || davData ) + res = sendBody(); + + infoMessage(i18n("%1 contacted. Waiting for reply...").arg(m_request.hostname)); + + return res; +} + +void HTTPProtocol::forwardHttpResponseHeader() +{ + // Send the response header if it was requested + if ( config()->readBoolEntry("PropagateHttpHeader", false) ) + { + setMetaData("HTTP-Headers", m_responseHeader.join("\n")); + sendMetaData(); + } + m_responseHeader.clear(); +} + +/** + * This function will read in the return header from the server. It will + * not read in the body of the return message. It will also not transmit + * the header to our client as the client doesn't need to know the gory + * details of HTTP headers. + */ +bool HTTPProtocol::readHeader() +{ +try_again: + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readHeader" << endl; + + // Check + if (m_request.bCachedRead) + { + m_responseHeader << "HTTP-CACHE"; + // Read header from cache... + char buffer[4097]; + if (!fgets(buffer, 4096, m_request.fcache) ) + { + // Error, delete cache entry + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readHeader: " + << "Could not access cache to obtain mimetype!" << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + + m_strMimeType = QString::fromUtf8( buffer).stripWhiteSpace(); + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readHeader: cached " + << "data mimetype: " << m_strMimeType << endl; + + if (!fgets(buffer, 4096, m_request.fcache) ) + { + // Error, delete cache entry + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readHeader: " + << "Could not access cached data! " << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + + m_request.strCharset = QString::fromUtf8( buffer).stripWhiteSpace().lower(); + setMetaData("charset", m_request.strCharset); + if (!m_request.lastModified.isEmpty()) + setMetaData("modified", m_request.lastModified); + QString tmp; + tmp.setNum(m_request.expireDate); + setMetaData("expire-date", tmp); + tmp.setNum(m_request.creationDate); + setMetaData("cache-creation-date", tmp); + mimeType(m_strMimeType); + forwardHttpResponseHeader(); + return true; + } + + QCString locationStr; // In case we get a redirect. + QCString cookieStr; // In case we get a cookie. + + QString dispositionType; // In case we get a Content-Disposition type + QString dispositionFilename; // In case we get a Content-Disposition filename + + QString mediaValue; + QString mediaAttribute; + + QStringList upgradeOffers; + + bool upgradeRequired = false; // Server demands that we upgrade to something + // This is also true if we ask to upgrade and + // the server accepts, since we are now + // committed to doing so + bool canUpgrade = false; // The server offered an upgrade + + + m_request.etag = QString::null; + m_request.lastModified = QString::null; + m_request.strCharset = QString::null; + + time_t dateHeader = 0; + time_t expireDate = 0; // 0 = no info, 1 = already expired, > 1 = actual date + int currentAge = 0; + int maxAge = -1; // -1 = no max age, 0 already expired, > 0 = actual time + int maxHeaderSize = 64*1024; // 64Kb to catch DOS-attacks + + // read in 8192 bytes at a time (HTTP cookies can be quite large.) + int len = 0; + char buffer[8193]; + bool cont = false; + bool cacheValidated = false; // Revalidation was successful + bool mayCache = true; + bool hasCacheDirective = false; + bool bCanResume = false; + + if (m_iSock == -1) + { + kdDebug(7113) << "HTTPProtocol::readHeader: No connection." << endl; + return false; // Restablish connection and try again + } + + if (!waitForResponse(m_remoteRespTimeout)) + { + // No response error + error( ERR_SERVER_TIMEOUT , m_state.hostname ); + return false; + } + + setRewindMarker(); + + gets(buffer, sizeof(buffer)-1); + + if (m_bEOF || *buffer == '\0') + { + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readHeader: " + << "EOF while waiting for header start." << endl; + if (m_bKeepAlive) // Try to reestablish connection. + { + httpCloseConnection(); + return false; // Reestablish connection and try again. + } + + if (m_request.method == HTTP_HEAD) + { + // HACK + // Some web-servers fail to respond properly to a HEAD request. + // We compensate for their failure to properly implement the HTTP standard + // by assuming that they will be sending html. + kdDebug(7113) << "(" << m_pid << ") HTTPPreadHeader: HEAD -> returned " + << "mimetype: " << DEFAULT_MIME_TYPE << endl; + mimeType(QString::fromLatin1(DEFAULT_MIME_TYPE)); + return true; + } + + kdDebug(7113) << "HTTPProtocol::readHeader: Connection broken !" << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + + kdDebug(7103) << "(" << m_pid << ") ============ Received Response:"<< endl; + + bool noHeader = true; + HTTP_REV httpRev = HTTP_None; + int headerSize = 0; + + do + { + // strip off \r and \n if we have them + len = strlen(buffer); + + while(len && (buffer[len-1] == '\n' || buffer[len-1] == '\r')) + buffer[--len] = 0; + + // if there was only a newline then continue + if (!len) + { + kdDebug(7103) << "(" << m_pid << ") --empty--" << endl; + continue; + } + + headerSize += len; + + // We have a response header. This flag is a work around for + // servers that append a "\r\n" before the beginning of the HEADER + // response!!! It only catches x number of \r\n being placed at the + // top of the reponse... + noHeader = false; + + kdDebug(7103) << "(" << m_pid << ") \"" << buffer << "\"" << endl; + + // Save broken servers from damnation!! + char* buf = buffer; + while( *buf == ' ' ) + buf++; + + + if (buf[0] == '<') + { + // We get XML / HTTP without a proper header + // put string back + kdDebug(7103) << "kio_http: No valid HTTP header found! Document starts with XML/HTML tag" << endl; + + // Document starts with a tag, assume html instead of text/plain + m_strMimeType = "text/html"; + + rewind(); + break; + } + + // Store the the headers so they can be passed to the + // calling application later + m_responseHeader << QString::fromLatin1(buf); + + if ((strncasecmp(buf, "HTTP", 4) == 0) || + (strncasecmp(buf, "ICY ", 4) == 0)) // Shoutcast support + { + if (strncasecmp(buf, "ICY ", 4) == 0) + { + // Shoutcast support + httpRev = SHOUTCAST; + m_bKeepAlive = false; + } + else if (strncmp((buf + 5), "1.0",3) == 0) + { + httpRev = HTTP_10; + // For 1.0 servers, the server itself has to explicitly + // tell us whether it supports persistent connection or + // not. By default, we assume it does not, but we do + // send the old style header "Connection: Keep-Alive" to + // inform it that we support persistence. + m_bKeepAlive = false; + } + else if (strncmp((buf + 5), "1.1",3) == 0) + { + httpRev = HTTP_11; + } + else + { + httpRev = HTTP_Unknown; + } + + if (m_responseCode) + m_prevResponseCode = m_responseCode; + + const char* rptr = buf; + while ( *rptr && *rptr > ' ' ) + ++rptr; + m_responseCode = atoi(rptr); + + // server side errors + if (m_responseCode >= 500 && m_responseCode <= 599) + { + if (m_request.method == HTTP_HEAD) + { + ; // Ignore error + } + else + { + if (m_request.bErrorPage) + errorPage(); + else + { + error(ERR_INTERNAL_SERVER, m_request.url.url()); + return false; + } + } + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + // Unauthorized access + else if (m_responseCode == 401 || m_responseCode == 407) + { + // Double authorization requests, i.e. a proxy auth + // request followed immediately by a regular auth request. + if ( m_prevResponseCode != m_responseCode && + (m_prevResponseCode == 401 || m_prevResponseCode == 407) ) + saveAuthorization(); + + m_bUnauthorized = true; + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + // + else if (m_responseCode == 416) // Range not supported + { + m_request.offset = 0; + httpCloseConnection(); + return false; // Try again. + } + // Upgrade Required + else if (m_responseCode == 426) + { + upgradeRequired = true; + } + // Any other client errors + else if (m_responseCode >= 400 && m_responseCode <= 499) + { + // Tell that we will only get an error page here. + if (m_request.bErrorPage) + errorPage(); + else + { + error(ERR_DOES_NOT_EXIST, m_request.url.url()); + return false; + } + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + else if (m_responseCode == 307) + { + // 307 Temporary Redirect + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + else if (m_responseCode == 304) + { + // 304 Not Modified + // The value in our cache is still valid. + cacheValidated = true; + } + else if (m_responseCode >= 301 && m_responseCode<= 303) + { + // 301 Moved permanently + if (m_responseCode == 301) + setMetaData("permanent-redirect", "true"); + + // 302 Found (temporary location) + // 303 See Other + if (m_request.method != HTTP_HEAD && m_request.method != HTTP_GET) + { +#if 0 + // Reset the POST buffer to avoid a double submit + // on redirection + if (m_request.method == HTTP_POST) + m_bufPOST.resize(0); +#endif + + // NOTE: This is wrong according to RFC 2616. However, + // because most other existing user agent implementations + // treat a 301/302 response as a 303 response and preform + // a GET action regardless of what the previous method was, + // many servers have simply adapted to this way of doing + // things!! Thus, we are forced to do the same thing or we + // won't be able to retrieve these pages correctly!! See RFC + // 2616 sections 10.3.[2/3/4/8] + m_request.method = HTTP_GET; // Force a GET + } + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + else if ( m_responseCode == 207 ) // Multi-status (for WebDav) + { + + } + else if ( m_responseCode == 204 ) // No content + { + // error(ERR_NO_CONTENT, i18n("Data have been successfully sent.")); + // Short circuit and do nothing! + + // The original handling here was wrong, this is not an error: eg. in the + // example of a 204 No Content response to a PUT completing. + // m_bError = true; + // return false; + } + else if ( m_responseCode == 206 ) + { + if ( m_request.offset ) + bCanResume = true; + } + else if (m_responseCode == 102) // Processing (for WebDAV) + { + /*** + * This status code is given when the server expects the + * command to take significant time to complete. So, inform + * the user. + */ + infoMessage( i18n( "Server processing request, please wait..." ) ); + cont = true; + } + else if (m_responseCode == 100) + { + // We got 'Continue' - ignore it + cont = true; + } + } + + // are we allowd to resume? this will tell us + else if (strncasecmp(buf, "Accept-Ranges:", 14) == 0) { + if (strncasecmp(trimLead(buf + 14), "none", 4) == 0) + bCanResume = false; + } + // Keep Alive + else if (strncasecmp(buf, "Keep-Alive:", 11) == 0) { + QStringList options = QStringList::split(',', + QString::fromLatin1(trimLead(buf+11))); + for(QStringList::ConstIterator it = options.begin(); + it != options.end(); + it++) + { + QString option = (*it).stripWhiteSpace().lower(); + if (option.startsWith("timeout=")) + { + m_keepAliveTimeout = option.mid(8).toInt(); + } + } + } + + // Cache control + else if (strncasecmp(buf, "Cache-Control:", 14) == 0) { + QStringList cacheControls = QStringList::split(',', + QString::fromLatin1(trimLead(buf+14))); + for(QStringList::ConstIterator it = cacheControls.begin(); + it != cacheControls.end(); + it++) + { + QString cacheControl = (*it).stripWhiteSpace(); + if (strncasecmp(cacheControl.latin1(), "no-cache", 8) == 0) + { + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + else if (strncasecmp(cacheControl.latin1(), "no-store", 8) == 0) + { + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + } + else if (strncasecmp(cacheControl.latin1(), "max-age=", 8) == 0) + { + QString age = cacheControl.mid(8).stripWhiteSpace(); + if (!age.isNull()) + maxAge = STRTOLL(age.latin1(), 0, 10); + } + } + hasCacheDirective = true; + } + + // get the size of our data + else if (strncasecmp(buf, "Content-length:", 15) == 0) { + char* len = trimLead(buf + 15); + if (len) + m_iSize = STRTOLL(len, 0, 10); + } + + else if (strncasecmp(buf, "Content-location:", 17) == 0) { + setMetaData ("content-location", + QString::fromLatin1(trimLead(buf+17)).stripWhiteSpace()); + } + + // what type of data do we have? + else if (strncasecmp(buf, "Content-type:", 13) == 0) { + char *start = trimLead(buf + 13); + char *pos = start; + + // Increment until we encounter ";" or the end of the buffer + while ( *pos && *pos != ';' ) pos++; + + // Assign the mime-type. + m_strMimeType = QString::fromLatin1(start, pos-start).stripWhiteSpace().lower(); + kdDebug(7113) << "(" << m_pid << ") Content-type: " << m_strMimeType << endl; + + // If we still have text, then it means we have a mime-type with a + // parameter (eg: charset=iso-8851) ; so let's get that... + while (*pos) + { + start = ++pos; + while ( *pos && *pos != '=' ) pos++; + + char *end = pos; + while ( *end && *end != ';' ) end++; + + if (*pos) + { + mediaAttribute = QString::fromLatin1(start, pos-start).stripWhiteSpace().lower(); + mediaValue = QString::fromLatin1(pos+1, end-pos-1).stripWhiteSpace(); + pos = end; + if (mediaValue.length() && + (mediaValue[0] == '"') && + (mediaValue[mediaValue.length()-1] == '"')) + mediaValue = mediaValue.mid(1, mediaValue.length()-2); + + kdDebug (7113) << "(" << m_pid << ") Media-Parameter Attribute: " + << mediaAttribute << endl; + kdDebug (7113) << "(" << m_pid << ") Media-Parameter Value: " + << mediaValue << endl; + + if ( mediaAttribute == "charset") + { + mediaValue = mediaValue.lower(); + m_request.strCharset = mediaValue; + } + else + { + setMetaData("media-"+mediaAttribute, mediaValue); + } + } + } + } + + // Date + else if (strncasecmp(buf, "Date:", 5) == 0) { + dateHeader = KRFCDate::parseDate(trimLead(buf+5)); + } + + // Cache management + else if (strncasecmp(buf, "ETag:", 5) == 0) { + m_request.etag = trimLead(buf+5); + } + + // Cache management + else if (strncasecmp(buf, "Expires:", 8) == 0) { + expireDate = KRFCDate::parseDate(trimLead(buf+8)); + if (!expireDate) + expireDate = 1; // Already expired + } + + // Cache management + else if (strncasecmp(buf, "Last-Modified:", 14) == 0) { + m_request.lastModified = (QString::fromLatin1(trimLead(buf+14))).stripWhiteSpace(); + } + + // whoops.. we received a warning + else if (strncasecmp(buf, "Warning:", 8) == 0) { + //Don't use warning() here, no need to bother the user. + //Those warnings are mostly about caches. + infoMessage(trimLead(buf + 8)); + } + + // Cache management (HTTP 1.0) + else if (strncasecmp(buf, "Pragma:", 7) == 0) { + QCString pragma = QCString(trimLead(buf+7)).stripWhiteSpace().lower(); + if (pragma == "no-cache") + { + m_request.bCachedWrite = false; // Don't put in cache + mayCache = false; + hasCacheDirective = true; + } + } + + // The deprecated Refresh Response + else if (strncasecmp(buf,"Refresh:", 8) == 0) { + mayCache = false; // Do not cache page as it defeats purpose of Refresh tag! + setMetaData( "http-refresh", QString::fromLatin1(trimLead(buf+8)).stripWhiteSpace() ); + } + + // In fact we should do redirection only if we got redirection code + else if (strncasecmp(buf, "Location:", 9) == 0) { + // Redirect only for 3xx status code, will ya! Thanks, pal! + if ( m_responseCode > 299 && m_responseCode < 400 ) + locationStr = QCString(trimLead(buf+9)).stripWhiteSpace(); + } + + // Check for cookies + else if (strncasecmp(buf, "Set-Cookie", 10) == 0) { + cookieStr += buf; + cookieStr += '\n'; + } + + // check for direct authentication + else if (strncasecmp(buf, "WWW-Authenticate:", 17) == 0) { + configAuth(trimLead(buf + 17), false); + } + + // check for proxy-based authentication + else if (strncasecmp(buf, "Proxy-Authenticate:", 19) == 0) { + configAuth(trimLead(buf + 19), true); + } + + else if (strncasecmp(buf, "Upgrade:", 8) == 0) { + // Now we have to check to see what is offered for the upgrade + QString offered = &(buf[8]); + upgradeOffers = QStringList::split(QRegExp("[ \n,\r\t]"), offered); + } + + // content? + else if (strncasecmp(buf, "Content-Encoding:", 17) == 0) { + // This is so wrong !! No wonder kio_http is stripping the + // gzip encoding from downloaded files. This solves multiple + // bug reports and caitoo's problem with downloads when such a + // header is encountered... + + // A quote from RFC 2616: + // " When present, its (Content-Encoding) value indicates what additional + // content have been applied to the entity body, and thus what decoding + // mechanism must be applied to obtain the media-type referenced by the + // Content-Type header field. Content-Encoding is primarily used to allow + // a document to be compressed without loosing the identity of its underlying + // media type. Simply put if it is specified, this is the actual mime-type + // we should use when we pull the resource !!! + addEncoding(trimLead(buf + 17), m_qContentEncodings); + } + // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183 + else if(strncasecmp(buf, "Content-Disposition:", 20) == 0) { + char* dispositionBuf = trimLead(buf + 20); + while ( *dispositionBuf ) + { + if ( strncasecmp( dispositionBuf, "filename", 8 ) == 0 ) + { + dispositionBuf += 8; + + while ( *dispositionBuf == ' ' || *dispositionBuf == '=' ) + dispositionBuf++; + + char* bufStart = dispositionBuf; + + while ( *dispositionBuf && *dispositionBuf != ';' ) + dispositionBuf++; + + if ( dispositionBuf > bufStart ) + { + // Skip any leading quotes... + while ( *bufStart == '"' ) + bufStart++; + + // Skip any trailing quotes as well as white spaces... + while ( *(dispositionBuf-1) == ' ' || *(dispositionBuf-1) == '"') + dispositionBuf--; + + if ( dispositionBuf > bufStart ) + dispositionFilename = QString::fromLatin1( bufStart, dispositionBuf-bufStart ); + + break; + } + } + else + { + char *bufStart = dispositionBuf; + + while ( *dispositionBuf && *dispositionBuf != ';' ) + dispositionBuf++; + + if ( dispositionBuf > bufStart ) + dispositionType = QString::fromLatin1( bufStart, dispositionBuf-bufStart ).stripWhiteSpace(); + + while ( *dispositionBuf == ';' || *dispositionBuf == ' ' ) + dispositionBuf++; + } + } + + // Content-Dispostion is not allowed to dictate directory + // path, thus we extract the filename only. + if ( !dispositionFilename.isEmpty() ) + { + int pos = dispositionFilename.findRev( '/' ); + + if( pos > -1 ) + dispositionFilename = dispositionFilename.mid(pos+1); + + kdDebug(7113) << "(" << m_pid << ") Content-Disposition: filename=" + << dispositionFilename<< endl; + } + } + else if(strncasecmp(buf, "Content-Language:", 17) == 0) { + QString language = QString::fromLatin1(trimLead(buf+17)).stripWhiteSpace(); + if (!language.isEmpty()) + setMetaData("content-language", language); + } + else if (strncasecmp(buf, "Proxy-Connection:", 17) == 0) + { + if (strncasecmp(trimLead(buf + 17), "Close", 5) == 0) + m_bKeepAlive = false; + else if (strncasecmp(trimLead(buf + 17), "Keep-Alive", 10)==0) + m_bKeepAlive = true; + } + else if (strncasecmp(buf, "Link:", 5) == 0) { + // We only support Link: <url>; rel="type" so far + QStringList link = QStringList::split(";", QString(buf) + .replace(QRegExp("^Link:[ ]*"), + "")); + if (link.count() == 2) { + QString rel = link[1].stripWhiteSpace(); + if (rel.startsWith("rel=\"")) { + rel = rel.mid(5, rel.length() - 6); + if (rel.lower() == "pageservices") { + QString url = link[0].replace(QRegExp("[<>]"),"").stripWhiteSpace(); + setMetaData("PageServices", url); + } + } + } + } + else if (strncasecmp(buf, "P3P:", 4) == 0) { + QString p3pstr = buf; + p3pstr = p3pstr.mid(4).simplifyWhiteSpace(); + QStringList policyrefs, compact; + QStringList policyfields = QStringList::split(QRegExp(",[ ]*"), p3pstr); + for (QStringList::Iterator it = policyfields.begin(); + it != policyfields.end(); + ++it) { + QStringList policy = QStringList::split("=", *it); + + if (policy.count() == 2) { + if (policy[0].lower() == "policyref") { + policyrefs << policy[1].replace(QRegExp("[\"\']"), "") + .stripWhiteSpace(); + } else if (policy[0].lower() == "cp") { + // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with + // other metadata sent in strings. This could be a bit more + // efficient but I'm going for correctness right now. + QStringList cps = QStringList::split(" ", + policy[1].replace(QRegExp("[\"\']"), "") + .simplifyWhiteSpace()); + + for (QStringList::Iterator j = cps.begin(); j != cps.end(); ++j) + compact << *j; + } + } + } + + if (!policyrefs.isEmpty()) + setMetaData("PrivacyPolicy", policyrefs.join("\n")); + + if (!compact.isEmpty()) + setMetaData("PrivacyCompactPolicy", compact.join("\n")); + } + // let them tell us if we should stay alive or not + else if (strncasecmp(buf, "Connection:", 11) == 0) + { + if (strncasecmp(trimLead(buf + 11), "Close", 5) == 0) + m_bKeepAlive = false; + else if (strncasecmp(trimLead(buf + 11), "Keep-Alive", 10)==0) + m_bKeepAlive = true; + else if (strncasecmp(trimLead(buf + 11), "Upgrade", 7)==0) + { + if (m_responseCode == 101) { + // Ok, an upgrade was accepted, now we must do it + upgradeRequired = true; + } else if (upgradeRequired) { // 426 + // Nothing to do since we did it above already + } else { + // Just an offer to upgrade - no need to take it + canUpgrade = true; + } + } + } + // continue only if we know that we're HTTP/1.1 + else if ( httpRev == HTTP_11) { + // what kind of encoding do we have? transfer? + if (strncasecmp(buf, "Transfer-Encoding:", 18) == 0) { + // If multiple encodings have been applied to an entity, the + // transfer-codings MUST be listed in the order in which they + // were applied. + addEncoding(trimLead(buf + 18), m_qTransferEncodings); + } + + // md5 signature + else if (strncasecmp(buf, "Content-MD5:", 12) == 0) { + m_sContentMD5 = QString::fromLatin1(trimLead(buf + 12)); + } + + // *** Responses to the HTTP OPTIONS method follow + // WebDAV capabilities + else if (strncasecmp(buf, "DAV:", 4) == 0) { + if (m_davCapabilities.isEmpty()) { + m_davCapabilities << QString::fromLatin1(trimLead(buf + 4)); + } + else { + m_davCapabilities << QString::fromLatin1(trimLead(buf + 4)); + } + } + // *** Responses to the HTTP OPTIONS method finished + } + else if ((httpRev == HTTP_None) && (strlen(buf) != 0)) + { + // Remote server does not seem to speak HTTP at all + // Put the crap back into the buffer and hope for the best + rewind(); + if (m_responseCode) + m_prevResponseCode = m_responseCode; + + m_responseCode = 200; // Fake it + httpRev = HTTP_Unknown; + m_bKeepAlive = false; + break; + } + setRewindMarker(); + + // Clear out our buffer for further use. + memset(buffer, 0, sizeof(buffer)); + + } while (!m_bEOF && (len || noHeader) && (headerSize < maxHeaderSize) && (gets(buffer, sizeof(buffer)-1))); + + // Now process the HTTP/1.1 upgrade + QStringList::Iterator opt = upgradeOffers.begin(); + for( ; opt != upgradeOffers.end(); ++opt) { + if (*opt == "TLS/1.0") { + if(upgradeRequired) { + if (!startTLS() && !usingTLS()) { + error(ERR_UPGRADE_REQUIRED, *opt); + return false; + } + } + } else if (*opt == "HTTP/1.1") { + httpRev = HTTP_11; + } else { + // unknown + if (upgradeRequired) { + error(ERR_UPGRADE_REQUIRED, *opt); + return false; + } + } + } + + setMetaData("charset", m_request.strCharset); + + // If we do not support the requested authentication method... + if ( (m_responseCode == 401 && Authentication == AUTH_None) || + (m_responseCode == 407 && ProxyAuthentication == AUTH_None) ) + { + m_bUnauthorized = false; + if (m_request.bErrorPage) + errorPage(); + else + { + error( ERR_UNSUPPORTED_ACTION, "Unknown Authorization method!" ); + return false; + } + } + + // Fixup expire date for clock drift. + if (expireDate && (expireDate <= dateHeader)) + expireDate = 1; // Already expired. + + // Convert max-age into expireDate (overriding previous set expireDate) + if (maxAge == 0) + expireDate = 1; // Already expired. + else if (maxAge > 0) + { + if (currentAge) + maxAge -= currentAge; + if (maxAge <=0) + maxAge = 0; + expireDate = time(0) + maxAge; + } + + if (!expireDate) + { + time_t lastModifiedDate = 0; + if (!m_request.lastModified.isEmpty()) + lastModifiedDate = KRFCDate::parseDate(m_request.lastModified); + + if (lastModifiedDate) + { + long diff = static_cast<long>(difftime(dateHeader, lastModifiedDate)); + if (diff < 0) + expireDate = time(0) + 1; + else + expireDate = time(0) + (diff / 10); + } + else + { + expireDate = time(0) + DEFAULT_CACHE_EXPIRE; + } + } + + // DONE receiving the header! + if (!cookieStr.isEmpty()) + { + if ((m_request.cookieMode == HTTPRequest::CookiesAuto) && m_request.bUseCookiejar) + { + // Give cookies to the cookiejar. + QString domain = config()->readEntry("cross-domain"); + if (!domain.isEmpty() && isCrossDomainRequest(m_request.url.host(), domain)) + cookieStr = "Cross-Domain\n" + cookieStr; + addCookies( m_request.url.url(), cookieStr ); + } + else if (m_request.cookieMode == HTTPRequest::CookiesManual) + { + // Pass cookie to application + setMetaData("setcookies", cookieStr); + } + } + + if (m_request.bMustRevalidate) + { + m_request.bMustRevalidate = false; // Reset just in case. + if (cacheValidated) + { + // Yippie, we can use the cached version. + // Update the cache with new "Expire" headers. + fclose(m_request.fcache); + m_request.fcache = 0; + updateExpireDate( expireDate, true ); + m_request.fcache = checkCacheEntry( ); // Re-read cache entry + + if (m_request.fcache) + { + m_request.bCachedRead = true; + goto try_again; // Read header again, but now from cache. + } + else + { + // Where did our cache entry go??? + } + } + else + { + // Validation failed. Close cache. + fclose(m_request.fcache); + m_request.fcache = 0; + } + } + + // We need to reread the header if we got a '100 Continue' or '102 Processing' + if ( cont ) + { + goto try_again; + } + + // Do not do a keep-alive connection if the size of the + // response is not known and the response is not Chunked. + if (!m_bChunked && (m_iSize == NO_SIZE)) + m_bKeepAlive = false; + + if ( m_responseCode == 204 ) + { + return true; + } + + // We need to try to login again if we failed earlier + if ( m_bUnauthorized ) + { + if ( (m_responseCode == 401) || + (m_bUseProxy && (m_responseCode == 407)) + ) + { + if ( getAuthorization() ) + { + // for NTLM Authentication we have to keep the connection open! + if ( Authentication == AUTH_NTLM && m_strAuthorization.length() > 4 ) + { + m_bKeepAlive = true; + readBody( true ); + } + else if (ProxyAuthentication == AUTH_NTLM && m_strProxyAuthorization.length() > 4) + { + readBody( true ); + } + else + httpCloseConnection(); + return false; // Try again. + } + + if (m_bError) + return false; // Error out + + // Show error page... + } + m_bUnauthorized = false; + } + + // We need to do a redirect + if (!locationStr.isEmpty()) + { + KURL u(m_request.url, locationStr); + if(!u.isValid()) + { + error(ERR_MALFORMED_URL, u.url()); + return false; + } + if ((u.protocol() != "http") && (u.protocol() != "https") && + (u.protocol() != "ftp") && (u.protocol() != "webdav") && + (u.protocol() != "webdavs")) + { + redirection(u); + error(ERR_ACCESS_DENIED, u.url()); + return false; + } + + // preserve #ref: (bug 124654) + // if we were at http://host/resource1#ref, we sent a GET for "/resource1" + // if we got redirected to http://host/resource2, then we have to re-add + // the fragment: + if (m_request.url.hasRef() && !u.hasRef() && + (m_request.url.host() == u.host()) && + (m_request.url.protocol() == u.protocol())) + u.setRef(m_request.url.ref()); + + m_bRedirect = true; + m_redirectLocation = u; + + if (!m_request.id.isEmpty()) + { + sendMetaData(); + } + + kdDebug(7113) << "(" << m_pid << ") request.url: " << m_request.url.url() + << endl << "LocationStr: " << locationStr.data() << endl; + + kdDebug(7113) << "(" << m_pid << ") Requesting redirection to: " << u.url() + << endl; + + // If we're redirected to a http:// url, remember that we're doing webdav... + if (m_protocol == "webdav" || m_protocol == "webdavs") + u.setProtocol(m_protocol); + + redirection(u); + m_request.bCachedWrite = false; // Turn off caching on re-direction (DA) + mayCache = false; + } + + // Inform the job that we can indeed resume... + if ( bCanResume && m_request.offset ) + canResume(); + else + m_request.offset = 0; + + // We don't cache certain text objects + if (m_strMimeType.startsWith("text/") && + (m_strMimeType != "text/css") && + (m_strMimeType != "text/x-javascript") && + !hasCacheDirective) + { + // Do not cache secure pages or pages + // originating from password protected sites + // unless the webserver explicitly allows it. + if ( m_bIsSSL || (Authentication != AUTH_None) ) + { + m_request.bCachedWrite = false; + mayCache = false; + } + } + + // WABA: Correct for tgz files with a gzip-encoding. + // They really shouldn't put gzip in the Content-Encoding field! + // Web-servers really shouldn't do this: They let Content-Size refer + // to the size of the tgz file, not to the size of the tar file, + // while the Content-Type refers to "tar" instead of "tgz". + if (m_qContentEncodings.last() == "gzip") + { + if (m_strMimeType == "application/x-tar") + { + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + m_strMimeType = QString::fromLatin1("application/x-tgz"); + } + else if (m_strMimeType == "application/postscript") + { + // LEONB: Adding another exception for psgz files. + // Could we use the mimelnk files instead of hardcoding all this? + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + m_strMimeType = QString::fromLatin1("application/x-gzpostscript"); + } + else if ( m_request.allowCompressedPage && + m_strMimeType != "application/x-tgz" && + m_strMimeType != "application/x-targz" && + m_strMimeType != "application/x-gzip" && + m_request.url.path().right(6) == ".ps.gz" ) + { + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + m_strMimeType = QString::fromLatin1("application/x-gzpostscript"); + } + else if ( (m_request.allowCompressedPage && + m_strMimeType == "text/html") + || + (m_request.allowCompressedPage && + m_strMimeType != "application/x-tgz" && + m_strMimeType != "application/x-targz" && + m_strMimeType != "application/x-gzip" && + m_request.url.path().right(3) != ".gz") + ) + { + // Unzip! + } + else + { + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + m_strMimeType = QString::fromLatin1("application/x-gzip"); + } + } + + // We can't handle "bzip2" encoding (yet). So if we get something with + // bzip2 encoding, we change the mimetype to "application/x-bzip2". + // Note for future changes: some web-servers send both "bzip2" as + // encoding and "application/x-bzip2" as mimetype. That is wrong. + // currently that doesn't bother us, because we remove the encoding + // and set the mimetype to x-bzip2 anyway. + if (m_qContentEncodings.last() == "bzip2") + { + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + m_strMimeType = QString::fromLatin1("application/x-bzip2"); + } + + // Convert some common mimetypes to standard KDE mimetypes + if (m_strMimeType == "application/x-targz") + m_strMimeType = QString::fromLatin1("application/x-tgz"); + else if (m_strMimeType == "application/zip") + m_strMimeType = QString::fromLatin1("application/x-zip"); + else if (m_strMimeType == "image/x-png") + m_strMimeType = QString::fromLatin1("image/png"); + else if (m_strMimeType == "image/bmp") + m_strMimeType = QString::fromLatin1("image/x-bmp"); + else if (m_strMimeType == "audio/mpeg" || m_strMimeType == "audio/x-mpeg" || m_strMimeType == "audio/mp3") + m_strMimeType = QString::fromLatin1("audio/x-mp3"); + else if (m_strMimeType == "audio/microsoft-wave") + m_strMimeType = QString::fromLatin1("audio/x-wav"); + else if (m_strMimeType == "audio/midi") + m_strMimeType = QString::fromLatin1("audio/x-midi"); + else if (m_strMimeType == "image/x-xpixmap") + m_strMimeType = QString::fromLatin1("image/x-xpm"); + else if (m_strMimeType == "application/rtf") + m_strMimeType = QString::fromLatin1("text/rtf"); + + // Crypto ones.... + else if (m_strMimeType == "application/pkix-cert" || + m_strMimeType == "application/binary-certificate") + { + m_strMimeType = QString::fromLatin1("application/x-x509-ca-cert"); + } + + // Prefer application/x-tgz or x-gzpostscript over application/x-gzip. + else if (m_strMimeType == "application/x-gzip") + { + if ((m_request.url.path().right(7) == ".tar.gz") || + (m_request.url.path().right(4) == ".tar")) + m_strMimeType = QString::fromLatin1("application/x-tgz"); + if ((m_request.url.path().right(6) == ".ps.gz")) + m_strMimeType = QString::fromLatin1("application/x-gzpostscript"); + } + + // Some webservers say "text/plain" when they mean "application/x-bzip2" + else if ((m_strMimeType == "text/plain") || (m_strMimeType == "application/octet-stream")) + { + QString ext = m_request.url.path().right(4).upper(); + if (ext == ".BZ2") + m_strMimeType = QString::fromLatin1("application/x-bzip2"); + else if (ext == ".PEM") + m_strMimeType = QString::fromLatin1("application/x-x509-ca-cert"); + else if (ext == ".SWF") + m_strMimeType = QString::fromLatin1("application/x-shockwave-flash"); + else if (ext == ".PLS") + m_strMimeType = QString::fromLatin1("audio/x-scpls"); + else if (ext == ".WMV") + m_strMimeType = QString::fromLatin1("video/x-ms-wmv"); + } + +#if 0 + // Even if we can't rely on content-length, it seems that we should + // never get more data than content-length. Maybe less, if the + // content-length refers to the unzipped data. + if (!m_qContentEncodings.isEmpty()) + { + // If we still have content encoding we can't rely on the Content-Length. + m_iSize = NO_SIZE; + } +#endif + + if( !dispositionType.isEmpty() ) + { + kdDebug(7113) << "(" << m_pid << ") Setting Content-Disposition type to: " + << dispositionType << endl; + setMetaData("content-disposition-type", dispositionType); + } + if( !dispositionFilename.isEmpty() ) + { + kdDebug(7113) << "(" << m_pid << ") Setting Content-Disposition filename to: " + << dispositionFilename << endl; + // ### KDE4: setting content-disposition to filename for pre 3.5.2 compatability + setMetaData("content-disposition", dispositionFilename); + setMetaData("content-disposition-filename", dispositionFilename); + } + + if (!m_request.lastModified.isEmpty()) + setMetaData("modified", m_request.lastModified); + + if (!mayCache) + { + setMetaData("no-cache", "true"); + setMetaData("expire-date", "1"); // Expired + } + else + { + QString tmp; + tmp.setNum(expireDate); + setMetaData("expire-date", tmp); + tmp.setNum(time(0)); // Cache entry will be created shortly. + setMetaData("cache-creation-date", tmp); + } + + // Let the app know about the mime-type iff this is not + // a redirection and the mime-type string is not empty. + if (locationStr.isEmpty() && (!m_strMimeType.isEmpty() || + m_request.method == HTTP_HEAD)) + { + kdDebug(7113) << "(" << m_pid << ") Emitting mimetype " << m_strMimeType << endl; + mimeType( m_strMimeType ); + } + + // Do not move send response header before any redirection as it seems + // to screw up some sites. See BR# 150904. + forwardHttpResponseHeader(); + + if (m_request.method == HTTP_HEAD) + return true; + + // Do we want to cache this request? + if (m_request.bUseCache) + { + ::unlink( QFile::encodeName(m_request.cef)); + if ( m_request.bCachedWrite && !m_strMimeType.isEmpty() ) + { + // Check... + createCacheEntry(m_strMimeType, expireDate); // Create a cache entry + if (!m_request.fcache) + { + m_request.bCachedWrite = false; // Error creating cache entry. + kdDebug(7113) << "(" << m_pid << ") Error creating cache entry for " << m_request.url.url()<<"!\n"; + } + m_request.expireDate = expireDate; + m_maxCacheSize = config()->readNumEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE) / 2; + } + } + + if (m_request.bCachedWrite && !m_strMimeType.isEmpty()) + kdDebug(7113) << "(" << m_pid << ") Cache, adding \"" << m_request.url.url() << "\"" << endl; + else if (m_request.bCachedWrite && m_strMimeType.isEmpty()) + kdDebug(7113) << "(" << m_pid << ") Cache, pending \"" << m_request.url.url() << "\"" << endl; + else + kdDebug(7113) << "(" << m_pid << ") Cache, not adding \"" << m_request.url.url() << "\"" << endl; + return true; +} + + +void HTTPProtocol::addEncoding(QString encoding, QStringList &encs) +{ + encoding = encoding.stripWhiteSpace().lower(); + // Identity is the same as no encoding + if (encoding == "identity") { + return; + } else if (encoding == "8bit") { + // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de + return; + } else if (encoding == "chunked") { + m_bChunked = true; + // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints? + //if ( m_cmd != CMD_COPY ) + m_iSize = NO_SIZE; + } else if ((encoding == "x-gzip") || (encoding == "gzip")) { + encs.append(QString::fromLatin1("gzip")); + } else if ((encoding == "x-bzip2") || (encoding == "bzip2")) { + encs.append(QString::fromLatin1("bzip2")); // Not yet supported! + } else if ((encoding == "x-deflate") || (encoding == "deflate")) { + encs.append(QString::fromLatin1("deflate")); + } else { + kdDebug(7113) << "(" << m_pid << ") Unknown encoding encountered. " + << "Please write code. Encoding = \"" << encoding + << "\"" << endl; + } +} + +bool HTTPProtocol::sendBody() +{ + int result=-1; + int length=0; + + infoMessage( i18n( "Requesting data to send" ) ); + + // m_bufPOST will NOT be empty iff authentication was required before posting + // the data OR a re-connect is requested from ::readHeader because the + // connection was lost for some reason. + if ( !m_bufPOST.isNull() ) + { + kdDebug(7113) << "(" << m_pid << ") POST'ing saved data..." << endl; + + result = 0; + length = m_bufPOST.size(); + } + else + { + kdDebug(7113) << "(" << m_pid << ") POST'ing live data..." << endl; + + QByteArray buffer; + int old_size; + + m_bufPOST.resize(0); + do + { + dataReq(); // Request for data + result = readData( buffer ); + if ( result > 0 ) + { + length += result; + old_size = m_bufPOST.size(); + m_bufPOST.resize( old_size+result ); + memcpy( m_bufPOST.data()+ old_size, buffer.data(), buffer.size() ); + buffer.resize(0); + } + } while ( result > 0 ); + } + + if ( result < 0 ) + { + error( ERR_ABORTED, m_request.hostname ); + return false; + } + + infoMessage( i18n( "Sending data to %1" ).arg( m_request.hostname ) ); + + QString size = QString ("Content-Length: %1\r\n\r\n").arg(length); + kdDebug( 7113 ) << "(" << m_pid << ")" << size << endl; + + // Send the content length... + bool sendOk = (write(size.latin1(), size.length()) == (ssize_t) size.length()); + if (!sendOk) + { + kdDebug( 7113 ) << "(" << m_pid << ") Connection broken when sending " + << "content length: (" << m_state.hostname << ")" << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + + // Send the data... + // kdDebug( 7113 ) << "(" << m_pid << ") POST DATA: " << QCString(m_bufPOST) << endl; + sendOk = (write(m_bufPOST.data(), m_bufPOST.size()) == (ssize_t) m_bufPOST.size()); + if (!sendOk) + { + kdDebug(7113) << "(" << m_pid << ") Connection broken when sending message body: (" + << m_state.hostname << ")" << endl; + error( ERR_CONNECTION_BROKEN, m_state.hostname ); + return false; + } + + return true; +} + +void HTTPProtocol::httpClose( bool keepAlive ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpClose" << endl; + + if (m_request.fcache) + { + fclose(m_request.fcache); + m_request.fcache = 0; + if (m_request.bCachedWrite) + { + QString filename = m_request.cef + ".new"; + ::unlink( QFile::encodeName(filename) ); + } + } + + // Only allow persistent connections for GET requests. + // NOTE: we might even want to narrow this down to non-form + // based submit requests which will require a meta-data from + // khtml. + if (keepAlive && (!m_bUseProxy || + m_bPersistentProxyConnection || m_bIsTunneled)) + { + if (!m_keepAliveTimeout) + m_keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; + else if (m_keepAliveTimeout > 2*DEFAULT_KEEP_ALIVE_TIMEOUT) + m_keepAliveTimeout = 2*DEFAULT_KEEP_ALIVE_TIMEOUT; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpClose: keep alive (" << m_keepAliveTimeout << ")" << endl; + QByteArray data; + QDataStream stream( data, IO_WriteOnly ); + stream << int(99); // special: Close connection + setTimeoutSpecialCommand(m_keepAliveTimeout, data); + return; + } + + httpCloseConnection(); +} + +void HTTPProtocol::closeConnection() +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::closeConnection" << endl; + httpCloseConnection (); +} + +void HTTPProtocol::httpCloseConnection () +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::httpCloseConnection" << endl; + m_bIsTunneled = false; + m_bKeepAlive = false; + closeDescriptor(); + setTimeoutSpecialCommand(-1); // Cancel any connection timeout +} + +void HTTPProtocol::slave_status() +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::slave_status" << endl; + + if ( m_iSock != -1 && !isConnectionValid() ) + httpCloseConnection(); + + slaveStatus( m_state.hostname, (m_iSock != -1) ); +} + +void HTTPProtocol::mimetype( const KURL& url ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::mimetype: " + << url.prettyURL() << endl; + + if ( !checkRequestURL( url ) ) + return; + + m_request.method = HTTP_HEAD; + m_request.path = url.path(); + m_request.query = url.query(); + m_request.cache = CC_Cache; + m_request.doProxy = m_bUseProxy; + + retrieveHeader(); + + kdDebug(7113) << "(" << m_pid << ") http: mimetype = " << m_strMimeType + << endl; +} + +void HTTPProtocol::special( const QByteArray &data ) +{ + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::special" << endl; + + int tmp; + QDataStream stream(data, IO_ReadOnly); + + stream >> tmp; + switch (tmp) { + case 1: // HTTP POST + { + KURL url; + stream >> url; + post( url ); + break; + } + case 2: // cache_update + { + KURL url; + bool no_cache; + time_t expireDate; + stream >> url >> no_cache >> expireDate; + cacheUpdate( url, no_cache, expireDate ); + break; + } + case 5: // WebDAV lock + { + KURL url; + QString scope, type, owner; + stream >> url >> scope >> type >> owner; + davLock( url, scope, type, owner ); + break; + } + case 6: // WebDAV unlock + { + KURL url; + stream >> url; + davUnlock( url ); + break; + } + case 7: // Generic WebDAV + { + KURL url; + int method; + stream >> url >> method; + davGeneric( url, (KIO::HTTP_METHOD) method ); + break; + } + case 99: // Close Connection + { + httpCloseConnection(); + break; + } + default: + // Some command we don't understand. + // Just ignore it, it may come from some future version of KDE. + break; + } +} + +/** + * Read a chunk from the data stream. + */ +int HTTPProtocol::readChunked() +{ + if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) + { + setRewindMarker(); + + m_bufReceive.resize(4096); + + if (!gets(m_bufReceive.data(), m_bufReceive.size()-1)) + { + kdDebug(7113) << "(" << m_pid << ") gets() failure on Chunk header" << endl; + return -1; + } + // We could have got the CRLF of the previous chunk. + // If so, try again. + if (m_bufReceive[0] == '\0') + { + if (!gets(m_bufReceive.data(), m_bufReceive.size()-1)) + { + kdDebug(7113) << "(" << m_pid << ") gets() failure on Chunk header" << endl; + return -1; + } + } + + // m_bEOF is set to true when read called from gets returns 0. For chunked reading 0 + // means end of chunked transfer and not error. See RFC 2615 section 3.6.1 + #if 0 + if (m_bEOF) + { + kdDebug(7113) << "(" << m_pid << ") EOF on Chunk header" << endl; + return -1; + } + #endif + + long long trunkSize = STRTOLL(m_bufReceive.data(), 0, 16); + if (trunkSize < 0) + { + kdDebug(7113) << "(" << m_pid << ") Negative chunk size" << endl; + return -1; + } + m_iBytesLeft = trunkSize; + + // kdDebug(7113) << "(" << m_pid << ") Chunk size = " << m_iBytesLeft << " bytes" << endl; + + if (m_iBytesLeft == 0) + { + // Last chunk. + // Skip trailers. + do { + // Skip trailer of last chunk. + if (!gets(m_bufReceive.data(), m_bufReceive.size()-1)) + { + kdDebug(7113) << "(" << m_pid << ") gets() failure on Chunk trailer" << endl; + return -1; + } + // kdDebug(7113) << "(" << m_pid << ") Chunk trailer = \"" << m_bufReceive.data() << "\"" << endl; + } + while (strlen(m_bufReceive.data()) != 0); + + return 0; + } + } + + int bytesReceived = readLimited(); + if (!m_iBytesLeft) + m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk + + // kdDebug(7113) << "(" << m_pid << ") readChunked: BytesReceived=" << bytesReceived << endl; + return bytesReceived; +} + +int HTTPProtocol::readLimited() +{ + if (!m_iBytesLeft) + return 0; + + m_bufReceive.resize(4096); + + int bytesReceived; + int bytesToReceive; + + if (m_iBytesLeft > m_bufReceive.size()) + bytesToReceive = m_bufReceive.size(); + else + bytesToReceive = m_iBytesLeft; + + bytesReceived = read(m_bufReceive.data(), bytesToReceive); + + if (bytesReceived <= 0) + return -1; // Error: connection lost + + m_iBytesLeft -= bytesReceived; + return bytesReceived; +} + +int HTTPProtocol::readUnlimited() +{ + if (m_bKeepAlive) + { + kdDebug(7113) << "(" << m_pid << ") Unbounded datastream on a Keep " + << "alive connection!" << endl; + m_bKeepAlive = false; + } + + m_bufReceive.resize(4096); + + int result = read(m_bufReceive.data(), m_bufReceive.size()); + if (result > 0) + return result; + + m_bEOF = true; + m_iBytesLeft = 0; + return 0; +} + +void HTTPProtocol::slotData(const QByteArray &_d) +{ + if (!_d.size()) + { + m_bEOD = true; + return; + } + + if (m_iContentLeft != NO_SIZE) + { + if (m_iContentLeft >= _d.size()) + m_iContentLeft -= _d.size(); + else + m_iContentLeft = NO_SIZE; + } + + QByteArray d = _d; + if ( !m_dataInternal ) + { + // If a broken server does not send the mime-type, + // we try to id it from the content before dealing + // with the content itself. + if ( m_strMimeType.isEmpty() && !m_bRedirect && + !( m_responseCode >= 300 && m_responseCode <=399) ) + { + kdDebug(7113) << "(" << m_pid << ") Determining mime-type from content..." << endl; + int old_size = m_mimeTypeBuffer.size(); + m_mimeTypeBuffer.resize( old_size + d.size() ); + memcpy( m_mimeTypeBuffer.data() + old_size, d.data(), d.size() ); + if ( (m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) + && (m_mimeTypeBuffer.size() < 1024) ) + { + m_cpMimeBuffer = true; + return; // Do not send up the data since we do not yet know its mimetype! + } + + kdDebug(7113) << "(" << m_pid << ") Mimetype buffer size: " << m_mimeTypeBuffer.size() + << endl; + + KMimeMagicResult *result; + result = KMimeMagic::self()->findBufferFileType( m_mimeTypeBuffer, + m_request.url.fileName() ); + if( result ) + { + m_strMimeType = result->mimeType(); + kdDebug(7113) << "(" << m_pid << ") Mimetype from content: " + << m_strMimeType << endl; + } + + if ( m_strMimeType.isEmpty() ) + { + m_strMimeType = QString::fromLatin1( DEFAULT_MIME_TYPE ); + kdDebug(7113) << "(" << m_pid << ") Using default mimetype: " + << m_strMimeType << endl; + } + + if ( m_request.bCachedWrite ) + { + createCacheEntry( m_strMimeType, m_request.expireDate ); + if (!m_request.fcache) + m_request.bCachedWrite = false; + } + + if ( m_cpMimeBuffer ) + { + // Do not make any assumption about the state of the QByteArray we received. + // Fix the crash described by BR# 130104. + d.detach(); + d.resize(0); + d.resize(m_mimeTypeBuffer.size()); + memcpy( d.data(), m_mimeTypeBuffer.data(), + d.size() ); + } + mimeType(m_strMimeType); + m_mimeTypeBuffer.resize(0); + } + + data( d ); + if (m_request.bCachedWrite && m_request.fcache) + writeCacheEntry(d.data(), d.size()); + } + else + { + uint old_size = m_bufWebDavData.size(); + m_bufWebDavData.resize (old_size + d.size()); + memcpy (m_bufWebDavData.data() + old_size, d.data(), d.size()); + } +} + +/** + * This function is our "receive" function. It is responsible for + * downloading the message (not the header) from the HTTP server. It + * is called either as a response to a client's KIOJob::dataEnd() + * (meaning that the client is done sending data) or by 'httpOpen()' + * (if we are in the process of a PUT/POST request). It can also be + * called by a webDAV function, to receive stat/list/property/etc. + * data; in this case the data is stored in m_bufWebDavData. + */ +bool HTTPProtocol::readBody( bool dataInternal /* = false */ ) +{ + if (m_responseCode == 204) + return true; + + m_bEOD = false; + // Note that when dataInternal is true, we are going to: + // 1) save the body data to a member variable, m_bufWebDavData + // 2) _not_ advertise the data, speed, size, etc., through the + // corresponding functions. + // This is used for returning data to WebDAV. + m_dataInternal = dataInternal; + if ( dataInternal ) + m_bufWebDavData.resize (0); + + // Check if we need to decode the data. + // If we are in copy mode, then use only transfer decoding. + bool useMD5 = !m_sContentMD5.isEmpty(); + + // Deal with the size of the file. + KIO::filesize_t sz = m_request.offset; + if ( sz ) + m_iSize += sz; + + // Update the application with total size except when + // it is compressed, or when the data is to be handled + // internally (webDAV). If compressed we have to wait + // until we uncompress to find out the actual data size + if ( !dataInternal ) { + if ( (m_iSize > 0) && (m_iSize != NO_SIZE)) { + totalSize(m_iSize); + infoMessage( i18n( "Retrieving %1 from %2...").arg(KIO::convertSize(m_iSize)) + .arg( m_request.hostname ) ); + } + else + { + totalSize ( 0 ); + } + } + else + infoMessage( i18n( "Retrieving from %1..." ).arg( m_request.hostname ) ); + + if (m_request.bCachedRead) + { + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readBody: read data from cache!" << endl; + m_request.bCachedWrite = false; + + char buffer[ MAX_IPC_SIZE ]; + + m_iContentLeft = NO_SIZE; + + // Jippie! It's already in the cache :-) + while (!feof(m_request.fcache) && !ferror(m_request.fcache)) + { + int nbytes = fread( buffer, 1, MAX_IPC_SIZE, m_request.fcache); + + if (nbytes > 0) + { + m_bufReceive.setRawData( buffer, nbytes); + slotData( m_bufReceive ); + m_bufReceive.resetRawData( buffer, nbytes ); + sz += nbytes; + } + } + + m_bufReceive.resize( 0 ); + + if ( !dataInternal ) + { + processedSize( sz ); + data( QByteArray() ); + } + + return true; + } + + + if (m_iSize != NO_SIZE) + m_iBytesLeft = m_iSize - sz; + else + m_iBytesLeft = NO_SIZE; + + m_iContentLeft = m_iBytesLeft; + + if (m_bChunked) + m_iBytesLeft = NO_SIZE; + + kdDebug(7113) << "(" << m_pid << ") HTTPProtocol::readBody: retrieve data. " + << KIO::number(m_iBytesLeft) << " left." << endl; + + // Main incoming loop... Gather everything while we can... + m_cpMimeBuffer = false; + m_mimeTypeBuffer.resize(0); + struct timeval last_tv; + gettimeofday( &last_tv, 0L ); + + HTTPFilterChain chain; + + QObject::connect(&chain, SIGNAL(output(const QByteArray &)), + this, SLOT(slotData(const QByteArray &))); + QObject::connect(&chain, SIGNAL(error(int, const QString &)), + this, SLOT(error(int, const QString &))); + + // decode all of the transfer encodings + while (!m_qTransferEncodings.isEmpty()) + { + QString enc = m_qTransferEncodings.last(); + m_qTransferEncodings.remove(m_qTransferEncodings.fromLast()); + if ( enc == "gzip" ) + chain.addFilter(new HTTPFilterGZip); + else if ( enc == "deflate" ) + chain.addFilter(new HTTPFilterDeflate); + } + + // From HTTP 1.1 Draft 6: + // The MD5 digest is computed based on the content of the entity-body, + // including any content-coding that has been applied, but not including + // any transfer-encoding applied to the message-body. If the message is + // received with a transfer-encoding, that encoding MUST be removed + // prior to checking the Content-MD5 value against the received entity. + HTTPFilterMD5 *md5Filter = 0; + if ( useMD5 ) + { + md5Filter = new HTTPFilterMD5; + chain.addFilter(md5Filter); + } + + // now decode all of the content encodings + // -- Why ?? We are not + // -- a proxy server, be a client side implementation!! The applications + // -- are capable of determinig how to extract the encoded implementation. + // WB: That's a misunderstanding. We are free to remove the encoding. + // WB: Some braindead www-servers however, give .tgz files an encoding + // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar" + // WB: They shouldn't do that. We can work around that though... + while (!m_qContentEncodings.isEmpty()) + { + QString enc = m_qContentEncodings.last(); + m_qContentEncodings.remove(m_qContentEncodings.fromLast()); + if ( enc == "gzip" ) + chain.addFilter(new HTTPFilterGZip); + else if ( enc == "deflate" ) + chain.addFilter(new HTTPFilterDeflate); + } + + while (!m_bEOF) + { + int bytesReceived; + + if (m_bChunked) + bytesReceived = readChunked(); + else if (m_iSize != NO_SIZE) + bytesReceived = readLimited(); + else + bytesReceived = readUnlimited(); + + // make sure that this wasn't an error, first + // kdDebug(7113) << "(" << (int) m_pid << ") readBody: bytesReceived: " + // << (int) bytesReceived << " m_iSize: " << (int) m_iSize << " Chunked: " + // << (int) m_bChunked << " BytesLeft: "<< (int) m_iBytesLeft << endl; + if (bytesReceived == -1) + { + if (m_iContentLeft == 0) + { + // gzip'ed data sometimes reports a too long content-length. + // (The length of the unzipped data) + m_iBytesLeft = 0; + break; + } + // Oh well... log an error and bug out + kdDebug(7113) << "(" << m_pid << ") readBody: bytesReceived==-1 sz=" << (int)sz + << " Connnection broken !" << endl; + error(ERR_CONNECTION_BROKEN, m_state.hostname); + return false; + } + + // I guess that nbytes == 0 isn't an error.. but we certainly + // won't work with it! + if (bytesReceived > 0) + { + // Important: truncate the buffer to the actual size received! + // Otherwise garbage will be passed to the app + m_bufReceive.truncate( bytesReceived ); + + chain.slotInput(m_bufReceive); + + if (m_bError) + return false; + + sz += bytesReceived; + if (!dataInternal) + processedSize( sz ); + } + m_bufReceive.resize(0); // res + + if (m_iBytesLeft && m_bEOD && !m_bChunked) + { + // gzip'ed data sometimes reports a too long content-length. + // (The length of the unzipped data) + m_iBytesLeft = 0; + } + + if (m_iBytesLeft == 0) + { + kdDebug(7113) << "("<<m_pid<<") EOD received! Left = "<< KIO::number(m_iBytesLeft) << endl; + break; + } + } + chain.slotInput(QByteArray()); // Flush chain. + + if ( useMD5 ) + { + QString calculatedMD5 = md5Filter->md5(); + + if ( m_sContentMD5 == calculatedMD5 ) + kdDebug(7113) << "(" << m_pid << ") MD5 checksum MATCHED!!" << endl; + else + kdDebug(7113) << "(" << m_pid << ") MD5 checksum MISMATCH! Expected: " + << calculatedMD5 << ", Got: " << m_sContentMD5 << endl; + } + + // Close cache entry + if (m_iBytesLeft == 0) + { + if (m_request.bCachedWrite && m_request.fcache) + closeCacheEntry(); + else if (m_request.bCachedWrite) + kdDebug(7113) << "(" << m_pid << ") no cache file!\n"; + } + else + { + kdDebug(7113) << "(" << m_pid << ") still "<< KIO::number(m_iBytesLeft) + << " bytes left! can't close cache entry!\n"; + } + + if (sz <= 1) + { + /* kdDebug(7113) << "(" << m_pid << ") readBody: sz = " << KIO::number(sz) + << ", responseCode =" << m_responseCode << endl; */ + if (m_responseCode >= 500 && m_responseCode <= 599) + error(ERR_INTERNAL_SERVER, m_state.hostname); + else if (m_responseCode >= 400 && m_responseCode <= 499) + error(ERR_DOES_NOT_EXIST, m_state.hostname); + } + + if (!dataInternal) + data( QByteArray() ); + + return true; +} + + +void HTTPProtocol::error( int _err, const QString &_text ) +{ + httpClose(false); + + if (!m_request.id.isEmpty()) + { + forwardHttpResponseHeader(); + sendMetaData(); + } + + // Clear of the temporary POST buffer if it is not empty... + if (!m_bufPOST.isEmpty()) + { + m_bufPOST.resize(0); + kdDebug(7113) << "(" << m_pid << ") HTTP::retreiveHeader: Cleared POST " + "buffer..." << endl; + } + + SlaveBase::error( _err, _text ); + m_bError = true; +} + + +void HTTPProtocol::addCookies( const QString &url, const QCString &cookieHeader ) +{ + long windowId = m_request.window.toLong(); + QByteArray params; + QDataStream stream(params, IO_WriteOnly); + stream << url << cookieHeader << windowId; + + kdDebug(7113) << "(" << m_pid << ") " << cookieHeader << endl; + kdDebug(7113) << "(" << m_pid << ") " << "Window ID: " + << windowId << ", for host = " << url << endl; + + if ( !dcopClient()->send( "kded", "kcookiejar", "addCookies(QString,QCString,long int)", params ) ) + { + kdWarning(7113) << "(" << m_pid << ") Can't communicate with kded_kcookiejar!" << endl; + } +} + +QString HTTPProtocol::findCookies( const QString &url) +{ + QCString replyType; + QByteArray params; + QByteArray reply; + QString result; + + long windowId = m_request.window.toLong(); + result = QString::null; + QDataStream stream(params, IO_WriteOnly); + stream << url << windowId; + + if ( !dcopClient()->call( "kded", "kcookiejar", "findCookies(QString,long int)", + params, replyType, reply ) ) + { + kdWarning(7113) << "(" << m_pid << ") Can't communicate with kded_kcookiejar!" << endl; + return result; + } + if ( replyType == "QString" ) + { + QDataStream stream2( reply, IO_ReadOnly ); + stream2 >> result; + } + else + { + kdError(7113) << "(" << m_pid << ") DCOP function findCookies(...) returns " + << replyType << ", expected QString" << endl; + } + return result; +} + +/******************************* CACHING CODE ****************************/ + + +void HTTPProtocol::cacheUpdate( const KURL& url, bool no_cache, time_t expireDate) +{ + if ( !checkRequestURL( url ) ) + return; + + m_request.path = url.path(); + m_request.query = url.query(); + m_request.cache = CC_Reload; + m_request.doProxy = m_bUseProxy; + + if (no_cache) + { + m_request.fcache = checkCacheEntry( ); + if (m_request.fcache) + { + fclose(m_request.fcache); + m_request.fcache = 0; + ::unlink( QFile::encodeName(m_request.cef) ); + } + } + else + { + updateExpireDate( expireDate ); + } + finished(); +} + +// !START SYNC! +// The following code should be kept in sync +// with the code in http_cache_cleaner.cpp + +FILE* HTTPProtocol::checkCacheEntry( bool readWrite) +{ + const QChar separator = '_'; + + QString CEF = m_request.path; + + int p = CEF.find('/'); + + while(p != -1) + { + CEF[p] = separator; + p = CEF.find('/', p); + } + + QString host = m_request.hostname.lower(); + CEF = host + CEF + '_'; + + QString dir = m_strCacheDir; + if (dir[dir.length()-1] != '/') + dir += "/"; + + int l = host.length(); + for(int i = 0; i < l; i++) + { + if (host[i].isLetter() && (host[i] != 'w')) + { + dir += host[i]; + break; + } + } + if (dir[dir.length()-1] == '/') + dir += "0"; + + unsigned long hash = 0x00000000; + QCString u = m_request.url.url().latin1(); + for(int i = u.length(); i--;) + { + hash = (hash * 12211 + u[i]) % 2147483563; + } + + QString hashString; + hashString.sprintf("%08lx", hash); + + CEF = CEF + hashString; + + CEF = dir + "/" + CEF; + + m_request.cef = CEF; + + const char *mode = (readWrite ? "r+" : "r"); + + FILE *fs = fopen( QFile::encodeName(CEF), mode); // Open for reading and writing + if (!fs) + return 0; + + char buffer[401]; + bool ok = true; + + // CacheRevision + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok && (strcmp(buffer, CACHE_REVISION) != 0)) + ok = false; + + time_t date; + time_t currentDate = time(0); + + // URL + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + int l = strlen(buffer); + if (l>0) + buffer[l-1] = 0; // Strip newline + if (m_request.url.url() != buffer) + { + ok = false; // Hash collision + } + } + + // Creation Date + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + date = (time_t) strtoul(buffer, 0, 10); + m_request.creationDate = date; + if (m_maxCacheAge && (difftime(currentDate, date) > m_maxCacheAge)) + { + m_request.bMustRevalidate = true; + m_request.expireDate = currentDate; + } + } + + // Expiration Date + m_request.cacheExpireDateOffset = ftell(fs); + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + if (m_request.cache == CC_Verify) + { + date = (time_t) strtoul(buffer, 0, 10); + // After the expire date we need to revalidate. + if (!date || difftime(currentDate, date) >= 0) + m_request.bMustRevalidate = true; + m_request.expireDate = date; + } + else if (m_request.cache == CC_Refresh) + { + m_request.bMustRevalidate = true; + m_request.expireDate = currentDate; + } + } + + // ETag + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + m_request.etag = QString(buffer).stripWhiteSpace(); + } + + // Last-Modified + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + m_request.lastModified = QString(buffer).stripWhiteSpace(); + } + + if (ok) + return fs; + + fclose(fs); + unlink( QFile::encodeName(CEF)); + return 0; +} + +void HTTPProtocol::updateExpireDate(time_t expireDate, bool updateCreationDate) +{ + bool ok = true; + + FILE *fs = checkCacheEntry(true); + if (fs) + { + QString date; + char buffer[401]; + time_t creationDate; + + fseek(fs, 0, SEEK_SET); + if (ok && !fgets(buffer, 400, fs)) + ok = false; + if (ok && !fgets(buffer, 400, fs)) + ok = false; + long cacheCreationDateOffset = ftell(fs); + if (ok && !fgets(buffer, 400, fs)) + ok = false; + creationDate = strtoul(buffer, 0, 10); + if (!creationDate) + ok = false; + + if (updateCreationDate) + { + if (!ok || fseek(fs, cacheCreationDateOffset, SEEK_SET)) + return; + QString date; + date.setNum( time(0) ); + date = date.leftJustify(16); + fputs(date.latin1(), fs); // Creation date + fputc('\n', fs); + } + + if (expireDate>(30*365*24*60*60)) + { + // expire date is a really a big number, it can't be + // a relative date. + date.setNum( expireDate ); + } + else + { + // expireDate before 2000. those values must be + // interpreted as relative expiration dates from + // <META http-equiv="Expires"> tags. + // so we have to scan the creation time and add + // it to the expiryDate + date.setNum( creationDate + expireDate ); + } + date = date.leftJustify(16); + if (!ok || fseek(fs, m_request.cacheExpireDateOffset, SEEK_SET)) + return; + fputs(date.latin1(), fs); // Expire date + fseek(fs, 0, SEEK_END); + fclose(fs); + } +} + +void HTTPProtocol::createCacheEntry( const QString &mimetype, time_t expireDate) +{ + QString dir = m_request.cef; + int p = dir.findRev('/'); + if (p == -1) return; // Error. + dir.truncate(p); + + // Create file + (void) ::mkdir( QFile::encodeName(dir), 0700 ); + + QString filename = m_request.cef + ".new"; // Create a new cache entryexpireDate + +// kdDebug( 7103 ) << "creating new cache entry: " << filename << endl; + + m_request.fcache = fopen( QFile::encodeName(filename), "w"); + if (!m_request.fcache) + { + kdWarning(7113) << "(" << m_pid << ")createCacheEntry: opening " << filename << " failed." << endl; + return; // Error. + } + + fputs(CACHE_REVISION, m_request.fcache); // Revision + + fputs(m_request.url.url().latin1(), m_request.fcache); // Url + fputc('\n', m_request.fcache); + + QString date; + m_request.creationDate = time(0); + date.setNum( m_request.creationDate ); + date = date.leftJustify(16); + fputs(date.latin1(), m_request.fcache); // Creation date + fputc('\n', m_request.fcache); + + date.setNum( expireDate ); + date = date.leftJustify(16); + fputs(date.latin1(), m_request.fcache); // Expire date + fputc('\n', m_request.fcache); + + if (!m_request.etag.isEmpty()) + fputs(m_request.etag.latin1(), m_request.fcache); //ETag + fputc('\n', m_request.fcache); + + if (!m_request.lastModified.isEmpty()) + fputs(m_request.lastModified.latin1(), m_request.fcache); // Last modified + fputc('\n', m_request.fcache); + + fputs(mimetype.latin1(), m_request.fcache); // Mimetype + fputc('\n', m_request.fcache); + + if (!m_request.strCharset.isEmpty()) + fputs(m_request.strCharset.latin1(), m_request.fcache); // Charset + fputc('\n', m_request.fcache); + + return; +} +// The above code should be kept in sync +// with the code in http_cache_cleaner.cpp +// !END SYNC! + +void HTTPProtocol::writeCacheEntry( const char *buffer, int nbytes) +{ + if (fwrite( buffer, nbytes, 1, m_request.fcache) != 1) + { + kdWarning(7113) << "(" << m_pid << ") writeCacheEntry: writing " << nbytes << " bytes failed." << endl; + fclose(m_request.fcache); + m_request.fcache = 0; + QString filename = m_request.cef + ".new"; + ::unlink( QFile::encodeName(filename) ); + return; + } + long file_pos = ftell( m_request.fcache ) / 1024; + if ( file_pos > m_maxCacheSize ) + { + kdDebug(7113) << "writeCacheEntry: File size reaches " << file_pos + << "Kb, exceeds cache limits. (" << m_maxCacheSize << "Kb)" << endl; + fclose(m_request.fcache); + m_request.fcache = 0; + QString filename = m_request.cef + ".new"; + ::unlink( QFile::encodeName(filename) ); + return; + } +} + +void HTTPProtocol::closeCacheEntry() +{ + QString filename = m_request.cef + ".new"; + int result = fclose( m_request.fcache); + m_request.fcache = 0; + if (result == 0) + { + if (::rename( QFile::encodeName(filename), QFile::encodeName(m_request.cef)) == 0) + return; // Success + + kdWarning(7113) << "(" << m_pid << ") closeCacheEntry: error renaming " + << "cache entry. (" << filename << " -> " << m_request.cef + << ")" << endl; + } + + kdWarning(7113) << "(" << m_pid << ") closeCacheEntry: error closing cache " + << "entry. (" << filename<< ")" << endl; +} + +void HTTPProtocol::cleanCache() +{ + const time_t maxAge = DEFAULT_CLEAN_CACHE_INTERVAL; // 30 Minutes. + bool doClean = false; + QString cleanFile = m_strCacheDir; + if (cleanFile[cleanFile.length()-1] != '/') + cleanFile += "/"; + cleanFile += "cleaned"; + + struct stat stat_buf; + + int result = ::stat(QFile::encodeName(cleanFile), &stat_buf); + if (result == -1) + { + int fd = creat( QFile::encodeName(cleanFile), 0600); + if (fd != -1) + { + doClean = true; + ::close(fd); + } + } + else + { + time_t age = (time_t) difftime( time(0), stat_buf.st_mtime ); + if (age > maxAge) // + doClean = true; + } + if (doClean) + { + // Touch file. + utime(QFile::encodeName(cleanFile), 0); + KApplication::startServiceByDesktopPath("http_cache_cleaner.desktop"); + } +} + + + +//************************** AUTHENTICATION CODE ********************/ + + +void HTTPProtocol::configAuth( char *p, bool isForProxy ) +{ + HTTP_AUTH f = AUTH_None; + const char *strAuth = p; + + if ( strncasecmp( p, "Basic", 5 ) == 0 ) + { + f = AUTH_Basic; + p += 5; + strAuth = "Basic"; // Correct for upper-case variations. + } + else if ( strncasecmp (p, "Digest", 6) == 0 ) + { + f = AUTH_Digest; + memcpy((void *)p, "Digest", 6); // Correct for upper-case variations. + p += 6; + } + else if (strncasecmp( p, "MBS_PWD_COOKIE", 14 ) == 0) + { + // Found on http://www.webscription.net/baen/default.asp + f = AUTH_Basic; + p += 14; + strAuth = "Basic"; + } +#ifdef HAVE_LIBGSSAPI + else if ( strncasecmp( p, "Negotiate", 9 ) == 0 ) + { + // if we get two 401 in a row let's assume for now that + // Negotiate isn't working and ignore it + if ( !isForProxy && !(m_responseCode == 401 && m_prevResponseCode == 401) ) + { + f = AUTH_Negotiate; + memcpy((void *)p, "Negotiate", 9); // Correct for upper-case variations. + p += 9; + }; + } +#endif + else if ( strncasecmp( p, "NTLM", 4 ) == 0 ) + { + f = AUTH_NTLM; + memcpy((void *)p, "NTLM", 4); // Correct for upper-case variations. + p += 4; + m_strRealm = "NTLM"; // set a dummy realm + } + else + { + kdWarning(7113) << "(" << m_pid << ") Unsupported or invalid authorization " + << "type requested" << endl; + if (isForProxy) + kdWarning(7113) << "(" << m_pid << ") Proxy URL: " << m_proxyURL << endl; + else + kdWarning(7113) << "(" << m_pid << ") URL: " << m_request.url << endl; + kdWarning(7113) << "(" << m_pid << ") Request Authorization: " << p << endl; + } + + /* + This check ensures the following: + 1.) Rejection of any unknown/unsupported authentication schemes + 2.) Usage of the strongest possible authentication schemes if + and when multiple Proxy-Authenticate or WWW-Authenticate + header field is sent. + */ + if (isForProxy) + { + if ((f == AUTH_None) || + ((m_iProxyAuthCount > 0) && (f < ProxyAuthentication))) + { + // Since I purposefully made the Proxy-Authentication settings + // persistent to reduce the number of round-trips to kdesud we + // have to take special care when an unknown/unsupported auth- + // scheme is received. This check accomplishes just that... + if ( m_iProxyAuthCount == 0) + ProxyAuthentication = f; + kdDebug(7113) << "(" << m_pid << ") Rejected proxy auth method: " << f << endl; + return; + } + m_iProxyAuthCount++; + kdDebug(7113) << "(" << m_pid << ") Accepted proxy auth method: " << f << endl; + } + else + { + if ((f == AUTH_None) || + ((m_iWWWAuthCount > 0) && (f < Authentication))) + { + kdDebug(7113) << "(" << m_pid << ") Rejected auth method: " << f << endl; + return; + } + m_iWWWAuthCount++; + kdDebug(7113) << "(" << m_pid << ") Accepted auth method: " << f << endl; + } + + + while (*p) + { + int i = 0; + while( (*p == ' ') || (*p == ',') || (*p == '\t') ) { p++; } + if ( strncasecmp( p, "realm=", 6 ) == 0 ) + { + //for sites like lib.homelinux.org + QTextCodec* oldCodec=QTextCodec::codecForCStrings(); + if (KGlobal::locale()->language().contains("ru")) + QTextCodec::setCodecForCStrings(QTextCodec::codecForName("CP1251")); + + p += 6; + if (*p == '"') p++; + while( p[i] && p[i] != '"' ) i++; + if( isForProxy ) + m_strProxyRealm = QString::fromAscii( p, i ); + else + m_strRealm = QString::fromAscii( p, i ); + + QTextCodec::setCodecForCStrings(oldCodec); + + if (!p[i]) break; + } + p+=(i+1); + } + + if( isForProxy ) + { + ProxyAuthentication = f; + m_strProxyAuthorization = QString::fromLatin1( strAuth ); + } + else + { + Authentication = f; + m_strAuthorization = QString::fromLatin1( strAuth ); + } +} + + +bool HTTPProtocol::retryPrompt() +{ + QString prompt; + switch ( m_responseCode ) + { + case 401: + prompt = i18n("Authentication Failed."); + break; + case 407: + prompt = i18n("Proxy Authentication Failed."); + break; + default: + break; + } + prompt += i18n(" Do you want to retry?"); + return (messageBox(QuestionYesNo, prompt, i18n("Authentication")) == 3); +} + +void HTTPProtocol::promptInfo( AuthInfo& info ) +{ + if ( m_responseCode == 401 ) + { + info.url = m_request.url; + if ( !m_state.user.isEmpty() ) + info.username = m_state.user; + info.readOnly = !m_request.url.user().isEmpty(); + info.prompt = i18n( "You need to supply a username and a " + "password to access this site." ); + info.keepPassword = true; // Prompt the user for persistence as well. + if ( !m_strRealm.isEmpty() ) + { + info.realmValue = m_strRealm; + info.verifyPath = false; + info.digestInfo = m_strAuthorization; + info.commentLabel = i18n( "Site:" ); + info.comment = i18n("<b>%1</b> at <b>%2</b>").arg( m_strRealm ).arg( m_request.hostname ); + } + } + else if ( m_responseCode == 407 ) + { + info.url = m_proxyURL; + info.username = m_proxyURL.user(); + info.prompt = i18n( "You need to supply a username and a password for " + "the proxy server listed below before you are allowed " + "to access any sites." ); + info.keepPassword = true; + if ( !m_strProxyRealm.isEmpty() ) + { + info.realmValue = m_strProxyRealm; + info.verifyPath = false; + info.digestInfo = m_strProxyAuthorization; + info.commentLabel = i18n( "Proxy:" ); + info.comment = i18n("<b>%1</b> at <b>%2</b>").arg( m_strProxyRealm ).arg( m_proxyURL.host() ); + } + } +} + +bool HTTPProtocol::getAuthorization() +{ + AuthInfo info; + bool result = false; + + kdDebug (7113) << "(" << m_pid << ") HTTPProtocol::getAuthorization: " + << "Current Response: " << m_responseCode << ", " + << "Previous Response: " << m_prevResponseCode << ", " + << "Authentication: " << Authentication << ", " + << "ProxyAuthentication: " << ProxyAuthentication << endl; + + if (m_request.bNoAuth) + { + if (m_request.bErrorPage) + errorPage(); + else + error( ERR_COULD_NOT_LOGIN, i18n("Authentication needed for %1 but authentication is disabled.").arg(m_request.hostname)); + return false; + } + + bool repeatFailure = (m_prevResponseCode == m_responseCode); + + QString errorMsg; + + if (repeatFailure) + { + bool prompt = true; + if ( Authentication == AUTH_Digest || ProxyAuthentication == AUTH_Digest ) + { + bool isStaleNonce = false; + QString auth = ( m_responseCode == 401 ) ? m_strAuthorization : m_strProxyAuthorization; + int pos = auth.find("stale", 0, false); + if ( pos != -1 ) + { + pos += 5; + int len = auth.length(); + while( pos < len && (auth[pos] == ' ' || auth[pos] == '=') ) pos++; + if ( pos < len && auth.find("true", pos, false) != -1 ) + { + isStaleNonce = true; + kdDebug(7113) << "(" << m_pid << ") Stale nonce value. " + << "Will retry using same info..." << endl; + } + } + if ( isStaleNonce ) + { + prompt = false; + result = true; + if ( m_responseCode == 401 ) + { + info.username = m_request.user; + info.password = m_request.passwd; + info.realmValue = m_strRealm; + info.digestInfo = m_strAuthorization; + } + else if ( m_responseCode == 407 ) + { + info.username = m_proxyURL.user(); + info.password = m_proxyURL.pass(); + info.realmValue = m_strProxyRealm; + info.digestInfo = m_strProxyAuthorization; + } + } + } + + if ( Authentication == AUTH_NTLM || ProxyAuthentication == AUTH_NTLM ) + { + QString auth = ( m_responseCode == 401 ) ? m_strAuthorization : m_strProxyAuthorization; + kdDebug(7113) << "auth: " << auth << endl; + if ( auth.length() > 4 ) + { + prompt = false; + result = true; + kdDebug(7113) << "(" << m_pid << ") NTLM auth second phase, " + << "sending response..." << endl; + if ( m_responseCode == 401 ) + { + info.username = m_request.user; + info.password = m_request.passwd; + info.realmValue = m_strRealm; + info.digestInfo = m_strAuthorization; + } + else if ( m_responseCode == 407 ) + { + info.username = m_proxyURL.user(); + info.password = m_proxyURL.pass(); + info.realmValue = m_strProxyRealm; + info.digestInfo = m_strProxyAuthorization; + } + } + } + + if ( prompt ) + { + switch ( m_responseCode ) + { + case 401: + errorMsg = i18n("Authentication Failed."); + break; + case 407: + errorMsg = i18n("Proxy Authentication Failed."); + break; + default: + break; + } + } + } + else + { + // At this point we know more details, so use it to find + // out if we have a cached version and avoid a re-prompt! + // We also do not use verify path unlike the pre-emptive + // requests because we already know the realm value... + + if (m_bProxyAuthValid) + { + // Reset cached proxy auth + m_bProxyAuthValid = false; + KURL proxy ( config()->readEntry("UseProxy") ); + m_proxyURL.setUser(proxy.user()); + m_proxyURL.setPass(proxy.pass()); + } + + info.verifyPath = false; + if ( m_responseCode == 407 ) + { + info.url = m_proxyURL; + info.username = m_proxyURL.user(); + info.password = m_proxyURL.pass(); + info.realmValue = m_strProxyRealm; + info.digestInfo = m_strProxyAuthorization; + } + else + { + info.url = m_request.url; + info.username = m_request.user; + info.password = m_request.passwd; + info.realmValue = m_strRealm; + info.digestInfo = m_strAuthorization; + } + + // If either username or password is not supplied + // with the request, check the password cache. + if ( info.username.isNull() || + info.password.isNull() ) + result = checkCachedAuthentication( info ); + + if ( Authentication == AUTH_Digest ) + { + QString auth; + + if (m_responseCode == 401) + auth = m_strAuthorization; + else + auth = m_strProxyAuthorization; + + int pos = auth.find("stale", 0, false); + if ( pos != -1 ) + { + pos += 5; + int len = auth.length(); + while( pos < len && (auth[pos] == ' ' || auth[pos] == '=') ) pos++; + if ( pos < len && auth.find("true", pos, false) != -1 ) + { + info.digestInfo = (m_responseCode == 401) ? m_strAuthorization : m_strProxyAuthorization; + kdDebug(7113) << "(" << m_pid << ") Just a stale nonce value! " + << "Retrying using the new nonce sent..." << endl; + } + } + } + } + + if (!result ) + { + // Do not prompt if the username & password + // is already supplied and the login attempt + // did not fail before. + if ( !repeatFailure && + !info.username.isNull() && + !info.password.isNull() ) + result = true; + else + { + if (Authentication == AUTH_Negotiate) + { + if (!repeatFailure) + result = true; + } + else if ( m_request.disablePassDlg == false ) + { + kdDebug( 7113 ) << "(" << m_pid << ") Prompting the user for authorization..." << endl; + promptInfo( info ); + result = openPassDlg( info, errorMsg ); + } + } + } + + if ( result ) + { + switch (m_responseCode) + { + case 401: // Request-Authentication + m_request.user = info.username; + m_request.passwd = info.password; + m_strRealm = info.realmValue; + m_strAuthorization = info.digestInfo; + break; + case 407: // Proxy-Authentication + m_proxyURL.setUser( info.username ); + m_proxyURL.setPass( info.password ); + m_strProxyRealm = info.realmValue; + m_strProxyAuthorization = info.digestInfo; + break; + default: + break; + } + return true; + } + + if (m_request.bErrorPage) + errorPage(); + else + error( ERR_USER_CANCELED, QString::null ); + return false; +} + +void HTTPProtocol::saveAuthorization() +{ + AuthInfo info; + if ( m_prevResponseCode == 407 ) + { + if (!m_bUseProxy) + return; + m_bProxyAuthValid = true; + info.url = m_proxyURL; + info.username = m_proxyURL.user(); + info.password = m_proxyURL.pass(); + info.realmValue = m_strProxyRealm; + info.digestInfo = m_strProxyAuthorization; + cacheAuthentication( info ); + } + else + { + info.url = m_request.url; + info.username = m_request.user; + info.password = m_request.passwd; + info.realmValue = m_strRealm; + info.digestInfo = m_strAuthorization; + cacheAuthentication( info ); + } +} + +#ifdef HAVE_LIBGSSAPI +QCString HTTPProtocol::gssError( int major_status, int minor_status ) +{ + OM_uint32 new_status; + OM_uint32 msg_ctx = 0; + gss_buffer_desc major_string; + gss_buffer_desc minor_string; + OM_uint32 ret; + QCString errorstr; + + errorstr = ""; + + do { + ret = gss_display_status(&new_status, major_status, GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_ctx, &major_string); + errorstr += (const char *)major_string.value; + errorstr += " "; + ret = gss_display_status(&new_status, minor_status, GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_ctx, &minor_string); + errorstr += (const char *)minor_string.value; + errorstr += " "; + } while (!GSS_ERROR(ret) && msg_ctx != 0); + + return errorstr; +} + +QString HTTPProtocol::createNegotiateAuth() +{ + QString auth; + QCString servicename; + QByteArray input; + OM_uint32 major_status, minor_status; + OM_uint32 req_flags = 0; + gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; + gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; + gss_name_t server; + gss_ctx_id_t ctx; + gss_OID mech_oid; + static gss_OID_desc krb5_oid_desc = {9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02"}; + static gss_OID_desc spnego_oid_desc = {6, (void *) "\x2b\x06\x01\x05\x05\x02"}; + int found = 0; + unsigned int i; + gss_OID_set mech_set; + gss_OID tmp_oid; + + ctx = GSS_C_NO_CONTEXT; + mech_oid = &krb5_oid_desc; + + // see whether we can use the SPNEGO mechanism + major_status = gss_indicate_mechs(&minor_status, &mech_set); + if (GSS_ERROR(major_status)) { + kdDebug(7113) << "(" << m_pid << ") gss_indicate_mechs failed: " << gssError(major_status, minor_status) << endl; + } else { + for (i=0; i<mech_set->count && !found; i++) { + tmp_oid = &mech_set->elements[i]; + if (tmp_oid->length == spnego_oid_desc.length && + !memcmp(tmp_oid->elements, spnego_oid_desc.elements, tmp_oid->length)) { + kdDebug(7113) << "(" << m_pid << ") createNegotiateAuth: found SPNEGO mech" << endl; + found = 1; + mech_oid = &spnego_oid_desc; + break; + } + } + gss_release_oid_set(&minor_status, &mech_set); + } + + // the service name is "HTTP/f.q.d.n" + servicename = "HTTP@"; + servicename += m_state.hostname.ascii(); + + input_token.value = (void *)servicename.data(); + input_token.length = servicename.length() + 1; + + major_status = gss_import_name(&minor_status, &input_token, + GSS_C_NT_HOSTBASED_SERVICE, &server); + + input_token.value = NULL; + input_token.length = 0; + + if (GSS_ERROR(major_status)) { + kdDebug(7113) << "(" << m_pid << ") gss_import_name failed: " << gssError(major_status, minor_status) << endl; + // reset the auth string so that subsequent methods aren't confused + m_strAuthorization = QString::null; + return QString::null; + } + + major_status = gss_init_sec_context(&minor_status, GSS_C_NO_CREDENTIAL, + &ctx, server, mech_oid, + req_flags, GSS_C_INDEFINITE, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_C_NO_BUFFER, NULL, &output_token, + NULL, NULL); + + + if (GSS_ERROR(major_status) || (output_token.length == 0)) { + kdDebug(7113) << "(" << m_pid << ") gss_init_sec_context failed: " << gssError(major_status, minor_status) << endl; + gss_release_name(&minor_status, &server); + if (ctx != GSS_C_NO_CONTEXT) { + gss_delete_sec_context(&minor_status, &ctx, GSS_C_NO_BUFFER); + ctx = GSS_C_NO_CONTEXT; + } + // reset the auth string so that subsequent methods aren't confused + m_strAuthorization = QString::null; + return QString::null; + } + + input.duplicate((const char *)output_token.value, output_token.length); + auth = "Authorization: Negotiate "; + auth += KCodecs::base64Encode( input ); + auth += "\r\n"; + + // free everything + gss_release_name(&minor_status, &server); + if (ctx != GSS_C_NO_CONTEXT) { + gss_delete_sec_context(&minor_status, &ctx, GSS_C_NO_BUFFER); + ctx = GSS_C_NO_CONTEXT; + } + gss_release_buffer(&minor_status, &output_token); + + return auth; +} +#else + +// Dummy +QCString HTTPProtocol::gssError( int, int ) +{ + return ""; +} + +// Dummy +QString HTTPProtocol::createNegotiateAuth() +{ + return QString::null; +} +#endif + +QString HTTPProtocol::createNTLMAuth( bool isForProxy ) +{ + uint len; + QString auth, user, domain, passwd; + QCString strauth; + QByteArray buf; + + if ( isForProxy ) + { + auth = "Proxy-Connection: Keep-Alive\r\n"; + auth += "Proxy-Authorization: NTLM "; + user = m_proxyURL.user(); + passwd = m_proxyURL.pass(); + strauth = m_strProxyAuthorization.latin1(); + len = m_strProxyAuthorization.length(); + } + else + { + auth = "Authorization: NTLM "; + user = m_state.user; + passwd = m_state.passwd; + strauth = m_strAuthorization.latin1(); + len = m_strAuthorization.length(); + } + if ( user.contains('\\') ) { + domain = user.section( '\\', 0, 0); + user = user.section( '\\', 1 ); + } + + kdDebug(7113) << "(" << m_pid << ") NTLM length: " << len << endl; + if ( user.isEmpty() || passwd.isEmpty() || len < 4 ) + return QString::null; + + if ( len > 4 ) + { + // create a response + QByteArray challenge; + KCodecs::base64Decode( strauth.right( len - 5 ), challenge ); + KNTLM::getAuth( buf, challenge, user, passwd, domain, + KNetwork::KResolver::localHostName(), false, false ); + } + else + { + KNTLM::getNegotiate( buf ); + } + + // remove the challenge to prevent reuse + if ( isForProxy ) + m_strProxyAuthorization = "NTLM"; + else + m_strAuthorization = "NTLM"; + + auth += KCodecs::base64Encode( buf ); + auth += "\r\n"; + + return auth; +} + +QString HTTPProtocol::createBasicAuth( bool isForProxy ) +{ + QString auth; + QCString user, passwd; + if ( isForProxy ) + { + auth = "Proxy-Authorization: Basic "; + user = m_proxyURL.user().latin1(); + passwd = m_proxyURL.pass().latin1(); + } + else + { + auth = "Authorization: Basic "; + user = m_state.user.latin1(); + passwd = m_state.passwd.latin1(); + } + + if ( user.isEmpty() ) + user = ""; + if ( passwd.isEmpty() ) + passwd = ""; + + user += ':'; + user += passwd; + auth += KCodecs::base64Encode( user ); + auth += "\r\n"; + + return auth; +} + +void HTTPProtocol::calculateResponse( DigestAuthInfo& info, QCString& Response ) +{ + KMD5 md; + QCString HA1; + QCString HA2; + + // Calculate H(A1) + QCString authStr = info.username; + authStr += ':'; + authStr += info.realm; + authStr += ':'; + authStr += info.password; + md.update( authStr ); + + if ( info.algorithm.lower() == "md5-sess" ) + { + authStr = md.hexDigest(); + authStr += ':'; + authStr += info.nonce; + authStr += ':'; + authStr += info.cnonce; + md.reset(); + md.update( authStr ); + } + HA1 = md.hexDigest(); + + kdDebug(7113) << "(" << m_pid << ") calculateResponse(): A1 => " << HA1 << endl; + + // Calcualte H(A2) + authStr = info.method; + authStr += ':'; + authStr += m_request.url.encodedPathAndQuery(0, true).latin1(); + if ( info.qop == "auth-int" ) + { + authStr += ':'; + authStr += info.entityBody; + } + md.reset(); + md.update( authStr ); + HA2 = md.hexDigest(); + + kdDebug(7113) << "(" << m_pid << ") calculateResponse(): A2 => " + << HA2 << endl; + + // Calcualte the response. + authStr = HA1; + authStr += ':'; + authStr += info.nonce; + authStr += ':'; + if ( !info.qop.isEmpty() ) + { + authStr += info.nc; + authStr += ':'; + authStr += info.cnonce; + authStr += ':'; + authStr += info.qop; + authStr += ':'; + } + authStr += HA2; + md.reset(); + md.update( authStr ); + Response = md.hexDigest(); + + kdDebug(7113) << "(" << m_pid << ") calculateResponse(): Response => " + << Response << endl; +} + +QString HTTPProtocol::createDigestAuth ( bool isForProxy ) +{ + const char *p; + + QString auth; + QCString opaque; + QCString Response; + + DigestAuthInfo info; + + opaque = ""; + if ( isForProxy ) + { + auth = "Proxy-Authorization: Digest "; + info.username = m_proxyURL.user().latin1(); + info.password = m_proxyURL.pass().latin1(); + p = m_strProxyAuthorization.latin1(); + } + else + { + auth = "Authorization: Digest "; + info.username = m_state.user.latin1(); + info.password = m_state.passwd.latin1(); + p = m_strAuthorization.latin1(); + } + if (!p || !*p) + return QString::null; + + p += 6; // Skip "Digest" + + if ( info.username.isEmpty() || info.password.isEmpty() || !p ) + return QString::null; + + // info.entityBody = p; // FIXME: send digest of data for POST action ?? + info.realm = ""; + info.algorithm = "MD5"; + info.nonce = ""; + info.qop = ""; + + // cnonce is recommended to contain about 64 bits of entropy + info.cnonce = KApplication::randomString(16).latin1(); + + // HACK: Should be fixed according to RFC 2617 section 3.2.2 + info.nc = "00000001"; + + // Set the method used... + switch ( m_request.method ) + { + case HTTP_GET: + info.method = "GET"; + break; + case HTTP_PUT: + info.method = "PUT"; + break; + case HTTP_POST: + info.method = "POST"; + break; + case HTTP_HEAD: + info.method = "HEAD"; + break; + case HTTP_DELETE: + info.method = "DELETE"; + break; + case DAV_PROPFIND: + info.method = "PROPFIND"; + break; + case DAV_PROPPATCH: + info.method = "PROPPATCH"; + break; + case DAV_MKCOL: + info.method = "MKCOL"; + break; + case DAV_COPY: + info.method = "COPY"; + break; + case DAV_MOVE: + info.method = "MOVE"; + break; + case DAV_LOCK: + info.method = "LOCK"; + break; + case DAV_UNLOCK: + info.method = "UNLOCK"; + break; + case DAV_SEARCH: + info.method = "SEARCH"; + break; + case DAV_SUBSCRIBE: + info.method = "SUBSCRIBE"; + break; + case DAV_UNSUBSCRIBE: + info.method = "UNSUBSCRIBE"; + break; + case DAV_POLL: + info.method = "POLL"; + break; + default: + error( ERR_UNSUPPORTED_ACTION, i18n("Unsupported method: authentication will fail. Please submit a bug report.")); + break; + } + + // Parse the Digest response.... + while (*p) + { + int i = 0; + while ( (*p == ' ') || (*p == ',') || (*p == '\t')) { p++; } + if (strncasecmp(p, "realm=", 6 )==0) + { + p+=6; + while ( *p == '"' ) p++; // Go past any number of " mark(s) first + while ( p[i] != '"' ) i++; // Read everything until the last " mark + info.realm = QCString( p, i+1 ); + } + else if (strncasecmp(p, "algorith=", 9)==0) + { + p+=9; + while ( *p == '"' ) p++; // Go past any number of " mark(s) first + while ( ( p[i] != '"' ) && ( p[i] != ',' ) && ( p[i] != '\0' ) ) i++; + info.algorithm = QCString(p, i+1); + } + else if (strncasecmp(p, "algorithm=", 10)==0) + { + p+=10; + while ( *p == '"' ) p++; // Go past any " mark(s) first + while ( ( p[i] != '"' ) && ( p[i] != ',' ) && ( p[i] != '\0' ) ) i++; + info.algorithm = QCString(p,i+1); + } + else if (strncasecmp(p, "domain=", 7)==0) + { + p+=7; + while ( *p == '"' ) p++; // Go past any " mark(s) first + while ( p[i] != '"' ) i++; // Read everything until the last " mark + int pos; + int idx = 0; + QCString uri = QCString(p,i+1); + do + { + pos = uri.find( ' ', idx ); + if ( pos != -1 ) + { + KURL u (m_request.url, uri.mid(idx, pos-idx)); + if (u.isValid ()) + info.digestURI.append( u.url().latin1() ); + } + else + { + KURL u (m_request.url, uri.mid(idx, uri.length()-idx)); + if (u.isValid ()) + info.digestURI.append( u.url().latin1() ); + } + idx = pos+1; + } while ( pos != -1 ); + } + else if (strncasecmp(p, "nonce=", 6)==0) + { + p+=6; + while ( *p == '"' ) p++; // Go past any " mark(s) first + while ( p[i] != '"' ) i++; // Read everything until the last " mark + info.nonce = QCString(p,i+1); + } + else if (strncasecmp(p, "opaque=", 7)==0) + { + p+=7; + while ( *p == '"' ) p++; // Go past any " mark(s) first + while ( p[i] != '"' ) i++; // Read everything until the last " mark + opaque = QCString(p,i+1); + } + else if (strncasecmp(p, "qop=", 4)==0) + { + p+=4; + while ( *p == '"' ) p++; // Go past any " mark(s) first + while ( p[i] != '"' ) i++; // Read everything until the last " mark + info.qop = QCString(p,i+1); + } + p+=(i+1); + } + + if (info.realm.isEmpty() || info.nonce.isEmpty()) + return QString::null; + + // If the "domain" attribute was not specified and the current response code + // is authentication needed, add the current request url to the list over which + // this credential can be automatically applied. + if (info.digestURI.isEmpty() && (m_responseCode == 401 || m_responseCode == 407)) + info.digestURI.append (m_request.url.url().latin1()); + else + { + // Verify whether or not we should send a cached credential to the + // server based on the stored "domain" attribute... + bool send = true; + + // Determine the path of the request url... + QString requestPath = m_request.url.directory(false, false); + if (requestPath.isEmpty()) + requestPath = "/"; + + int count = info.digestURI.count(); + + for (int i = 0; i < count; i++ ) + { + KURL u ( info.digestURI.at(i) ); + + send &= (m_request.url.protocol().lower() == u.protocol().lower()); + send &= (m_request.hostname.lower() == u.host().lower()); + + if (m_request.port > 0 && u.port() > 0) + send &= (m_request.port == u.port()); + + QString digestPath = u.directory (false, false); + if (digestPath.isEmpty()) + digestPath = "/"; + + send &= (requestPath.startsWith(digestPath)); + + if (send) + break; + } + + kdDebug(7113) << "(" << m_pid << ") createDigestAuth(): passed digest " + "authentication credential test: " << send << endl; + + if (!send) + return QString::null; + } + + kdDebug(7113) << "(" << m_pid << ") RESULT OF PARSING:" << endl; + kdDebug(7113) << "(" << m_pid << ") algorithm: " << info.algorithm << endl; + kdDebug(7113) << "(" << m_pid << ") realm: " << info.realm << endl; + kdDebug(7113) << "(" << m_pid << ") nonce: " << info.nonce << endl; + kdDebug(7113) << "(" << m_pid << ") opaque: " << opaque << endl; + kdDebug(7113) << "(" << m_pid << ") qop: " << info.qop << endl; + + // Calculate the response... + calculateResponse( info, Response ); + + auth += "username=\""; + auth += info.username; + + auth += "\", realm=\""; + auth += info.realm; + auth += "\""; + + auth += ", nonce=\""; + auth += info.nonce; + + auth += "\", uri=\""; + auth += m_request.url.encodedPathAndQuery(0, true); + + auth += "\", algorithm=\""; + auth += info.algorithm; + auth +="\""; + + if ( !info.qop.isEmpty() ) + { + auth += ", qop=\""; + auth += info.qop; + auth += "\", cnonce=\""; + auth += info.cnonce; + auth += "\", nc="; + auth += info.nc; + } + + auth += ", response=\""; + auth += Response; + if ( !opaque.isEmpty() ) + { + auth += "\", opaque=\""; + auth += opaque; + } + auth += "\"\r\n"; + + return auth; +} + +QString HTTPProtocol::proxyAuthenticationHeader() +{ + QString header; + + // We keep proxy authentication locally until they are changed. + // Thus, no need to check with the password manager for every + // connection. + if ( m_strProxyRealm.isEmpty() ) + { + AuthInfo info; + info.url = m_proxyURL; + info.username = m_proxyURL.user(); + info.password = m_proxyURL.pass(); + info.verifyPath = true; + + // If the proxy URL already contains username + // and password simply attempt to retrieve it + // without prompting the user... + if ( !info.username.isNull() && !info.password.isNull() ) + { + if( m_strProxyAuthorization.isEmpty() ) + ProxyAuthentication = AUTH_None; + else if( m_strProxyAuthorization.startsWith("Basic") ) + ProxyAuthentication = AUTH_Basic; + else if( m_strProxyAuthorization.startsWith("NTLM") ) + ProxyAuthentication = AUTH_NTLM; + else + ProxyAuthentication = AUTH_Digest; + } + else + { + if ( checkCachedAuthentication(info) && !info.digestInfo.isEmpty() ) + { + m_proxyURL.setUser( info.username ); + m_proxyURL.setPass( info.password ); + m_strProxyRealm = info.realmValue; + m_strProxyAuthorization = info.digestInfo; + if( m_strProxyAuthorization.startsWith("Basic") ) + ProxyAuthentication = AUTH_Basic; + else if( m_strProxyAuthorization.startsWith("NTLM") ) + ProxyAuthentication = AUTH_NTLM; + else + ProxyAuthentication = AUTH_Digest; + } + else + { + ProxyAuthentication = AUTH_None; + } + } + } + + /********* Only for debugging purpose... *********/ + if ( ProxyAuthentication != AUTH_None ) + { + kdDebug(7113) << "(" << m_pid << ") Using Proxy Authentication: " << endl; + kdDebug(7113) << "(" << m_pid << ") HOST= " << m_proxyURL.host() << endl; + kdDebug(7113) << "(" << m_pid << ") PORT= " << m_proxyURL.port() << endl; + kdDebug(7113) << "(" << m_pid << ") USER= " << m_proxyURL.user() << endl; + kdDebug(7113) << "(" << m_pid << ") PASSWORD= [protected]" << endl; + kdDebug(7113) << "(" << m_pid << ") REALM= " << m_strProxyRealm << endl; + kdDebug(7113) << "(" << m_pid << ") EXTRA= " << m_strProxyAuthorization << endl; + } + + switch ( ProxyAuthentication ) + { + case AUTH_Basic: + header += createBasicAuth( true ); + break; + case AUTH_Digest: + header += createDigestAuth( true ); + break; + case AUTH_NTLM: + if ( m_bFirstRequest ) header += createNTLMAuth( true ); + break; + case AUTH_None: + default: + break; + } + + return header; +} + +#include "http.moc" diff --git a/kioslave/http/http.h b/kioslave/http/http.h new file mode 100644 index 000000000..ea2e68a8a --- /dev/null +++ b/kioslave/http/http.h @@ -0,0 +1,577 @@ +/* + Copyright (C) 2000,2001 Dawit Alemayehu <adawit@kde.org> + Copyright (C) 2000,2001 Waldo Bastian <bastian@kde.org> + Copyright (C) 2000,2001 George Staikos <staikos@kde.org> + Copyright (C) 2001,2002 Hamish Rodda <rodda@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef HTTP_H_ +#define HTTP_H_ + + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <string.h> +#include <stdio.h> +#include <time.h> + +#include <qptrlist.h> +#include <qstrlist.h> +#include <qstringlist.h> + +#include <kurl.h> +#include "kio/tcpslavebase.h" +#include "kio/http.h" + +class DCOPClient; +class QDomElement; +class QDomNodeList; + +namespace KIO { + class AuthInfo; +} + +class HTTPProtocol : public QObject, public KIO::TCPSlaveBase +{ + Q_OBJECT +public: + HTTPProtocol( const QCString &protocol, const QCString &pool, + const QCString &app ); + virtual ~HTTPProtocol(); + + /** HTTP version **/ + enum HTTP_REV {HTTP_None, HTTP_Unknown, HTTP_10, HTTP_11, SHOUTCAST}; + + /** Authorization method used **/ + enum HTTP_AUTH {AUTH_None, AUTH_Basic, AUTH_NTLM, AUTH_Digest, AUTH_Negotiate}; + + /** HTTP / DAV method **/ + // Removed to interfaces/kio/http.h + //enum HTTP_METHOD {HTTP_GET, HTTP_PUT, HTTP_POST, HTTP_HEAD, HTTP_DELETE, + // HTTP_OPTIONS, DAV_PROPFIND, DAV_PROPPATCH, DAV_MKCOL, + // DAV_COPY, DAV_MOVE, DAV_LOCK, DAV_UNLOCK, DAV_SEARCH }; + + /** State of the current Connection **/ + struct HTTPState + { + HTTPState () + { + port = 0; + doProxy = false; + } + + QString hostname; + QString encoded_hostname; + short unsigned int port; + QString user; + QString passwd; + bool doProxy; + }; + + /** DAV-specific request elements for the current connection **/ + struct DAVRequest + { + DAVRequest () + { + overwrite = false; + depth = 0; + } + + QString desturl; + bool overwrite; + int depth; + }; + + /** The request for the current connection **/ + struct HTTPRequest + { + HTTPRequest () + { + port = 0; + method = KIO::HTTP_UNKNOWN; + offset = 0; + doProxy = false; + allowCompressedPage = false; + disablePassDlg = false; + bNoAuth = false; + bUseCache = false; + bCachedRead = false; + bCachedWrite = false; + fcache = 0; + bMustRevalidate = false; + cacheExpireDateOffset = 0; + bErrorPage = false; + bUseCookiejar = false; + expireDate = 0; + creationDate = 0; + } + + QString hostname; + QString encoded_hostname; + short unsigned int port; + QString user; + QString passwd; + QString path; + QString query; + KIO::HTTP_METHOD method; + KIO::CacheControl cache; + KIO::filesize_t offset; + bool doProxy; + KURL url; + QString window; // Window Id this request is related to. + QString referrer; + QString charsets; + QString languages; + bool allowCompressedPage; + bool disablePassDlg; + QString userAgent; + QString id; + DAVRequest davData; + + bool bNoAuth; // Do not authenticate + + // Cache related + QString cef; // Cache Entry File belonging to this URL. + bool bUseCache; // Whether the cache is active + bool bCachedRead; // Whether the file is to be read from m_fcache. + bool bCachedWrite; // Whether the file is to be written to m_fcache. + FILE* fcache; // File stream of a cache entry + QString etag; // ETag header. + QString lastModified; // Last modified. + bool bMustRevalidate; // Cache entry is expired. + long cacheExpireDateOffset; // Position in the cache entry where the + // 16 byte expire date is stored. + time_t expireDate; // Date when the cache entry will expire + time_t creationDate; // Date when the cache entry was created + QString strCharset; // Charset + + // Indicates whether an error-page or error-msg should is preferred. + bool bErrorPage; + + // Cookie flags + bool bUseCookiejar; + enum { CookiesAuto, CookiesManual, CookiesNone } cookieMode; + }; + + struct DigestAuthInfo + { + QCString nc; + QCString qop; + QCString realm; + QCString nonce; + QCString method; + QCString cnonce; + QCString username; + QCString password; + QStrList digestURI; + QCString algorithm; + QCString entityBody; + }; + +//---------------------- Re-implemented methods ---------------- + virtual void setHost(const QString& host, int port, const QString& user, + const QString& pass); + + virtual void slave_status(); + + virtual void get( const KURL& url ); + virtual void put( const KURL& url, int permissions, bool overwrite, + bool resume ); + +//----------------- Re-implemented methods for WebDAV ----------- + virtual void listDir( const KURL& url ); + virtual void mkdir( const KURL& url, int permissions ); + + virtual void rename( const KURL& src, const KURL& dest, bool overwrite ); + virtual void copy( const KURL& src, const KURL& dest, int permissions, bool overwrite ); + virtual void del( const KURL& url, bool isfile ); + + // ask the host whether it supports WebDAV & cache this info + bool davHostOk(); + + // send generic DAV request + void davGeneric( const KURL& url, KIO::HTTP_METHOD method ); + + // Send requests to lock and unlock resources + void davLock( const KURL& url, const QString& scope, + const QString& type, const QString& owner ); + void davUnlock( const KURL& url ); + + // Calls httpClose() and finished() + void davFinished(); + + // Handle error conditions + QString davError( int code = -1, QString url = QString::null ); +//---------------------------- End WebDAV ----------------------- + + /** + * Special commands supported by this slave : + * 1 - HTTP POST + * 2 - Cache has been updated + * 3 - SSL Certificate Cache has been updated + * 4 - HTTP multi get + * 5 - DAV LOCK (see + * 6 - DAV UNLOCK README.webdav) + */ + virtual void special( const QByteArray &data ); + + virtual void mimetype( const KURL& url); + + virtual void stat( const KURL& url ); + + virtual void reparseConfiguration(); + + virtual void closeConnection(); // Forced close of connection + + void post( const KURL& url ); + void multiGet(const QByteArray &data); + bool checkRequestURL( const KURL& ); + void cacheUpdate( const KURL &url, bool nocache, time_t expireDate); + + void httpError(); // Generate error message based on response code + + bool isOffline(const KURL &url); // Check network status + +protected slots: + void slotData(const QByteArray &); + void error( int _errid, const QString &_text ); + +protected: + int readChunked(); // Read a chunk + int readLimited(); // Read maximum m_iSize bytes. + int readUnlimited(); // Read as much as possible. + + /** + * A "smart" wrapper around write that will use SSL_write or + * write(2) depending on whether you've got an SSL connection or not. + * The only shortcomming is that it uses the "global" file handles and + * soforth. So you can't really use this on individual files/sockets. + */ + ssize_t write(const void *buf, size_t nbytes); + + /** + * Another "smart" wrapper, this time around read that will + * use SSL_read or read(2) depending on whether you've got an + * SSL connection or not. + */ + ssize_t read (void *b, size_t nbytes); + + char *gets (char *str, int size); + + void setRewindMarker(); + void rewind(); + + /** + * Add an encoding on to the appropriate stack this + * is nececesary because transfer encodings and + * content encodings must be handled separately. + */ + void addEncoding(QString, QStringList &); + + void configAuth( char *, bool ); + + bool httpOpen(); // Open transfer + void httpClose(bool keepAlive); // Close transfer + + bool httpOpenConnection(); // Open connection + void httpCloseConnection(); // Close connection + void httpCheckConnection(); // Check whether to keep connection. + + void forwardHttpResponseHeader(); + + bool readHeader(); + + bool sendBody(); + + // where dataInternal == true, the content is to be made available + // to an internal function. + bool readBody( bool dataInternal = false ); + + /** + * Performs a WebDAV stat or list + */ + void davSetRequest( const QCString& requestXML ); + void davStatList( const KURL& url, bool stat = true ); + void davParsePropstats( const QDomNodeList& propstats, KIO::UDSEntry& entry ); + void davParseActiveLocks( const QDomNodeList& activeLocks, + uint& lockCount ); + + /** + * Parses a date & time string + */ + long parseDateTime( const QString& input, const QString& type ); + + /** + * Returns the error code from a "HTTP/1.1 code Code Name" string + */ + int codeFromResponse( const QString& response ); + + /** + * Extracts locks from metadata + * Returns the appropriate If: header + */ + QString davProcessLocks(); + + /** + * Send a cookie to the cookiejar + */ + void addCookies( const QString &url, const QCString &cookieHeader); + + /** + * Look for cookies in the cookiejar + */ + QString findCookies( const QString &url); + + /** + * Do a cache lookup for the current url. (m_state.url) + * + * @param readWrite If true, file is opened read/write. + * If false, file is opened read-only. + * + * @return a file stream open for reading and at the start of + * the header section when the Cache entry exists and is valid. + * 0 if no cache entry could be found, or if the entry is not + * valid (any more). + */ + FILE *checkCacheEntry(bool readWrite = false); + + /** + * Create a cache entry for the current url. (m_state.url) + * + * Set the contents type of the cache entry to 'mimetype'. + */ + void createCacheEntry(const QString &mimetype, time_t expireDate); + + /** + * Write data to cache. + * + * Write 'nbytes' from 'buffer' to the Cache Entry File + */ + void writeCacheEntry( const char *buffer, int nbytes); + + /** + * Close cache entry + */ + void closeCacheEntry(); + + /** + * Update expire time of current cache entry. + */ + void updateExpireDate(time_t expireDate, bool updateCreationDate=false); + + /** + * Quick check whether the cache needs cleaning. + */ + void cleanCache(); + + /** + * Performs a GET HTTP request. + */ + // where dataInternal == true, the content is to be made available + // to an internal function. + void retrieveContent( bool dataInternal = false ); + + /** + * Performs a HEAD HTTP request. + */ + bool retrieveHeader(bool close_connection = true); + + /** + * Resets any per session settings. + */ + void resetSessionSettings(); + + /** + * Resets settings related to parsing a response. + */ + void resetResponseSettings(); + + /** + * Resets any per connection settings. These are different from + * per-session settings in that they must be invalidates every time + * a request is made, e.g. a retry to re-send the header to the + * server, as compared to only when a new request arrives. + */ + void resetConnectionSettings(); + + /** + * Returns any pre-cached proxy authentication info + * info in HTTP header format. + */ + QString proxyAuthenticationHeader(); + + /** + * Retrieves authorization info from cache or user. + */ + bool getAuthorization(); + + /** + * Saves valid authorization info in the cache daemon. + */ + void saveAuthorization(); + + /** + * Creates the entity-header for Basic authentication. + */ + QString createBasicAuth( bool isForProxy = false ); + + /** + * Creates the entity-header for Digest authentication. + */ + QString createDigestAuth( bool isForProxy = false ); + + /** + * Creates the entity-header for NTLM authentication. + */ + QString createNTLMAuth( bool isForProxy = false ); + + /** + * Creates the entity-header for Negotiate authentication. + */ + QString createNegotiateAuth(); + + /** + * create GSS error string + */ + QCString gssError( int major_status, int minor_status ); + + /** + * Calcualtes the message digest response based on RFC 2617. + */ + void calculateResponse( DigestAuthInfo &info, QCString &Response ); + + /** + * Prompts the user for authorization retry. + */ + bool retryPrompt(); + + /** + * Creates authorization prompt info. + */ + void promptInfo( KIO::AuthInfo& info ); + +protected: + HTTPState m_state; + HTTPRequest m_request; + QPtrList<HTTPRequest> m_requestQueue; + + bool m_bBusy; // Busy handling request queue. + bool m_bEOF; + bool m_bEOD; + +//--- Settings related to a single response only + QStringList m_responseHeader; // All headers + KURL m_redirectLocation; + bool m_bRedirect; // Indicates current request is a redirection + + // Processing related + bool m_bChunked; // Chunked tranfer encoding + KIO::filesize_t m_iSize; // Expected size of message + KIO::filesize_t m_iBytesLeft; // # of bytes left to receive in this message. + KIO::filesize_t m_iContentLeft; // # of content bytes left + QByteArray m_bufReceive; // Receive buffer + bool m_dataInternal; // Data is for internal consumption + char m_lineBuf[1024]; + char m_rewindBuf[8192]; + size_t m_rewindCount; + char *m_linePtr; + size_t m_lineCount; + char *m_lineBufUnget; + char *m_linePtrUnget; + size_t m_lineCountUnget; + + // Mimetype determination + bool m_cpMimeBuffer; + QByteArray m_mimeTypeBuffer; + + // Language/Encoding related + QStringList m_qTransferEncodings; + QStringList m_qContentEncodings; + QString m_sContentMD5; + QString m_strMimeType; + + +//--- WebDAV + // Data structure to hold data which will be passed to an internal func. + QByteArray m_bufWebDavData; + QStringList m_davCapabilities; + + bool m_davHostOk; + bool m_davHostUnsupported; +//---------- + + // Holds the POST data so it won't get lost on if we + // happend to get a 401/407 response when submitting, + // a form. + QByteArray m_bufPOST; + + // Cache related + int m_maxCacheAge; // Maximum age of a cache entry. + long m_maxCacheSize; // Maximum cache size in Kb. + QString m_strCacheDir; // Location of the cache. + + + +//--- Proxy related members + bool m_bUseProxy; + bool m_bNeedTunnel; // Whether we need to make a SSL tunnel + bool m_bIsTunneled; // Whether we have an active SSL tunnel + bool m_bProxyAuthValid; + int m_iProxyPort; + KURL m_proxyURL; + QString m_strProxyRealm; + + // Operation mode + QCString m_protocol; + + // Authentication + QString m_strRealm; + QString m_strAuthorization; + QString m_strProxyAuthorization; + HTTP_AUTH Authentication; + HTTP_AUTH ProxyAuthentication; + bool m_bUnauthorized; + short unsigned int m_iProxyAuthCount; + short unsigned int m_iWWWAuthCount; + + // First request on a connection + bool m_bFirstRequest; + + // Persistent connections + bool m_bKeepAlive; + int m_keepAliveTimeout; // Timeout in seconds. + + // Persistent proxy connections + bool m_bPersistentProxyConnection; + + + // Indicates whether there was some connection error. + bool m_bError; + + // Previous and current response codes + unsigned int m_responseCode; + unsigned int m_prevResponseCode; + + // Values that determine the remote connection timeouts. + int m_proxyConnTimeout; + int m_remoteConnTimeout; + int m_remoteRespTimeout; + + int m_pid; +}; +#endif diff --git a/kioslave/http/http.protocol b/kioslave/http/http.protocol new file mode 100644 index 000000000..ea7b57869 --- /dev/null +++ b/kioslave/http/http.protocol @@ -0,0 +1,12 @@ +[Protocol] +exec=kio_http +protocol=http +input=none +output=filesystem +reading=true +defaultMimetype=application/octet-stream +determineMimetypeFromExtension=false +Icon=www +maxInstances=3 +DocPath=kioslave/http.html +Class=:internet diff --git a/kioslave/http/http_cache_cleaner.cpp b/kioslave/http/http_cache_cleaner.cpp new file mode 100644 index 000000000..f7406bcc1 --- /dev/null +++ b/kioslave/http/http_cache_cleaner.cpp @@ -0,0 +1,284 @@ +/* +This file is part of KDE + + Copyright (C) 1999-2000 Waldo Bastian (bastian@kde.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +//---------------------------------------------------------------------------- +// +// KDE Http Cache cleanup tool +// $Id$ + +#include <time.h> +#include <stdlib.h> + +#include <qdir.h> +#include <qstring.h> +#include <qptrlist.h> + +#include <kinstance.h> +#include <klocale.h> +#include <kcmdlineargs.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <dcopclient.h> +#include <kprotocolmanager.h> + +#include <unistd.h> + +#include <kdebug.h> + +time_t currentDate; +int m_maxCacheAge; +int m_maxCacheSize; + +static const char appName[] = "kio_http_cache_cleaner"; + +static const char description[] = I18N_NOOP("KDE HTTP cache maintenance tool"); + +static const char version[] = "1.0.0"; + +static const KCmdLineOptions options[] = +{ + {"clear-all", I18N_NOOP("Empty the cache"), 0}, + KCmdLineLastOption +}; + +struct FileInfo { + QString name; + int size; // Size in Kb. + int age; +}; + +template class QPtrList<FileInfo>; + +class FileInfoList : public QPtrList<FileInfo> +{ +public: + FileInfoList() : QPtrList<FileInfo>() { } + int compareItems(QPtrCollection::Item item1, QPtrCollection::Item item2) + { return ((FileInfo *)item1)->age - ((FileInfo *)item2)->age; } +}; + +// !START OF SYNC! +// Keep the following in sync with the cache code in http.cc +#define CACHE_REVISION "7\n" + +FileInfo *readEntry( const QString &filename) +{ + QCString CEF = QFile::encodeName(filename); + FILE *fs = fopen( CEF.data(), "r"); + if (!fs) + return 0; + + char buffer[401]; + bool ok = true; + + // CacheRevision + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok && (strcmp(buffer, CACHE_REVISION) != 0)) + ok = false; + + // Full URL + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + + time_t creationDate; + int age =0; + + // Creation Date + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + creationDate = (time_t) strtoul(buffer, 0, 10); + age = (int) difftime(currentDate, creationDate); + if ( m_maxCacheAge && ( age > m_maxCacheAge)) + { + ok = false; // Expired + } + } + + // Expiration Date + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { +//WABA: It seems I slightly misunderstood the meaning of "Expire:" header. +#if 0 + time_t expireDate; + expireDate = (time_t) strtoul(buffer, 0, 10); + if (expireDate && (expireDate < currentDate)) + ok = false; // Expired +#endif + } + + // ETag + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + // Ignore ETag + } + + // Last-Modified + if (ok && (!fgets(buffer, 400, fs))) + ok = false; + if (ok) + { + // Ignore Last-Modified + } + + + fclose(fs); + if (ok) + { + FileInfo *info = new FileInfo; + info->age = age; + return info; + } + + unlink( CEF.data()); + return 0; +} +// Keep the above in sync with the cache code in http.cc +// !END OF SYNC! + +void scanDirectory(FileInfoList &fileEntries, const QString &name, const QString &strDir) +{ + QDir dir(strDir); + if (!dir.exists()) return; + + QFileInfoList *newEntries = (QFileInfoList *) dir.entryInfoList(); + + if (!newEntries) return; // Directory not accessible ?? + + for(QFileInfo *qFileInfo = newEntries->first(); + qFileInfo; + qFileInfo = newEntries->next()) + { + if (qFileInfo->isFile()) + { + FileInfo *fileInfo = readEntry( strDir + "/" + qFileInfo->fileName()); + if (fileInfo) + { + fileInfo->name = name + "/" + qFileInfo->fileName(); + fileInfo->size = (qFileInfo->size() + 1023) / 1024; + fileEntries.append(fileInfo); + } + } + } +} + +extern "C" KDE_EXPORT int kdemain(int argc, char **argv) +{ + KLocale::setMainCatalogue("kdelibs"); + KCmdLineArgs::init( argc, argv, appName, + I18N_NOOP("KDE HTTP cache maintenance tool"), + description, version, true); + + KCmdLineArgs::addCmdLineOptions( options ); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + + bool deleteAll = args->isSet("clear-all"); + + KInstance ins( appName ); + + if (!deleteAll) + { + DCOPClient *dcop = new DCOPClient(); + QCString name = dcop->registerAs(appName, false); + if (!name.isEmpty() && (name != appName)) + { + fprintf(stderr, "%s: Already running! (%s)\n", appName, name.data()); + return 0; + } + } + + currentDate = time(0); + m_maxCacheAge = KProtocolManager::maxCacheAge(); + m_maxCacheSize = KProtocolManager::maxCacheSize(); + + if (deleteAll) + m_maxCacheSize = -1; + + QString strCacheDir = KGlobal::dirs()->saveLocation("cache", "http"); + + QDir cacheDir( strCacheDir ); + if (!cacheDir.exists()) + { + fprintf(stderr, "%s: '%s' does not exist.\n", appName, strCacheDir.ascii()); + return 0; + } + + QStringList dirs = cacheDir.entryList( ); + + FileInfoList cachedEntries; + + for(QStringList::Iterator it = dirs.begin(); + it != dirs.end(); + it++) + { + if ((*it)[0] != '.') + { + scanDirectory( cachedEntries, *it, strCacheDir + "/" + *it); + } + } + + cachedEntries.sort(); + + int maxCachedSize = m_maxCacheSize / 2; + + for(FileInfo *fileInfo = cachedEntries.first(); + fileInfo; + fileInfo = cachedEntries.next()) + { + if (fileInfo->size > maxCachedSize) + { + QCString filename = QFile::encodeName( strCacheDir + "/" + fileInfo->name); + unlink(filename.data()); +// kdDebug () << appName << ": Object too big, deleting '" << filename.data() << "' (" << result<< ")" << endl; + } + } + + int totalSize = 0; + + for(FileInfo *fileInfo = cachedEntries.first(); + fileInfo; + fileInfo = cachedEntries.next()) + { + if ((totalSize + fileInfo->size) > m_maxCacheSize) + { + QCString filename = QFile::encodeName( strCacheDir + "/" + fileInfo->name); + unlink(filename.data()); +// kdDebug () << appName << ": Cache too big, deleting '" << filename.data() << "' (" << fileInfo->size << ")" << endl; + } + else + { + totalSize += fileInfo->size; +// fprintf(stderr, "Keep in cache: %s %d %d total = %d\n", fileInfo->name.ascii(), fileInfo->size, fileInfo->age, totalSize); + } + } + kdDebug () << appName << ": Current size of cache = " << totalSize << " kB." << endl; + return 0; +} + + diff --git a/kioslave/http/http_cache_cleaner.desktop b/kioslave/http/http_cache_cleaner.desktop new file mode 100644 index 000000000..a2e129977 --- /dev/null +++ b/kioslave/http/http_cache_cleaner.desktop @@ -0,0 +1,168 @@ +[Desktop Entry] +Type=Service +Name=HTTP Cache Cleaner +Name[af]=Http Kas Skoonmaker +Name[ar]=مزيل كاش HTTP +Name[az]=HTTP Ön Yaddaş Təmizləyici +Name[be]=Ачыстка кэшу HTTP +Name[bg]=Изчистване на кеш-паметта на HTTP +Name[bn]=এইচ-টি-টি-পি ক্যাশ ক্লীনার +Name[br]=Naeter Krubuilh HTTP +Name[bs]=Čistač HTTP cache-a +Name[ca]=Neteja la memòria cau del HTTP +Name[cs]=Nástroj pro vyprázdnění cache protokolu HTTP +Name[csb]=Czëszczenié cache HTTP +Name[cy]=Glanhauwr Storfa HTTP +Name[da]=HTTP-cache-rydder +Name[de]=Aufräumprogramm für den HTTP-Zwischenspeicher +Name[el]=Καθαριστής λανθάνουσας μνήμης HTTP +Name[eo]=HTTP-Tenejpurigilo +Name[es]=Limpiador del caché de HTTP +Name[et]=HTTP vahemälu puhastaja +Name[eu]=HTTP cache-garbitzailea +Name[fa]=پاککنندۀ نهانگاه قام +Name[fi]=HTTP-välimuistin tyhjentäjä +Name[fr]=Nettoyage du cache HTTP +Name[fy]=HTTP Cache oprommer +Name[ga]=Glantóir Taisce HTTP +Name[gl]=Limpador da caché de HTTP +Name[he]=מנקה מטמון ה־HTTP +Name[hi]=HTTP कैश साफ करने वाला +Name[hr]=Brisanje HTTP pohrane +Name[hu]=HTTP gyorstártisztító +Name[id]=Pembersih Cache HTTP +Name[is]=Hreinsiforrit HTTP skyndiminnis +Name[it]=Ripulitore della cache HTTP +Name[ja]=HTTP キャッシュマネージャ +Name[ka]=HTTP ბუფერის გასუფთავება +Name[kk]=HTTP бүркемесін босату +Name[km]=កម្មវិធីសម្អាតឃ្លាំងសម្ងាត់ HTTP +Name[ko]=HTTP 캐시 정리 +Name[lb]=Opraumer fir den HTTP-Zwëschespäicher +Name[lt]=HTTP krepšio ištuštintojas +Name[lv]=HTTP Kešatmiņas tīrītājs +Name[mk]=Бришење на HTTP-кешот +Name[mn]=HTTP-завсрын хадгалагчийн цэвэрлэгээ +Name[ms]=Pembersih Penyimpan HTTP +Name[mt]=Tindif tal-cache HTTP +Name[nb]=HTTP Mellomlagerrenser +Name[nds]=Reenmaker för HTTP-Twischenspieker +Name[ne]=HTTP क्यास क्लीनर +Name[nl]=HTTP Cache opschonen +Name[nn]=HTTP-mellomlageropprensking +Name[nso]=Sehlwekisi sa Polokelo ya HTTP +Name[oc]=Netejador de cabia HTTP +Name[pa]=HTTP ਕੈਂਚੇ ਸਾਫ਼ +Name[pl]=Czyszczenie bufora HTTP +Name[pt]=Limpeza da Cache de HTTP +Name[pt_BR]=Limpador de cache HTTP +Name[ro]=Curăţător cache HTTP +Name[ru]=Очистка кэша HTTP +Name[rw]=Musukura Ubwihisho HTTP +Name[se]=HTTP gaskarádjosa buhtisteaddji +Name[sk]=Čistič vyrovnávacej pamäti HTTP +Name[sl]=Čistilnik predpomnilnika HTTP +Name[sq]=Pastrues për Depon e Fshehtësitëve të HTTP +Name[sr]=Чистач HTTP кеша +Name[sr@Latn]=Čistač HTTP keša +Name[sv]=HTTP-cacherensare +Name[ta]=HTTP தற்காலிக நினைவகத்தை சுத்தம் செய்தல் +Name[te]=హెచ్ టిటిపి కోశం శుభ్రంచేసేది +Name[tg]=HTTP Софкунаки Махфӣ +Name[th]=ตัวล้างแคช HTTP +Name[tr]=HTTP Önbellek Temizleyici +Name[tt]=HTTP Alxäteren Buşatqıç +Name[uk]=Очищувач кешу HTTP +Name[uz]=HTTP kesh boʻshatgich +Name[uz@cyrillic]=HTTP кэш бўшатгич +Name[ven]=Tshikulumagi tsha HTTP Cache +Name[vi]=Bộ làm sạch bộ nhớ tạm HTTP +Name[xh]=Umcoci wendawo efihlakeleyo yokugcina we HTTP +Name[zh_CN]=HTTP 缓存清除程序 +Name[zh_HK]=HTTP 快取清除程式 +Name[zh_TW]=HTTP 快取清除程式 +Name[zu]=Umhlanzi we-Cache ye-HTTP +Exec=kio_http_cache_cleaner +Comment=Cleans up old entries from the HTTP cache +Comment[af]=Skoonmaak begin ou inskrywings van die Http kas +Comment[ar]=يزيل المداخل القديمة من كاش HTTP +Comment[az]=HTTP ön yaddaşından köhnə girişləri silər +Comment[be]=Выдаляе старыя запісы з кэшу HTTP +Comment[bg]=Изчистване на старите данни в кеш-паметта на HTTP +Comment[bn]=HTTP ক্যাশ থেকে পুরনো তথ্য মুছে ফেলে +Comment[br]=Skarañ enmontoù kozh diwar ar grubuilh HTTP +Comment[bs]=Čisti stare datoteke iz HTTP cache-a +Comment[ca]=Neteja les entrades antigues de la memòria cau del HTTP +Comment[cs]=Odstraňuje staré položky z HTTP cache +Comment[csb]=Rëmô stôré wpisënczi z cache HTTP +Comment[cy]=Glanhau'r hen gofnodion o'r storfa HTTP +Comment[da]=Rydder op i gamle indgange fra HTTP-cachen +Comment[de]=Löscht alte Einträge aus dem HTTP-Zwischenspeicher +Comment[el]=Καθαρίζει παλιές καταχωρήσεις από τη λανθάνουσα μνήμη HTTP +Comment[eo]=Forigas malnovajn erojn el HTTP-tenejo +Comment[es]=Elimina entradas antiguas del caché de HTTP +Comment[et]=Puhastab HTTP vahemälu vanadest kirjetest +Comment[eu]=HTTP cachearen sarrera zaharrak garbitzen ditu +Comment[fa]=مدخلهای قدیمی را از نهانگاه قام پاک میکند +Comment[fi]=Puhdistaa vanhat tiedot HTTP-välimuistista +Comment[fr]=Efface les anciennes entrées du cache HTTP +Comment[fy]=Ferwidert âlde items út de HTTP-cache +Comment[ga]=Glanann seaniontrálacha ón taisce HTTP +Comment[gl]=Elimina as entradas antigas da caché de HTTP +Comment[he]=מנקה רשומות ישנות ממטמון ה־HTTP +Comment[hi]=HTTP कैश से पुरानी प्रविष्टि साफ करे +Comment[hr]=Uklanjanje starih datoteka iz HTTP privremene lokalne pohrane +Comment[hu]=Kitörli a régi bejegyzéseket a HTTP gyorstárból +Comment[id]=Membersihkan entri lama dari cache HTTP +Comment[is]=Hreinsar gamlar færslur úr HTTP skyndiminninu +Comment[it]=Ripulisce la cache HTTP dalle voci vecchie +Comment[ja]=HTTP キャッシュから古いエントリを削除します +Comment[ka]=HTTP ბუფერის მოძველებელი ელემენტების +Comment[kk]=HTTP бүркемесін ескі жазулардан тазалау +Comment[km]=សម្អាតធាតុចាស់ៗពីឃ្លាំងសម្ងាត់ HTTP +Comment[ko]=HTTP 캐시에서 오래된 것들을 정리합니다 +Comment[lb]=Entfernt al Entréen aus dem HTTP-Zwëschespäicher +Comment[lt]=Išvalo senus įrašus iš HTTP krepšio +Comment[lv]=Iztīra vecos ierakstus no HTTP kešatmiņas +Comment[mk]=Ги брише старите работи од HTTP кешот +Comment[mn]=HTTP-завсрын хадгалагчаас хуучин бичлэгийг устгах +Comment[ms]=Membersihkan masukan lama daripada penyimpan HTTP +Comment[mt]=Ineħħi fajls antiki mill-cache tal-HTTP +Comment[nb]=Fjerner gamle oppføringer fra hurtiglageret for HTTP +Comment[nds]=Smitt ole Indrääg ut den HTTP-Twischenspieker rut +Comment[ne]=HTTP क्यासबाट पुराना प्रविष्टिहरू सफा गर्दछ +Comment[nl]=Verwijdert oude items uit de HTTP-cache +Comment[nn]=Reinskar opp i gamle oppføringar i HTTP-mellomlageret +Comment[nso]=E hlwekisa ditsenyo tsa kgale gotswa polokelong ya HTTP +Comment[oc]=Neteja les entrades antigues dèu cabia HTTP +Comment[pa]=HTTP ਕੈਂਚੇ ਤੋਂ ਪੁਰਾਣੀਆਂ ਇਕਾਈਆਂ ਸਾਫ +Comment[pl]=Usuwa stare wpisy z bufora HTTP +Comment[pt]=Limpa o conteúdo desactualizado da cache do HTTP +Comment[pt_BR]=Limpa itens velhos do cache HTTP +Comment[ro]=Elimină înregistrările vechi din cache-ul HTTP +Comment[ru]=Удаление устаревших элементов из кэша HTTP +Comment[rw]=Isukura ibyinjijwe bishaje biri mu bwihisho HTTP +Comment[se]=Buhtista boares merko3/4iid HTTP gaskarádjosis +Comment[sk]=Vyčistiť staré záznamy z vyrovnávacej pamäti HTTP +Comment[sl]=Zbriše stare vnose iz pomnilnika HTTP +Comment[sq]=I pastron hyrjet e vjetra nga depoja e fshehtësive të HTTP +Comment[sr]=Чисти старе ставке из HTTP кеша +Comment[sr@Latn]=Čisti stare stavke iz HTTP keša +Comment[sv]=Rensar bort gamla poster från HTTP-cachen +Comment[ta]=HTTP நினைவத்திலிருந்து பழைய உள்ளீடுகளை சுத்தம் செய்கிறது +Comment[te]=హెచ్ టిటిపి కోశం నుంచి పాత ఆరొపములను శుభ్రం చేసేది +Comment[tg]=Ёддоштҳои Кӯҳна аз HTTP Махфӣ Тоза Кунед +Comment[th]=ล้างรายการเก่าๆ จากแคช HTTP +Comment[tr]=HTTP önbelleğinden eski girişleri siler +Comment[tt]=HTTP alxäterendä bulğan iske keremnär beterä +Comment[uk]=Вичищає старі елементи з кешу HTTP +Comment[uz]=HTTP keshidagi eski elementlarni oʻchiradi +Comment[uz@cyrillic]=HTTP кэшидаги эски элементларни ўчиради +Comment[ven]=I kulumaga zwithu zwakale u bva kha HTTP cache +Comment[vi]=Xoá sạch các mục nhập cũ ra bộ nhớ tạm HTTP. +Comment[xh]=Icoca amangeno amadala asuka kwindawo efihlakeleyo yokugcina ye HTTP +Comment[zh_CN]=从 HTTP 缓存中清除旧条目 +Comment[zh_HK]=從 HTTP 快取中清除舊的項目 +Comment[zh_TW]=從 HTTP 快取中清除舊的項目 +Comment[zu]=Ihlanza izingeniso ezindalam ezisuka kwi-cache ye-HTTP +X-KDE-StartupNotify=false diff --git a/kioslave/http/https.protocol b/kioslave/http/https.protocol new file mode 100644 index 000000000..8a9c2f0da --- /dev/null +++ b/kioslave/http/https.protocol @@ -0,0 +1,12 @@ +[Protocol] +exec=kio_http +protocol=https +input=none +output=filesystem +reading=true +defaultMimetype=application/octet-stream +determineMimetypeFromExtension=false +Icon=www +config=http +DocPath=kioslave/https.html +Class=:internet diff --git a/kioslave/http/kcookiejar/Makefile.am b/kioslave/http/kcookiejar/Makefile.am new file mode 100644 index 000000000..933de5e13 --- /dev/null +++ b/kioslave/http/kcookiejar/Makefile.am @@ -0,0 +1,31 @@ +# Makefile.am of kdebase/kioslave/http + +SUBDIRS=tests +INCLUDES= $(all_includes) + +####### Files + +bin_PROGRAMS = +lib_LTLIBRARIES = +kdeinit_LTLIBRARIES = kcookiejar.la +kde_module_LTLIBRARIES = kded_kcookiejar.la + +kcookiejar_la_SOURCES = main.cpp +METASOURCES = AUTO +kcookiejar_la_LDFLAGS = $(all_libraries) -module -avoid-version +kcookiejar_la_LIBADD = $(LIB_KDECORE) + +kded_kcookiejar_la_SOURCES = kcookiejar.cpp kcookieserver.cpp \ + kcookieserver.skel kcookiewin.cpp +kded_kcookiejar_la_LDFLAGS = $(all_libraries) -module -avoid-version +kded_kcookiejar_la_LIBADD = $(LIB_KIO) $(LIB_KDED) + +kded_DATA = kcookiejar.desktop +kdeddir = $(kde_servicesdir)/kded + +update_DATA = kcookiescfg.upd +updatedir = $(kde_datadir)/kconf_update + +cookie_DATA = domain_info +cookiedir = $(kde_datadir)/khtml + diff --git a/kioslave/http/kcookiejar/domain_info b/kioslave/http/kcookiejar/domain_info new file mode 100644 index 000000000..94baf8dae --- /dev/null +++ b/kioslave/http/kcookiejar/domain_info @@ -0,0 +1 @@ +twoLevelTLD=name,ai,au,bd,bh,ck,eg,et,fk,il,in,kh,kr,mk,mt,na,np,nz,pg,pk,qa,sa,sb,sg,sv,ua,ug,uk,uy,vn,za,zw diff --git a/kioslave/http/kcookiejar/kcookiejar.cpp b/kioslave/http/kcookiejar/kcookiejar.cpp new file mode 100644 index 000000000..5b5f78f6b --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiejar.cpp @@ -0,0 +1,1558 @@ +/* This file is part of the KDE File Manager + + Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) + Copyright (C) 2000,2001 Dawit Alemayehu (adawit@kde.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, and/or sell copies of the + Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +//---------------------------------------------------------------------------- +// +// KDE File Manager -- HTTP Cookies +// $Id$ + +// +// The cookie protocol is a mess. RFC2109 is a joke since nobody seems to +// use it. Apart from that it is badly written. +// We try to implement Netscape Cookies and try to behave us according to +// RFC2109 as much as we can. +// +// We assume cookies do not contain any spaces (Netscape spec.) +// According to RFC2109 this is allowed though. +// + +#include <config.h> +#include <sys/types.h> +#include <sys/stat.h> +#ifdef HAVE_SYS_PARAM_H +#include <sys/param.h> +#endif +#include <fcntl.h> +#include <unistd.h> +#include <stdio.h> +#include <string.h> + +#ifdef USE_SOLARIS +#include <strings.h> +#endif + +#include <stdlib.h> + +//#include <netinet/in.h> +//#include <arpa/inet.h> + +#include <qstring.h> +#include <qstrlist.h> +#include <qptrlist.h> +#include <qptrdict.h> +#include <qfile.h> +#include <qdir.h> +#include <qregexp.h> + +#include <kurl.h> +#include <krfcdate.h> +#include <kconfig.h> +#include <ksavefile.h> +#include <kdebug.h> + +#include "kcookiejar.h" + + +// BR87227 +// Waba: Should the number of cookies be limited? +// I am not convinced of the need of such limit +// Mozilla seems to limit to 20 cookies / domain +// but it is unclear which policy it uses to expire +// cookies when it exceeds that amount +#undef MAX_COOKIE_LIMIT + +#define MAX_COOKIES_PER_HOST 25 +#define READ_BUFFER_SIZE 8192 +#define IP_ADDRESS_EXPRESSION "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + +// Note with respect to QString::fromLatin1( ) +// Cookies are stored as 8 bit data and passed to kio_http as +// latin1 regardless of their actual encoding. + +// L1 is used to indicate latin1 constants +#define L1(x) QString::fromLatin1(x) + +template class QPtrList<KHttpCookie>; +template class QPtrDict<KHttpCookieList>; + +QString KCookieJar::adviceToStr(KCookieAdvice _advice) +{ + switch( _advice ) + { + case KCookieAccept: return L1("Accept"); + case KCookieReject: return L1("Reject"); + case KCookieAsk: return L1("Ask"); + default: return L1("Dunno"); + } +} + +KCookieAdvice KCookieJar::strToAdvice(const QString &_str) +{ + if (_str.isEmpty()) + return KCookieDunno; + + QCString advice = _str.lower().latin1(); + + if (advice == "accept") + return KCookieAccept; + else if (advice == "reject") + return KCookieReject; + else if (advice == "ask") + return KCookieAsk; + + return KCookieDunno; +} + +// KHttpCookie +/////////////////////////////////////////////////////////////////////////// + +// +// Cookie constructor +// +KHttpCookie::KHttpCookie(const QString &_host, + const QString &_domain, + const QString &_path, + const QString &_name, + const QString &_value, + time_t _expireDate, + int _protocolVersion, + bool _secure, + bool _httpOnly, + bool _explicitPath) : + mHost(_host), + mDomain(_domain), + mPath(_path.isEmpty() ? QString::null : _path), + mName(_name), + mValue(_value), + mExpireDate(_expireDate), + mProtocolVersion(_protocolVersion), + mSecure(_secure), + mHttpOnly(_httpOnly), + mExplicitPath(_explicitPath) +{ +} + +// +// Checks if a cookie has been expired +// +bool KHttpCookie::isExpired(time_t currentDate) +{ + return (mExpireDate != 0) && (mExpireDate < currentDate); +} + +// +// Returns a string for a HTTP-header +// +QString KHttpCookie::cookieStr(bool useDOMFormat) +{ + QString result; + + if (useDOMFormat || (mProtocolVersion == 0)) + { + if ( !mName.isEmpty() ) + result = mName + '='; + result += mValue; + } + else + { + result = mName + '=' + mValue; + if (mExplicitPath) + result += L1("; $Path=\"") + mPath + L1("\""); + if (!mDomain.isEmpty()) + result += L1("; $Domain=\"") + mDomain + L1("\""); + } + return result; +} + +// +// Returns whether this cookie should be send to this location. +bool KHttpCookie::match(const QString &fqdn, const QStringList &domains, + const QString &path) +{ + // Cookie domain match check + if (mDomain.isEmpty()) + { + if (fqdn != mHost) + return false; + } + else if (!domains.contains(mDomain)) + { + if (mDomain[0] == '.') + return false; + + // Maybe the domain needs an extra dot. + QString domain = '.' + mDomain; + if ( !domains.contains( domain ) ) + if ( fqdn != mDomain ) + return false; + } + + // Cookie path match check + if (mPath.isEmpty()) + return true; + + // According to the netscape spec both http://www.acme.com/foobar, + // http://www.acme.com/foo.bar and http://www.acme.com/foo/bar + // match http://www.acme.com/foo. + // We only match http://www.acme.com/foo/bar + + if( path.startsWith(mPath) && + ( + (path.length() == mPath.length() ) || // Paths are exact match + (path[mPath.length()-1] == '/') || // mPath ended with a slash + (path[mPath.length()] == '/') // A slash follows. + )) + return true; // Path of URL starts with cookie-path + + return false; +} + +// KHttpCookieList +/////////////////////////////////////////////////////////////////////////// + +int KHttpCookieList::compareItems( void * item1, void * item2) +{ + int pathLen1 = ((KHttpCookie *)item1)->path().length(); + int pathLen2 = ((KHttpCookie *)item2)->path().length(); + if (pathLen1 > pathLen2) + return -1; + if (pathLen1 < pathLen2) + return 1; + return 0; +} + + +// KCookieJar +/////////////////////////////////////////////////////////////////////////// + +// +// Constructs a new cookie jar +// +// One jar should be enough for all cookies. +// +KCookieJar::KCookieJar() +{ + m_cookieDomains.setAutoDelete( true ); + m_globalAdvice = KCookieDunno; + m_configChanged = false; + m_cookiesChanged = false; + + KConfig cfg("khtml/domain_info", true, false, "data"); + QStringList countries = cfg.readListEntry("twoLevelTLD"); + for(QStringList::ConstIterator it = countries.begin(); + it != countries.end(); ++it) + { + m_twoLevelTLD.replace(*it, (int *) 1); + } +} + +// +// Destructs the cookie jar +// +// Poor little cookies, they will all be eaten by the cookie monster! +// +KCookieJar::~KCookieJar() +{ + // Not much to do here +} + +static void removeDuplicateFromList(KHttpCookieList *list, KHttpCookie *cookiePtr, bool nameMatchOnly=false, bool updateWindowId=false) +{ + QString domain1 = cookiePtr->domain(); + if (domain1.isEmpty()) + domain1 = cookiePtr->host(); + + for ( KHttpCookiePtr cookie=list->first(); cookie != 0; ) + { + QString domain2 = cookie->domain(); + if (domain2.isEmpty()) + domain2 = cookie->host(); + + if ( + (cookiePtr->name() == cookie->name()) && + ( + nameMatchOnly || + ( (domain1 == domain2) && (cookiePtr->path() == cookie->path()) ) + ) + ) + { + if (updateWindowId) + { + for(QValueList<long>::ConstIterator it = cookie->windowIds().begin(); + it != cookie->windowIds().end(); ++it) + { + long windowId = *it; + if (windowId && (cookiePtr->windowIds().find(windowId) == cookiePtr->windowIds().end())) + { + cookiePtr->windowIds().append(windowId); + } + } + } + KHttpCookiePtr old_cookie = cookie; + cookie = list->next(); + list->removeRef( old_cookie ); + break; + } + else + { + cookie = list->next(); + } + } +} + + +// +// Looks for cookies in the cookie jar which are appropriate for _url. +// Returned is a string containing all appropriate cookies in a format +// which can be added to a HTTP-header without any additional processing. +// +QString KCookieJar::findCookies(const QString &_url, bool useDOMFormat, long windowId, KHttpCookieList *pendingCookies) +{ + QString cookieStr; + QStringList domains; + QString fqdn; + QString path; + KHttpCookiePtr cookie; + KCookieAdvice advice = m_globalAdvice; + + if (!parseURL(_url, fqdn, path)) + return cookieStr; + + bool secureRequest = (_url.find( L1("https://"), 0, false) == 0 || + _url.find( L1("webdavs://"), 0, false) == 0); + + // kdDebug(7104) << "findCookies: URL= " << _url << ", secure = " << secureRequest << endl; + + extractDomains(fqdn, domains); + + KHttpCookieList allCookies; + + for(QStringList::ConstIterator it = domains.begin(); + true; + ++it) + { + KHttpCookieList *cookieList; + if (it == domains.end()) + { + cookieList = pendingCookies; // Add pending cookies + pendingCookies = 0; + if (!cookieList) + break; + } + else + { + QString key = (*it).isNull() ? L1("") : (*it); + cookieList = m_cookieDomains[key]; + if (!cookieList) + continue; // No cookies for this domain + } + + if (cookieList->getAdvice() != KCookieDunno) + advice = cookieList->getAdvice(); + + for ( cookie=cookieList->first(); cookie != 0; cookie=cookieList->next() ) + { + // If the we are setup to automatically accept all session cookies and to + // treat all cookies as session cookies or the current cookie is a session + // cookie, then send the cookie back regardless of either policy. + if (advice == KCookieReject && + !(m_autoAcceptSessionCookies && + (m_ignoreCookieExpirationDate || cookie->expireDate() == 0))) + continue; + + if (!cookie->match(fqdn, domains, path)) + continue; + + if( cookie->isSecure() && !secureRequest ) + continue; + + if( cookie->isHttpOnly() && useDOMFormat ) + continue; + + // Do not send expired cookies. + if ( cookie->isExpired (time(0)) ) + { + // Note there is no need to actually delete the cookie here + // since the cookieserver will invoke ::saveCookieJar because + // of the state change below. This will then do the job of + // deleting the cookie for us. + m_cookiesChanged = true; + continue; + } + + if (windowId && (cookie->windowIds().find(windowId) == cookie->windowIds().end())) + { + cookie->windowIds().append(windowId); + } + + if (it == domains.end()) // Only needed when processing pending cookies + removeDuplicateFromList(&allCookies, cookie); + + allCookies.append(cookie); + } + if (it == domains.end()) + break; // Finished. + } + + int cookieCount = 0; + + int protVersion=0; + for ( cookie=allCookies.first(); cookie != 0; cookie=allCookies.next() ) + { + if (cookie->protocolVersion() > protVersion) + protVersion = cookie->protocolVersion(); + } + + for ( cookie=allCookies.first(); cookie != 0; cookie=allCookies.next() ) + { + if (useDOMFormat) + { + if (cookieCount > 0) + cookieStr += L1("; "); + cookieStr += cookie->cookieStr(true); + } + else + { + if (cookieCount == 0) + { + cookieStr += L1("Cookie: "); + if (protVersion > 0) + { + QString version; + version.sprintf("$Version=%d; ", protVersion); // Without quotes + cookieStr += version; + } + } + else + { + cookieStr += L1("; "); + } + cookieStr += cookie->cookieStr(false); + } + cookieCount++; + } + + return cookieStr; +} + +// +// This function parses a string like 'my_name="my_value";' and returns +// 'my_name' in Name and 'my_value' in Value. +// +// A pointer to the end of the parsed part is returned. +// This pointer points either to: +// '\0' - The end of the string has reached. +// ';' - Another my_name="my_value" pair follows +// ',' - Another cookie follows +// '\n' - Another header follows +static const char * parseNameValue(const char *header, + QString &Name, + QString &Value, + bool keepQuotes=false, + bool rfcQuotes=false) +{ + const char *s = header; + // Parse 'my_name' part + for(; (*s != '='); s++) + { + if ((*s=='\0') || (*s==';') || (*s=='\n')) + { + // No '=' sign -> use string as the value, name is empty + // (behavior found in Mozilla and IE) + Name = ""; + Value = QString::fromLatin1(header); + Value.truncate( s - header ); + Value = Value.stripWhiteSpace(); + return (s); + } + } + + Name = header; + Name.truncate( s - header ); + Name = Name.stripWhiteSpace(); + + // *s == '=' + s++; + + // Skip any whitespace + for(; (*s == ' ') || (*s == '\t'); s++) + { + if ((*s=='\0') || (*s==';') || (*s=='\n')) + { + // End of Name + Value = ""; + return (s); + } + } + + if ((rfcQuotes || !keepQuotes) && (*s == '\"')) + { + // Parse '"my_value"' part (quoted value) + if (keepQuotes) + header = s++; + else + header = ++s; // skip " + for(;(*s != '\"');s++) + { + if ((*s=='\0') || (*s=='\n')) + { + // End of Name + Value = QString::fromLatin1(header); + Value.truncate(s - header); + return (s); + } + } + Value = QString::fromLatin1(header); + // *s == '\"'; + if (keepQuotes) + Value.truncate( ++s - header ); + else + Value.truncate( s++ - header ); + + // Skip any remaining garbage + for(;; s++) + { + if ((*s=='\0') || (*s==';') || (*s=='\n')) + break; + } + } + else + { + // Parse 'my_value' part (unquoted value) + header = s; + while ((*s != '\0') && (*s != ';') && (*s != '\n')) + s++; + // End of Name + Value = QString::fromLatin1(header); + Value.truncate( s - header ); + Value = Value.stripWhiteSpace(); + } + return (s); + +} + +void KCookieJar::stripDomain(const QString &_fqdn, QString &_domain) +{ + QStringList domains; + extractDomains(_fqdn, domains); + if (domains.count() > 3) + _domain = domains[3]; + else + _domain = domains[0]; +} + +QString KCookieJar::stripDomain( KHttpCookiePtr cookiePtr) +{ + QString domain; // We file the cookie under this domain. + if (cookiePtr->domain().isEmpty()) + stripDomain( cookiePtr->host(), domain); + else + stripDomain (cookiePtr->domain(), domain); + return domain; +} + +bool KCookieJar::parseURL(const QString &_url, + QString &_fqdn, + QString &_path) +{ + KURL kurl(_url); + if (!kurl.isValid()) + return false; + + _fqdn = kurl.host().lower(); + if (kurl.port()) + { + if (((kurl.protocol() == L1("http")) && (kurl.port() != 80)) || + ((kurl.protocol() == L1("https")) && (kurl.port() != 443))) + { + _fqdn = L1("%1:%2").arg(kurl.port()).arg(_fqdn); + } + } + + // Cookie spoofing protection. Since there is no way a path separator + // or escape encoded character is allowed in the hostname according + // to RFC 2396, reject attempts to include such things there! + if(_fqdn.find('/') > -1 || _fqdn.find('%') > -1) + { + return false; // deny everything!! + } + + _path = kurl.path(); + if (_path.isEmpty()) + _path = L1("/"); + + QRegExp exp(L1("[\\\\/]\\.\\.[\\\\/]")); + // Weird path, cookie stealing attempt? + if (exp.search(_path) != -1) + return false; // Deny everything!! + + return true; +} + +void KCookieJar::extractDomains(const QString &_fqdn, + QStringList &_domains) +{ + // Return numeric IPv6 addresses as is... + if (_fqdn[0] == '[') + { + _domains.append( _fqdn ); + return; + } + // Return numeric IPv4 addresses as is... + if ((_fqdn[0] >= '0') && (_fqdn[0] <= '9')) + { + if (_fqdn.find(QRegExp(IP_ADDRESS_EXPRESSION)) > -1) + { + _domains.append( _fqdn ); + return; + } + } + + QStringList partList = QStringList::split('.', _fqdn, false); + + if (partList.count()) + partList.remove(partList.begin()); // Remove hostname + + while(partList.count()) + { + + if (partList.count() == 1) + break; // We only have a TLD left. + + if ((partList.count() == 2) && (m_twoLevelTLD[partList[1].lower()])) + { + // This domain uses two-level TLDs in the form xxxx.yy + break; + } + + if ((partList.count() == 2) && (partList[1].length() == 2)) + { + // If this is a TLD, we should stop. (e.g. co.uk) + // We assume this is a TLD if it ends with .xx.yy or .x.yy + if (partList[0].length() <= 2) + break; // This is a TLD. + + // Catch some TLDs that we miss with the previous check + // e.g. com.au, org.uk, mil.co + QCString t = partList[0].lower().utf8(); + if ((t == "com") || (t == "net") || (t == "org") || (t == "gov") || (t == "edu") || (t == "mil") || (t == "int")) + break; + } + + QString domain = partList.join(L1(".")); + _domains.append(domain); + _domains.append('.' + domain); + partList.remove(partList.begin()); // Remove part + } + + // Always add the FQDN at the start of the list for + // hostname == cookie-domainname checks! + _domains.prepend( '.' + _fqdn ); + _domains.prepend( _fqdn ); +} + + +/* + Changes dates in from the following format + + Wed Sep 12 07:00:00 2007 GMT + to + Wed Sep 12 2007 07:00:00 GMT + + to allow KRFCDate::parseDate to properly parse expiration date formats + used in cookies by some servers such as amazon.com. See BR# 145244. +*/ +static QString fixupDateTime(const QString& dt) +{ + const int index = dt.find(QRegExp("[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}")); + + if (index > -1) + { + QStringList dateStrList = QStringList::split(' ', dt.mid(index)); + if (dateStrList.count() > 1) + { + QString date = dateStrList[0]; + dateStrList[0] = dateStrList[1]; + dateStrList[1] = date; + date = dt; + return date.replace(index, date.length(), dateStrList.join(" ")); + } + } + + return dt; +} + +// +// This function parses cookie_headers and returns a linked list of +// KHttpCookie objects for all cookies found in cookie_headers. +// If no cookies could be found 0 is returned. +// +// cookie_headers should be a concatenation of all lines of a HTTP-header +// which start with "Set-Cookie". The lines should be separated by '\n's. +// +KHttpCookieList KCookieJar::makeCookies(const QString &_url, + const QCString &cookie_headers, + long windowId) +{ + KHttpCookieList cookieList; + KHttpCookieList cookieList2; + KHttpCookiePtr lastCookie = 0; + const char *cookieStr = cookie_headers.data(); + QString Name; + QString Value; + QString fqdn; + QString path; + bool crossDomain = false; + + if (!parseURL(_url, fqdn, path)) + { + // Error parsing _url + return KHttpCookieList(); + } + QString defaultPath; + int i = path.findRev('/'); + if (i > 0) + defaultPath = path.left(i); + + // The hard stuff :) + for(;;) + { + // check for "Set-Cookie" + if (strncmp(cookieStr, "Cross-Domain\n", 13) == 0) + { + cookieStr += 13; + crossDomain = true; + } + else if (strncasecmp(cookieStr, "Set-Cookie:", 11) == 0) + { + cookieStr = parseNameValue(cookieStr+11, Name, Value, true); + + // Host = FQDN + // Default domain = "" + // Default path according to rfc2109 + + KHttpCookie *cookie = new KHttpCookie(fqdn, L1(""), defaultPath, Name, Value); + if (windowId) + cookie->mWindowIds.append(windowId); + cookie->mCrossDomain = crossDomain; + + // Insert cookie in chain + cookieList.append(cookie); + lastCookie = cookie; + } + else if (strncasecmp(cookieStr, "Set-Cookie2:", 12) == 0) + { + // Attempt to follow rfc2965 + cookieStr = parseNameValue(cookieStr+12, Name, Value, true, true); + + // Host = FQDN + // Default domain = "" + // Default path according to rfc2965 + + KHttpCookie *cookie = new KHttpCookie(fqdn, L1(""), defaultPath, Name, Value); + if (windowId) + cookie->mWindowIds.append(windowId); + cookie->mCrossDomain = crossDomain; + + // Insert cookie in chain + cookieList2.append(cookie); + lastCookie = cookie; + } + else + { + // This is not the start of a cookie header, skip till next line. + while (*cookieStr && *cookieStr != '\n') + cookieStr++; + + if (*cookieStr == '\n') + cookieStr++; + + if (!*cookieStr) + break; // End of cookie_headers + else + continue; // end of this header, continue with next. + } + + while ((*cookieStr == ';') || (*cookieStr == ' ')) + { + cookieStr++; + + // Name-Value pair follows + cookieStr = parseNameValue(cookieStr, Name, Value); + + QCString cName = Name.lower().latin1(); + if (cName == "domain") + { + QString dom = Value.lower(); + // RFC2965 3.2.2: If an explicitly specified value does not + // start with a dot, the user agent supplies a leading dot + if(dom.length() && dom[0] != '.') + dom.prepend("."); + // remove a trailing dot + if(dom.length() > 2 && dom[dom.length()-1] == '.') + dom = dom.left(dom.length()-1); + + if(dom.contains('.') > 1 || dom == ".local") + lastCookie->mDomain = dom; + } + else if (cName == "max-age") + { + int max_age = Value.toInt(); + if (max_age == 0) + lastCookie->mExpireDate = 1; + else + lastCookie->mExpireDate = time(0)+max_age; + } + else if (cName == "expires") + { + // Parse brain-dead netscape cookie-format + lastCookie->mExpireDate = KRFCDate::parseDate(Value); + + // Workaround for servers that send the expiration date in + // 'Wed Sep 12 07:00:00 2007 GMT' format. See BR# 145244. + if (lastCookie->mExpireDate == 0) + lastCookie->mExpireDate = KRFCDate::parseDate(fixupDateTime(Value)); + } + else if (cName == "path") + { + if (Value.isEmpty()) + lastCookie->mPath = QString::null; // Catch "" <> QString::null + else + lastCookie->mPath = KURL::decode_string(Value); + lastCookie->mExplicitPath = true; + } + else if (cName == "version") + { + lastCookie->mProtocolVersion = Value.toInt(); + } + else if ((cName == "secure") || + (cName.isEmpty() && Value.lower() == L1("secure"))) + { + lastCookie->mSecure = true; + } + else if ((cName == "httponly") || + (cName.isEmpty() && Value.lower() == L1("httponly"))) + { + lastCookie->mHttpOnly = true; + } + } + + if (*cookieStr == '\0') + break; // End of header + + // Skip ';' or '\n' + cookieStr++; + } + + // RFC2965 cookies come last so that they override netscape cookies. + while( !cookieList2.isEmpty() && (lastCookie = cookieList2.take(0)) ) + { + removeDuplicateFromList(&cookieList, lastCookie, true); + cookieList.append(lastCookie); + } + + return cookieList; +} + +/** +* Parses cookie_domstr and returns a linked list of KHttpCookie objects. +* cookie_domstr should be a semicolon-delimited list of "name=value" +* pairs. Any whitespace before "name" or around '=' is discarded. +* If no cookies are found, 0 is returned. +*/ +KHttpCookieList KCookieJar::makeDOMCookies(const QString &_url, + const QCString &cookie_domstring, + long windowId) +{ + // A lot copied from above + KHttpCookieList cookieList; + KHttpCookiePtr lastCookie = 0; + + const char *cookieStr = cookie_domstring.data(); + QString Name; + QString Value; + QString fqdn; + QString path; + + if (!parseURL(_url, fqdn, path)) + { + // Error parsing _url + return KHttpCookieList(); + } + + // This time it's easy + while(*cookieStr) + { + cookieStr = parseNameValue(cookieStr, Name, Value); + + // Host = FQDN + // Default domain = "" + // Default path = "" + KHttpCookie *cookie = new KHttpCookie(fqdn, QString::null, QString::null, + Name, Value ); + if (windowId) + cookie->mWindowIds.append(windowId); + + cookieList.append(cookie); + lastCookie = cookie; + + if (*cookieStr != '\0') + cookieStr++; // Skip ';' or '\n' + } + + return cookieList; +} + +#ifdef MAX_COOKIE_LIMIT +static void makeRoom(KHttpCookieList *cookieList, KHttpCookiePtr &cookiePtr) +{ + // Too much cookies: throw one away, try to be somewhat clever + KHttpCookiePtr lastCookie = 0; + for(KHttpCookiePtr cookie = cookieList->first(); cookie; cookie = cookieList->next()) + { + if (cookieList->compareItems(cookie, cookiePtr) < 0) + break; + lastCookie = cookie; + } + if (!lastCookie) + lastCookie = cookieList->first(); + cookieList->removeRef(lastCookie); +} +#endif + +// +// This function hands a KHttpCookie object over to the cookie jar. +// +// On return cookiePtr is set to 0. +// +void KCookieJar::addCookie(KHttpCookiePtr &cookiePtr) +{ + QStringList domains; + KHttpCookieList *cookieList = 0L; + + // We always need to do this to make sure that the + // that cookies of type hostname == cookie-domainname + // are properly removed and/or updated as necessary! + extractDomains( cookiePtr->host(), domains ); + for ( QStringList::ConstIterator it = domains.begin(); + (it != domains.end() && !cookieList); + ++it ) + { + QString key = (*it).isNull() ? L1("") : (*it); + KHttpCookieList *list= m_cookieDomains[key]; + if ( !list ) continue; + + removeDuplicateFromList(list, cookiePtr, false, true); + } + + QString domain = stripDomain( cookiePtr ); + QString key = domain.isNull() ? L1("") : domain; + cookieList = m_cookieDomains[ key ]; + if (!cookieList) + { + // Make a new cookie list + cookieList = new KHttpCookieList(); + cookieList->setAutoDelete(true); + + // All cookies whose domain is not already + // known to us should be added with KCookieDunno. + // KCookieDunno means that we use the global policy. + cookieList->setAdvice( KCookieDunno ); + + m_cookieDomains.insert( domain, cookieList); + + // Update the list of domains + m_domainList.append(domain); + } + + // Add the cookie to the cookie list + // The cookie list is sorted 'longest path first' + if (!cookiePtr->isExpired(time(0))) + { +#ifdef MAX_COOKIE_LIMIT + if (cookieList->count() >= MAX_COOKIES_PER_HOST) + makeRoom(cookieList, cookiePtr); // Delete a cookie +#endif + cookieList->inSort( cookiePtr ); + m_cookiesChanged = true; + } + else + { + delete cookiePtr; + } + cookiePtr = 0; +} + +// +// This function advices whether a single KHttpCookie object should +// be added to the cookie jar. +// +KCookieAdvice KCookieJar::cookieAdvice(KHttpCookiePtr cookiePtr) +{ + if (m_rejectCrossDomainCookies && cookiePtr->isCrossDomain()) + return KCookieReject; + + QStringList domains; + + extractDomains(cookiePtr->host(), domains); + + // If the cookie specifies a domain, check whether it is valid. Otherwise, + // accept the cookie anyways but remove the domain="" value to prevent + // cross-site cookie injection. + if (!cookiePtr->domain().isEmpty()) + { + if (!domains.contains(cookiePtr->domain()) && + !cookiePtr->domain().endsWith("."+cookiePtr->host())) + cookiePtr->fixDomain(QString::null); + } + + if (m_autoAcceptSessionCookies && (cookiePtr->expireDate() == 0 || + m_ignoreCookieExpirationDate)) + return KCookieAccept; + + KCookieAdvice advice = KCookieDunno; + bool isFQDN = true; // First is FQDN + QStringList::Iterator it = domains.begin(); // Start with FQDN which first in the list. + while( (advice == KCookieDunno) && (it != domains.end())) + { + QString domain = *it; + // Check if a policy for the FQDN/domain is set. + if ( domain[0] == '.' || isFQDN ) + { + isFQDN = false; + KHttpCookieList *cookieList = m_cookieDomains[domain]; + if (cookieList) + advice = cookieList->getAdvice(); + } + domains.remove(it); + it = domains.begin(); // Continue from begin of remaining list + } + + if (advice == KCookieDunno) + advice = m_globalAdvice; + + return advice; +} + +// +// This function gets the advice for all cookies originating from +// _domain. +// +KCookieAdvice KCookieJar::getDomainAdvice(const QString &_domain) +{ + KHttpCookieList *cookieList = m_cookieDomains[_domain]; + KCookieAdvice advice; + + if (cookieList) + { + advice = cookieList->getAdvice(); + } + else + { + advice = KCookieDunno; + } + + return advice; +} + +// +// This function sets the advice for all cookies originating from +// _domain. +// +void KCookieJar::setDomainAdvice(const QString &_domain, KCookieAdvice _advice) +{ + QString domain(_domain); + KHttpCookieList *cookieList = m_cookieDomains[domain]; + + if (cookieList) + { + if (cookieList->getAdvice() != _advice) + { + m_configChanged = true; + // domain is already known + cookieList->setAdvice( _advice); + } + + if ((cookieList->isEmpty()) && + (_advice == KCookieDunno)) + { + // This deletes cookieList! + m_cookieDomains.remove(domain); + m_domainList.remove(domain); + } + } + else + { + // domain is not yet known + if (_advice != KCookieDunno) + { + // We should create a domain entry + m_configChanged = true; + // Make a new cookie list + cookieList = new KHttpCookieList(); + cookieList->setAutoDelete(true); + cookieList->setAdvice( _advice); + m_cookieDomains.insert( domain, cookieList); + // Update the list of domains + m_domainList.append( domain); + } + } +} + +// +// This function sets the advice for all cookies originating from +// the same domain as _cookie +// +void KCookieJar::setDomainAdvice(KHttpCookiePtr cookiePtr, KCookieAdvice _advice) +{ + QString domain; + stripDomain(cookiePtr->host(), domain); // We file the cookie under this domain. + + setDomainAdvice(domain, _advice); +} + +// +// This function sets the global advice for cookies +// +void KCookieJar::setGlobalAdvice(KCookieAdvice _advice) +{ + if (m_globalAdvice != _advice) + m_configChanged = true; + m_globalAdvice = _advice; +} + +// +// Get a list of all domains known to the cookie jar. +// +const QStringList& KCookieJar::getDomainList() +{ + return m_domainList; +} + +// +// Get a list of all cookies in the cookie jar originating from _domain. +// +const KHttpCookieList *KCookieJar::getCookieList(const QString & _domain, + const QString & _fqdn ) +{ + QString domain; + + if (_domain.isEmpty()) + stripDomain( _fqdn, domain ); + else + domain = _domain; + + return m_cookieDomains[domain]; +} + +// +// Eat a cookie out of the jar. +// cookiePtr should be one of the cookies returned by getCookieList() +// +void KCookieJar::eatCookie(KHttpCookiePtr cookiePtr) +{ + QString domain = stripDomain(cookiePtr); // We file the cookie under this domain. + KHttpCookieList *cookieList = m_cookieDomains[domain]; + + if (cookieList) + { + // This deletes cookiePtr! + if (cookieList->removeRef( cookiePtr )) + m_cookiesChanged = true; + + if ((cookieList->isEmpty()) && + (cookieList->getAdvice() == KCookieDunno)) + { + // This deletes cookieList! + m_cookieDomains.remove(domain); + + m_domainList.remove(domain); + } + } +} + +void KCookieJar::eatCookiesForDomain(const QString &domain) +{ + KHttpCookieList *cookieList = m_cookieDomains[domain]; + if (!cookieList || cookieList->isEmpty()) return; + + cookieList->clear(); + if (cookieList->getAdvice() == KCookieDunno) + { + // This deletes cookieList! + m_cookieDomains.remove(domain); + m_domainList.remove(domain); + } + m_cookiesChanged = true; +} + +void KCookieJar::eatSessionCookies( long windowId ) +{ + if (!windowId) + return; + + QStringList::Iterator it=m_domainList.begin(); + for ( ; it != m_domainList.end(); ++it ) + eatSessionCookies( *it, windowId, false ); +} + +void KCookieJar::eatAllCookies() +{ + for ( QStringList::Iterator it=m_domainList.begin(); + it != m_domainList.end();) + { + QString domain = *it++; + // This might remove domain from domainList! + eatCookiesForDomain(domain); + } +} + +void KCookieJar::eatSessionCookies( const QString& fqdn, long windowId, + bool isFQDN ) +{ + KHttpCookieList* cookieList; + if ( !isFQDN ) + cookieList = m_cookieDomains[fqdn]; + else + { + QString domain; + stripDomain( fqdn, domain ); + cookieList = m_cookieDomains[domain]; + } + + if ( cookieList ) + { + KHttpCookiePtr cookie=cookieList->first(); + for (; cookie != 0;) + { + if ((cookie->expireDate() != 0) && !m_ignoreCookieExpirationDate) + { + cookie = cookieList->next(); + continue; + } + + QValueList<long> &ids = cookie->windowIds(); + if (!ids.remove(windowId) || !ids.isEmpty()) + { + cookie = cookieList->next(); + continue; + } + KHttpCookiePtr old_cookie = cookie; + cookie = cookieList->next(); + cookieList->removeRef( old_cookie ); + } + } +} + +// +// Saves all cookies to the file '_filename'. +// On succes 'true' is returned. +// On failure 'false' is returned. +bool KCookieJar::saveCookies(const QString &_filename) +{ + KSaveFile saveFile(_filename, 0600); + + if (saveFile.status() != 0) + return false; + + FILE *fStream = saveFile.fstream(); + + time_t curTime = time(0); + + fprintf(fStream, "# KDE Cookie File v2\n#\n"); + + fprintf(fStream, "%-20s %-20s %-12s %-10s %-4s %-20s %-4s %s\n", + "# Host", "Domain", "Path", "Exp.date", "Prot", + "Name", "Sec", "Value"); + + for ( QStringList::Iterator it=m_domainList.begin(); it != m_domainList.end(); + it++ ) + { + const QString &domain = *it; + bool domainPrinted = false; + + KHttpCookieList *cookieList = m_cookieDomains[domain]; + KHttpCookiePtr cookie=cookieList->last(); + + for (; cookie != 0;) + { + if (cookie->isExpired(curTime)) + { + // Delete expired cookies + KHttpCookiePtr old_cookie = cookie; + cookie = cookieList->prev(); + cookieList->removeRef( old_cookie ); + } + else if (cookie->expireDate() != 0 && !m_ignoreCookieExpirationDate) + { + if (!domainPrinted) + { + domainPrinted = true; + fprintf(fStream, "[%s]\n", domain.local8Bit().data()); + } + // Store persistent cookies + QString path = L1("\""); + path += cookie->path(); + path += '"'; + QString domain = L1("\""); + domain += cookie->domain(); + domain += '"'; + fprintf(fStream, "%-20s %-20s %-12s %10lu %3d %-20s %-4i %s\n", + cookie->host().latin1(), domain.latin1(), + path.latin1(), (unsigned long) cookie->expireDate(), + cookie->protocolVersion(), + cookie->name().isEmpty() ? cookie->value().latin1() : cookie->name().latin1(), + (cookie->isSecure() ? 1 : 0) + (cookie->isHttpOnly() ? 2 : 0) + + (cookie->hasExplicitPath() ? 4 : 0) + (cookie->name().isEmpty() ? 8 : 0), + cookie->value().latin1()); + cookie = cookieList->prev(); + } + else + { + // Skip session-only cookies + cookie = cookieList->prev(); + } + } + } + + return saveFile.close(); +} + +typedef char *charPtr; + +static const char *parseField(charPtr &buffer, bool keepQuotes=false) +{ + char *result; + if (!keepQuotes && (*buffer == '\"')) + { + // Find terminating " + buffer++; + result = buffer; + while((*buffer != '\"') && (*buffer)) + buffer++; + } + else + { + // Find first white space + result = buffer; + while((*buffer != ' ') && (*buffer != '\t') && (*buffer != '\n') && (*buffer)) + buffer++; + } + + if (!*buffer) + return result; // + *buffer++ = '\0'; + + // Skip white-space + while((*buffer == ' ') || (*buffer == '\t') || (*buffer == '\n')) + buffer++; + + return result; +} + + +// +// Reloads all cookies from the file '_filename'. +// On succes 'true' is returned. +// On failure 'false' is returned. +bool KCookieJar::loadCookies(const QString &_filename) +{ + FILE *fStream = fopen( QFile::encodeName(_filename), "r"); + if (fStream == 0) + { + return false; + } + + time_t curTime = time(0); + + char *buffer = new char[READ_BUFFER_SIZE]; + + bool err = false; + err = (fgets(buffer, READ_BUFFER_SIZE, fStream) == 0); + + int version = 1; + if (!err) + { + if (strcmp(buffer, "# KDE Cookie File\n") == 0) + { + // version 1 + } + else if (sscanf(buffer, "# KDE Cookie File v%d\n", &version) != 1) + { + err = true; + } + } + + if (!err) + { + while(fgets(buffer, READ_BUFFER_SIZE, fStream) != 0) + { + char *line = buffer; + // Skip lines which begin with '#' or '[' + if ((line[0] == '#') || (line[0] == '[')) + continue; + + const char *host( parseField(line) ); + const char *domain( parseField(line) ); + const char *path( parseField(line) ); + const char *expStr( parseField(line) ); + if (!expStr) continue; + int expDate = (time_t) strtoul(expStr, 0, 10); + const char *verStr( parseField(line) ); + if (!verStr) continue; + int protVer = (time_t) strtoul(verStr, 0, 10); + const char *name( parseField(line) ); + bool keepQuotes = false; + bool secure = false; + bool httpOnly = false; + bool explicitPath = false; + const char *value = 0; + if ((version == 2) || (protVer >= 200)) + { + if (protVer >= 200) + protVer -= 200; + int i = atoi( parseField(line) ); + secure = i & 1; + httpOnly = i & 2; + explicitPath = i & 4; + if (i & 8) + name = ""; + line[strlen(line)-1] = '\0'; // Strip LF. + value = line; + } + else + { + if (protVer >= 100) + { + protVer -= 100; + keepQuotes = true; + } + value = parseField(line, keepQuotes); + secure = atoi( parseField(line) ); + } + + // Parse error + if (!value) continue; + + // Expired or parse error + if ((expDate == 0) || (expDate < curTime)) + continue; + + KHttpCookie *cookie = new KHttpCookie(QString::fromLatin1(host), + QString::fromLatin1(domain), + QString::fromLatin1(path), + QString::fromLatin1(name), + QString::fromLatin1(value), + expDate, protVer, + secure, httpOnly, explicitPath); + addCookie(cookie); + } + } + delete [] buffer; + m_cookiesChanged = false; + + fclose( fStream); + return err; +} + +// +// Save the cookie configuration +// + +void KCookieJar::saveConfig(KConfig *_config) +{ + if (!m_configChanged) + return; + + _config->setGroup("Cookie Dialog"); + _config->writeEntry("PreferredPolicy", m_preferredPolicy); + _config->writeEntry("ShowCookieDetails", m_showCookieDetails ); + _config->setGroup("Cookie Policy"); + _config->writeEntry("CookieGlobalAdvice", adviceToStr( m_globalAdvice)); + + QStringList domainSettings; + for ( QStringList::Iterator it=m_domainList.begin(); + it != m_domainList.end(); + it++ ) + { + const QString &domain = *it; + KCookieAdvice advice = getDomainAdvice( domain); + if (advice != KCookieDunno) + { + QString value(domain); + value += ':'; + value += adviceToStr(advice); + domainSettings.append(value); + } + } + _config->writeEntry("CookieDomainAdvice", domainSettings); + _config->sync(); + m_configChanged = false; +} + + +// +// Load the cookie configuration +// + +void KCookieJar::loadConfig(KConfig *_config, bool reparse ) +{ + if ( reparse ) + _config->reparseConfiguration(); + + _config->setGroup("Cookie Dialog"); + m_showCookieDetails = _config->readBoolEntry( "ShowCookieDetails" ); + m_preferredPolicy = _config->readNumEntry( "PreferredPolicy", 0 ); + + _config->setGroup("Cookie Policy"); + QStringList domainSettings = _config->readListEntry("CookieDomainAdvice"); + m_rejectCrossDomainCookies = _config->readBoolEntry( "RejectCrossDomainCookies", true ); + m_autoAcceptSessionCookies = _config->readBoolEntry( "AcceptSessionCookies", true ); + m_ignoreCookieExpirationDate = _config->readBoolEntry( "IgnoreExpirationDate", false ); + QString value = _config->readEntry("CookieGlobalAdvice", L1("Ask")); + m_globalAdvice = strToAdvice(value); + + // Reset current domain settings first. + for ( QStringList::Iterator it=m_domainList.begin(); it != m_domainList.end(); ) + { + // Make sure to update iterator before calling setDomainAdvice() + // setDomainAdvice() might delete the domain from domainList. + QString domain = *it++; + setDomainAdvice(domain, KCookieDunno); + } + + // Now apply the domain settings read from config file... + for ( QStringList::Iterator it=domainSettings.begin(); + it != domainSettings.end(); ) + { + const QString &value = *it++; + + int sepPos = value.findRev(':'); + + if (sepPos <= 0) + continue; + + QString domain(value.left(sepPos)); + KCookieAdvice advice = strToAdvice( value.mid(sepPos + 1) ); + setDomainAdvice(domain, advice); + } +} diff --git a/kioslave/http/kcookiejar/kcookiejar.desktop b/kioslave/http/kcookiejar/kcookiejar.desktop new file mode 100644 index 000000000..54421225a --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiejar.desktop @@ -0,0 +1,157 @@ +[Desktop Entry] +Type=Service +Name=KDED Cookie Jar Module +Name[af]=Kded Koekie Houer Module +Name[ar]=وحدة Jar لكعكة KDED +Name[az]=KDED Kökə Jar Modulu +Name[be]=Модуль "печыва" KDED +Name[bg]=Модул KDED Cookie Jar +Name[bn]=KDED কুকি জার মডিউল +Name[bs]=KDED modul "Tegla sa keksima" +Name[ca]=Mòdul Jar de cookies per a KDED +Name[cs]=KDED modul pro cookies +Name[csb]=Sprôwianié kùszkama +Name[cy]=Modiwl Jar Cwci KDED +Name[da]=KDED-cookie-jar-modul +Name[de]=Cookie-Verwaltung +Name[el]=Άρθρωμα Cookie Jar του KDED +Name[eo]=KDED-kuketotraktila modulo +Name[es]=Módulo Jar de cookies de KDED +Name[et]=KDED Cookie Jar moodul +Name[eu]=KDED Cookie Jar modulua +Name[fa]=پیمانۀ ظرف کوکی KDED +Name[fi]=KDED-evästemoduuli +Name[fr]=Module Jar de cookie KDED +Name[fy]=KDED-module foar it bewarjen fan Koekjes +Name[gl]=Módulo Jar de cookies de KDED +Name[he]=מודול צנצנת העוגיות של KDED +Name[hi]=KDED कुकी जार मॉड्यूल +Name[hr]=KDED modul za čuvanje kolačića +Name[hu]=KDED cookie-modul +Name[id]=Modul Penyimpanan Cookies KDED +Name[is]=KDED smákökukrukka +Name[it]=Modulo Jar dei cookie per KDED +Name[ja]=KDED クッキー Jar モジュール +Name[ka]=KDED-ის ბმულების Jar მოდული +Name[kk]=KDE cookie модулі +Name[km]=ម៉ូឌុល Jar នៃខូគី KDED +Name[ko]=KDED 쿠키 JAR 모듈 +Name[lb]=KDED-Modul fir d'Verwaltung vun de Cookien +Name[lt]=KDED slapukų rinkinio modulis +Name[lv]=KDED Cepumu Jar modulis +Name[mk]=KDED модул Тегла со колачиња +Name[ms]=Modul Balang Cecikut KDED +Name[mt]=Modulu tal-"cookies" KDED +Name[nb]=KDEDs modul for informasjonskapsler (Cookie Jar) +Name[nds]=KDED-Kookjepleeg +Name[ne]=KDED कुकी जार मोड्युल +Name[nl]=KDED-module voor het opslaan van cookies +Name[nn]=KDED-informasjonskapselmodul +Name[nso]=Seripa sa Jar ya Cookie ya KDED +Name[pa]=KDED ਕੂਕੀਜ਼ Jar ਮੈਡੀਊਲ +Name[pl]=Zarządzanie ciasteczkami +Name[pt]=Módulo de 'Cookies' do KDED +Name[pt_BR]=Módulo de Cookie Jar do KDE +Name[ro]=Modul Cookie JAR pentru KDED +Name[ru]=Служба cookie +Name[rw]=Igice Jar Inyandikonyakwirema KDED +Name[se]=KDED gáhkošlihtti-moduvla +Name[sk]=Modul pre cookies KDED +Name[sl]=Modul posode za piškotke KDED +Name[sq]=Modul i KDED-it për Qyp të keksave nga KDED +Name[sr]=KDED модул тегле за колачиће +Name[sr@Latn]=KDED modul tegle za kolačiće +Name[sv]=KDED-kakburksmodul +Name[ta]=KDED தற்காலிக நினைவக சாடி பகுதி +Name[te]=కెడిఈడి కుకీ జాడి మాడ్యూల్ +Name[tg]=Модули KDED Cookie Jar +Name[th]=โมดูลโถคุกกี KDED +Name[tr]=KDED Cookie Jar Modülü +Name[tt]=KDED'nıñ Cookie Modulı +Name[uk]=Модуль глечика з куками KDED +Name[uz]=KDED kuki idish moduli +Name[uz@cyrillic]=KDED куки идиш модули +Name[ven]=Modulu wa Jar wa Cookie ya KDED +Name[vi]=Mô-đun Cookie Jar của KDED +Name[xh]=Isicatshulwa se KDED Cookie Jar +Name[zh_CN]=KDED Cookie Jar 模块 +Name[zh_HK]=KDED Cookie Jar 模組 +Name[zh_TW]=KDED Cookie Jar 模組 +Name[zu]=Ingxenye Yojeke ye-Cookie ye-KDED +Comment=Keeps track of all cookies in the system +Comment[af]=Hou tred van al die koekies in die stelsel +Comment[ar]=يراقب جميع الكعكات الموجودة على النظام +Comment[be]=Захоўвае звесткі пра "печыва" +Comment[bg]=Контрол над всички бисквитки в системата +Comment[bn]=সিস্টেমে সমস্ত কুকি-র খোঁজখবর রাখে +Comment[bs]=Prati sve kolačiće (cookije) na sistemu +Comment[ca]=Segueix totes les galetes en el sistema +Comment[cs]=Spravuje Cookies v počítači +Comment[csb]=Trzëmô wszëtczé kùszczi w systemie +Comment[da]=Holder styr på alle cookier på systemet +Comment[de]=Verwaltet die Cookies in KDE +Comment[el]=Διατηρεί αρχείο από όλα τα cookies στο σύστημα +Comment[eo]=Registras ĉiujn kuketojn en la sistemo +Comment[es]=Mantiene registro todas las cookies en el sistema +Comment[et]=Hoiab silma peal kõigil süsteemi küpsistel +Comment[eu]=Sistemaren cookie guztien jarraipena egiten du +Comment[fa]=رد همۀ کوکیها را در سیستم نگه میدارد +Comment[fi]=Seuraa järjestelmän evästeitä +Comment[fr]=Conserve une trace de tous les cookies dans le système +Comment[fy]=Hâld by wer alle koekjes binne +Comment[gl]=Manter as pegadas de todas as Cookies no sistema +Comment[he]=מבצע מעקב אחרי כל העוגיות במערכת +Comment[hi]=तंत्र की सभी कुकी की जानकारी रखता है +Comment[hr]=Vođenje evidencije o svim kolačićima na sustavu +Comment[hu]=Nyomon követi a rendszerben létrejövő cookie-kat +Comment[id]=Menyimpan semua cookies pada sistem +Comment[is]=Heldur utanum allar smákökur í kerfinu +Comment[it]=Tiene traccia di tutti i cookie del sistema +Comment[ja]=システムのすべてのクッキーを管理します +Comment[ka]=სისტემის ყველა ბმულის თვალმიდევნება +Comment[kk]=Жүйедегі бүкіл cookie файлдарды бақылау +Comment[km]=រក្សាការតាមដានខូគីទាំងអស់ក្នុងប្រព័ន្ធ +Comment[lb]=Iwwerwaacht all d'Cookie vum System +Comment[lt]=Seka visus slapukus sistemoje +Comment[lv]=Seko visiem sistēmā esošajiem cepumiem +Comment[mk]=Води сметка за сите колачиња во системот +Comment[ms]=Memerhati semua cecikut dalam sistem +Comment[nb]=Holder rede på alle informasjonskapsler i systemet +Comment[nds]=Passt all Kookjes in't Systeem +Comment[ne]=प्रणालीमा सबै कुकीहरूको पदचिन्ह राख्दछ +Comment[nl]=Houdt alle cookies in het systeem bij +Comment[nn]=Held greie på informasjonskapslane +Comment[pa]=ਸਿਸਟਮ ਦੇ ਸਾਰੇ ਕੂਕੀਜ਼ ਦਾ ਰਿਕਾਰਡ ਰੱਖੋ +Comment[pl]=Przechowuje wszystkie ciasteczka w systemie +Comment[pt]=Mantém um registo de todos os 'cookies' no sistema +Comment[pt_BR]=Mantém informações sobre todos os cookies do sistema +Comment[ro]=Administrează toate "cookie"-urile din sistem +Comment[ru]=Управление закладками-cookie в KDE +Comment[rw]=Iguma inzira y'inyandikonyakwirema zose muri sisitemu +Comment[se]=Halddaša buot diehtočoahkuid +Comment[sk]=Sleduje všetky cookie v systéme +Comment[sl]=Opazuje vse piškotke v sistemu +Comment[sr]=Води евиденцију о свим колачићима на систему +Comment[sr@Latn]=Vodi evidenciju o svim kolačićima na sistemu +Comment[sv]=Håller ordning på alla kakor i systemet +Comment[ta]=கணினியின் எல்லா தற்காலிக நினைவகங்களையும் கண்காணிக்கிறது +Comment[te]=వ్యవస్థలోని అన్ని కుకీల జాడని వుంచుకుంటుంది +Comment[tg]=Гузаргоҳи ҳамаша Cookies дар система муҳофизат кунед +Comment[th]=ใช้ติดตามคุกกีทั้งหมดในระบบ +Comment[tr]=Sistemdeki tüm çerezleri izler +Comment[tt]=Sistemdäge bar cookie'larnı küz astında tota +Comment[uk]=Стежить за всіма куками в системі +Comment[uz]=Tizimdagi hamma kukilarni kuzatadi +Comment[uz@cyrillic]=Тизимдаги ҳамма кукиларни кузатади +Comment[vi]=Theo dõi các tập tin cookie trong hệ thống. +Comment[zh_CN]=将全部 cookies 的记录保存在系统中 +Comment[zh_TW]=追蹤系統所有的 cookies +ServiceTypes=KDEDModule +Exec=kcookiejar +X-DCOP-ServiceType=Unique +X-KDE-StartupNotify=false +X-KDE-ModuleType=Library +X-KDE-Library=kcookiejar +X-KDE-FactoryName=kcookiejar +X-KDE-Kded-autoload=false +X-KDE-Kded-load-on-demand=true diff --git a/kioslave/http/kcookiejar/kcookiejar.h b/kioslave/http/kcookiejar/kcookiejar.h new file mode 100644 index 000000000..c73708bea --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiejar.h @@ -0,0 +1,365 @@ +/* + This file is part of the KDE File Manager + + Copyright (C) 1998 Waldo Bastian (bastian@kde.org) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + + This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +//---------------------------------------------------------------------------- +// +// KDE File Manager -- HTTP Cookies +// $Id$ + +#ifndef KCOOKIEJAR_H +#define KCOOKIEJAR_H + +#include <qstring.h> +#include <qstringlist.h> +#include <qdict.h> +#include <qptrlist.h> +#include <time.h> + +class KConfig; +class KCookieJar; +class KHttpCookie; +class KHttpCookieList; + +typedef KHttpCookie *KHttpCookiePtr; + +enum KCookieAdvice +{ + KCookieDunno=0, + KCookieAccept, + KCookieReject, + KCookieAsk +}; + +class KHttpCookie +{ + friend class KCookieJar; + friend class KHttpCookieList; + +protected: + QString mHost; + QString mDomain; + QString mPath; + QString mName; + QString mValue; + time_t mExpireDate; + int mProtocolVersion; + bool mSecure; + bool mCrossDomain; + bool mHttpOnly; + bool mExplicitPath; + QValueList<long> mWindowIds; + + QString cookieStr(bool useDOMFormat); + +public: + KHttpCookie(const QString &_host=QString::null, + const QString &_domain=QString::null, + const QString &_path=QString::null, + const QString &_name=QString::null, + const QString &_value=QString::null, + time_t _expireDate=0, + int _protocolVersion=0, + bool _secure = false, + bool _httpOnly = false, + bool _explicitPath = false); + + QString domain(void) { return mDomain; } + QString host(void) { return mHost; } + QString path(void) { return mPath; } + QString name(void) { return mName; } + QString value(void) { return mValue; } + QValueList<long> &windowIds(void) { return mWindowIds; } + void fixDomain(const QString &domain) { mDomain = domain; } + time_t expireDate(void) { return mExpireDate; } + int protocolVersion(void) { return mProtocolVersion; } + bool isSecure(void) { return mSecure; } + bool isExpired(time_t currentDate); + bool isCrossDomain(void) { return mCrossDomain; } + bool isHttpOnly(void) { return mHttpOnly; } + bool hasExplicitPath(void) { return mExplicitPath; } + bool match(const QString &fqdn, const QStringList &domainList, const QString &path); +}; + +class KHttpCookieList : public QPtrList<KHttpCookie> +{ +public: + KHttpCookieList() : QPtrList<KHttpCookie>(), advice( KCookieDunno ) + { } + virtual ~KHttpCookieList() { } + + virtual int compareItems( void * item1, void * item2); + KCookieAdvice getAdvice(void) { return advice; } + void setAdvice(KCookieAdvice _advice) { advice = _advice; } + +private: + KCookieAdvice advice; +}; + +class KCookieJar +{ +public: + /** + * Constructs a new cookie jar + * + * One jar should be enough for all cookies. + */ + KCookieJar(); + + /** + * Destructs the cookie jar + * + * Poor little cookies, they will all be eaten by the cookie monster! + */ + ~KCookieJar(); + + /** + * Returns whether the cookiejar has been changed + */ + bool changed() const { return m_cookiesChanged || m_configChanged; } + + /** + * Store all the cookies in a safe(?) place + */ + bool saveCookies(const QString &_filename); + + /** + * Load all the cookies from file and add them to the cookie jar. + */ + bool loadCookies(const QString &_filename); + + /** + * Save the cookie configuration + */ + void saveConfig(KConfig *_config); + + /** + * Load the cookie configuration + */ + void loadConfig(KConfig *_config, bool reparse = false); + + /** + * Looks for cookies in the cookie jar which are appropriate for _url. + * Returned is a string containing all appropriate cookies in a format + * which can be added to a HTTP-header without any additional processing. + * + * If @p useDOMFormat is true, the string is formatted in a format + * in compliance with the DOM standard. + * @p pendingCookies contains a list of cookies that have not been + * approved yet by the user but that will be included in the result + * none the less. + */ + QString findCookies(const QString &_url, bool useDOMFormat, long windowId, KHttpCookieList *pendingCookies=0); + + /** + * This function parses cookie_headers and returns a linked list of + * valid KHttpCookie objects for all cookies found in cookie_headers. + * If no cookies could be found 0 is returned. + * + * cookie_headers should be a concatenation of all lines of a HTTP-header + * which start with "Set-Cookie". The lines should be separated by '\n's. + */ + KHttpCookieList makeCookies(const QString &_url, const QCString &cookie_headers, long windowId); + + /** + * This function parses cookie_headers and returns a linked list of + * valid KHttpCookie objects for all cookies found in cookie_headers. + * If no cookies could be found 0 is returned. + * + * cookie_domstr should be a concatenation of "name=value" pairs, separated + * by a semicolon ';'. + */ + KHttpCookieList makeDOMCookies(const QString &_url, const QCString &cookie_domstr, long windowId); + + /** + * This function hands a KHttpCookie object over to the cookie jar. + * + * On return cookiePtr is set to 0. + */ + void addCookie(KHttpCookiePtr &cookiePtr); + + /** + * This function advices whether a single KHttpCookie object should + * be added to the cookie jar. + * + * Possible return values are: + * - KCookieAccept, the cookie should be added + * - KCookieReject, the cookie should not be added + * - KCookieAsk, the user should decide what to do + */ + KCookieAdvice cookieAdvice(KHttpCookiePtr cookiePtr); + + /** + * This function gets the advice for all cookies originating from + * _domain. + * + * - KCookieDunno, no specific advice for _domain + * - KCookieAccept, accept all cookies for _domain + * - KCookieReject, reject all cookies for _domain + * - KCookieAsk, the user decides what to do with cookies for _domain + */ + KCookieAdvice getDomainAdvice(const QString &_domain); + + /** + * This function sets the advice for all cookies originating from + * _domain. + * + * _advice can have the following values: + * - KCookieDunno, no specific advice for _domain + * - KCookieAccept, accept all cookies for _domain + * - KCookieReject, reject all cookies for _domain + * - KCookieAsk, the user decides what to do with cookies for _domain + */ + void setDomainAdvice(const QString &_domain, KCookieAdvice _advice); + + /** + * This function sets the advice for all cookies originating from + * the same domain as _cookie + * + * _advice can have the following values: + * - KCookieDunno, no specific advice for _domain + * - KCookieAccept, accept all cookies for _domain + * - KCookieReject, reject all cookies for _domain + * - KCookieAsk, the user decides what to do with cookies for _domain + */ + void setDomainAdvice(KHttpCookiePtr _cookie, KCookieAdvice _advice); + + /** + * Get the global advice for cookies + * + * The returned advice can have the following values: + * - KCookieAccept, accept cookies + * - KCookieReject, reject cookies + * - KCookieAsk, the user decides what to do with cookies + * + * The global advice is used if the domain has no advice set. + */ + KCookieAdvice getGlobalAdvice() { return m_globalAdvice; } + + /** + * This function sets the global advice for cookies + * + * _advice can have the following values: + * - KCookieAccept, accept cookies + * - KCookieReject, reject cookies + * - KCookieAsk, the user decides what to do with cookies + * + * The global advice is used if the domain has no advice set. + */ + void setGlobalAdvice(KCookieAdvice _advice); + + /** + * Get a list of all domains known to the cookie jar. + * A domain is known to the cookie jar if: + * - It has a cookie originating from the domain + * - It has a specific advice set for the domain + */ + const QStringList& getDomainList(); + + /** + * Get a list of all cookies in the cookie jar originating from _domain. + */ + const KHttpCookieList *getCookieList(const QString & _domain, + const QString& _fqdn ); + + /** + * Remove & delete a cookie from the jar. + * + * cookiePtr should be one of the entries in a KHttpCookieList. + * Update your KHttpCookieList by calling getCookieList after + * calling this function. + */ + void eatCookie(KHttpCookiePtr cookiePtr); + + /** + * Remove & delete all cookies for @p domain. + */ + void eatCookiesForDomain(const QString &domain); + + /** + * Remove & delete all cookies + */ + void eatAllCookies(); + + /** + * Removes all end of session cookies set by the + * session @p windId. + */ + void eatSessionCookies( long windowId ); + + /** + * Removes all end of session cookies set by the + * session @p windId. + */ + void eatSessionCookies( const QString& fqdn, long windowId, bool isFQDN = true ); + + /** + * Parses _url and returns the FQDN (_fqdn) and path (_path). + */ + static bool parseURL(const QString &_url, + QString &_fqdn, + QString &_path); + + /** + * Returns a list of domains in @p _domainList relevant for this host. + * The list is sorted with the FQDN listed first and the top-most + * domain listed last + */ + void extractDomains(const QString &_fqdn, + QStringList &_domainList); + + static QString adviceToStr(KCookieAdvice _advice); + static KCookieAdvice strToAdvice(const QString &_str); + + /** Returns the */ + int preferredDefaultPolicy() const { return m_preferredPolicy; } + + /** Returns the */ + bool showCookieDetails () const { return m_showCookieDetails; } + + /** + * Sets the user's default preference cookie policy. + */ + void setPreferredDefaultPolicy (int value) { m_preferredPolicy = value; } + + /** + * Sets the user's preference of level of detail displayed + * by the cookie dialog. + */ + void setShowCookieDetails (bool value) { m_showCookieDetails = value; } + +protected: + void stripDomain(const QString &_fqdn, QString &_domain); + QString stripDomain( KHttpCookiePtr cookiePtr); + +protected: + QStringList m_domainList; + KCookieAdvice m_globalAdvice; + QDict<KHttpCookieList> m_cookieDomains; + QDict<int> m_twoLevelTLD; + + bool m_configChanged; + bool m_cookiesChanged; + bool m_showCookieDetails; + bool m_rejectCrossDomainCookies; + bool m_autoAcceptSessionCookies; + bool m_ignoreCookieExpirationDate; + + int m_preferredPolicy; +}; +#endif diff --git a/kioslave/http/kcookiejar/kcookiescfg.upd b/kioslave/http/kcookiejar/kcookiescfg.upd new file mode 100644 index 000000000..3c1cd028d --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiescfg.upd @@ -0,0 +1,16 @@ +# Update for old cookie config files, if present +Id=kde2.2/b1 +File=kcookiejarrc +Group=Browser Settings/HTTP,Cookie Policy + +# Update cookies config file... +Id=kde3.1/cvs +File=kcookiejarrc +Group=<default>,Cookie Dialog +Key=DefaultRadioButton,PreferredPolicy +Key=ShowCookieDetails +Group=Cookie Policy +Key=AcceptTempCookies,AcceptSessionCookies +Key=AutoAcceptSessionCookies,AcceptSessionCookies +Key=RejectCrossDomain,RejectCrossDomainCookies +Key=IgnoreCookieExpirationDate,IgnoreExpirationDate diff --git a/kioslave/http/kcookiejar/kcookieserver.cpp b/kioslave/http/kcookiejar/kcookieserver.cpp new file mode 100644 index 000000000..365f15e79 --- /dev/null +++ b/kioslave/http/kcookiejar/kcookieserver.cpp @@ -0,0 +1,606 @@ +/* +This file is part of KDE + + Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +//---------------------------------------------------------------------------- +// +// KDE Cookie Server +// $Id$ + +#define SAVE_DELAY 3 // Save after 3 minutes + +#include <unistd.h> + +#include <qtimer.h> +#include <qptrlist.h> +#include <qfile.h> + +#include <dcopclient.h> + +#include <kconfig.h> +#include <kdebug.h> +#include <kapplication.h> +#include <kcmdlineargs.h> +#include <kstandarddirs.h> + +#include "kcookiejar.h" +#include "kcookiewin.h" +#include "kcookieserver.h" + +extern "C" { + KDE_EXPORT KDEDModule *create_kcookiejar(const QCString &name) + { + return new KCookieServer(name); + } +} + + +// Cookie field indexes +enum CookieDetails { CF_DOMAIN=0, CF_PATH, CF_NAME, CF_HOST, + CF_VALUE, CF_EXPIRE, CF_PROVER, CF_SECURE }; + + +class CookieRequest { +public: + DCOPClient *client; + DCOPClientTransaction *transaction; + QString url; + bool DOM; + long windowId; +}; + +template class QPtrList<CookieRequest>; + +class RequestList : public QPtrList<CookieRequest> +{ +public: + RequestList() : QPtrList<CookieRequest>() { } +}; + +KCookieServer::KCookieServer(const QCString &name) + :KDEDModule(name) +{ + mOldCookieServer = new DCOPClient(); // backwards compatibility. + mOldCookieServer->registerAs("kcookiejar", false); + mOldCookieServer->setDaemonMode( true ); + mCookieJar = new KCookieJar; + mPendingCookies = new KHttpCookieList; + mPendingCookies->setAutoDelete(true); + mRequestList = new RequestList; + mAdvicePending = false; + mTimer = new QTimer(); + connect( mTimer, SIGNAL( timeout()), SLOT( slotSave())); + mConfig = new KConfig("kcookiejarrc"); + mCookieJar->loadConfig( mConfig ); + + QString filename = locateLocal("data", "kcookiejar/cookies"); + + // Stay backwards compatible! + QString filenameOld = locate("data", "kfm/cookies"); + if (!filenameOld.isEmpty()) + { + mCookieJar->loadCookies( filenameOld ); + if (mCookieJar->saveCookies( filename)) + { + unlink(QFile::encodeName(filenameOld)); // Remove old kfm cookie file + } + } + else + { + mCookieJar->loadCookies( filename); + } + connect(this, SIGNAL(windowUnregistered(long)), + this, SLOT(slotDeleteSessionCookies(long))); +} + +KCookieServer::~KCookieServer() +{ + if (mCookieJar->changed()) + slotSave(); + delete mOldCookieServer; + delete mCookieJar; + delete mTimer; + delete mPendingCookies; + delete mConfig; +} + +bool KCookieServer::cookiesPending( const QString &url, KHttpCookieList *cookieList ) +{ + QString fqdn; + QStringList domains; + QString path; + // Check whether 'url' has cookies on the pending list + if (mPendingCookies->isEmpty()) + return false; + if (!KCookieJar::parseURL(url, fqdn, path)) + return false; + + mCookieJar->extractDomains( fqdn, domains ); + for( KHttpCookie *cookie = mPendingCookies->first(); + cookie != 0L; + cookie = mPendingCookies->next()) + { + if (cookie->match( fqdn, domains, path)) + { + if (!cookieList) + return true; + cookieList->append(cookie); + } + } + if (!cookieList) + return false; + return cookieList->isEmpty(); +} + +void KCookieServer::addCookies( const QString &url, const QCString &cookieHeader, + long windowId, bool useDOMFormat ) +{ + KHttpCookieList cookieList; + if (useDOMFormat) + cookieList = mCookieJar->makeDOMCookies(url, cookieHeader, windowId); + else + cookieList = mCookieJar->makeCookies(url, cookieHeader, windowId); + + checkCookies(&cookieList); + + for(KHttpCookiePtr cookie = cookieList.first(); cookie; cookie = cookieList.first()) + mPendingCookies->append(cookieList.take()); + + if (!mAdvicePending) + { + mAdvicePending = true; + while (!mPendingCookies->isEmpty()) + { + checkCookies(0); + } + mAdvicePending = false; + } +} + +void KCookieServer::checkCookies( KHttpCookieList *cookieList) +{ + KHttpCookieList *list; + + if (cookieList) + list = cookieList; + else + list = mPendingCookies; + + KHttpCookiePtr cookie = list->first(); + while (cookie) + { + kdDebug(7104) << "checkCookies: Asking cookie advice for " << cookie->host() << endl; + KCookieAdvice advice = mCookieJar->cookieAdvice(cookie); + switch(advice) + { + case KCookieAccept: + list->take(); + mCookieJar->addCookie(cookie); + cookie = list->current(); + break; + + case KCookieReject: + list->take(); + delete cookie; + cookie = list->current(); + break; + + default: + cookie = list->next(); + break; + } + } + + if (cookieList || list->isEmpty()) + return; + + KHttpCookiePtr currentCookie = mPendingCookies->first(); + + KHttpCookieList currentList; + currentList.append(currentCookie); + QString currentHost = currentCookie->host(); + + cookie = mPendingCookies->next(); + while (cookie) + { + if (cookie->host() == currentHost) + { + currentList.append(cookie); + } + cookie = mPendingCookies->next(); + } + + KCookieWin *kw = new KCookieWin( 0L, currentList, + mCookieJar->preferredDefaultPolicy(), + mCookieJar->showCookieDetails() ); + KCookieAdvice userAdvice = kw->advice(mCookieJar, currentCookie); + delete kw; + // Save the cookie config if it has changed + mCookieJar->saveConfig( mConfig ); + + // Apply the user's choice to all cookies that are currently + // queued for this host. + cookie = mPendingCookies->first(); + while (cookie) + { + if (cookie->host() == currentHost) + { + switch(userAdvice) + { + case KCookieAccept: + mPendingCookies->take(); + mCookieJar->addCookie(cookie); + cookie = mPendingCookies->current(); + break; + + case KCookieReject: + mPendingCookies->take(); + delete cookie; + cookie = mPendingCookies->current(); + break; + + default: + qWarning(__FILE__":%d Problem!", __LINE__); + cookie = mPendingCookies->next(); + break; + } + } + else + { + cookie = mPendingCookies->next(); + } + } + + + // Check if we can handle any request + for ( CookieRequest *request = mRequestList->first(); request;) + { + if (!cookiesPending( request->url )) + { + QCString replyType; + QByteArray replyData; + QString res = mCookieJar->findCookies( request->url, request->DOM, request->windowId ); + + QDataStream stream2(replyData, IO_WriteOnly); + stream2 << res; + replyType = "QString"; + request->client->endTransaction( request->transaction, + replyType, replyData); + CookieRequest *tmp = request; + request = mRequestList->next(); + mRequestList->removeRef( tmp ); + delete tmp; + } + else + { + request = mRequestList->next(); + } + } + if (mCookieJar->changed()) + saveCookieJar(); +} + +void KCookieServer::slotSave() +{ + QString filename = locateLocal("data", "kcookiejar/cookies"); + mCookieJar->saveCookies(filename); +} + +void KCookieServer::saveCookieJar() +{ + if( mTimer->isActive() ) + return; + + mTimer->start( 1000*60*SAVE_DELAY, true ); +} + +void KCookieServer::putCookie( QStringList& out, KHttpCookie *cookie, + const QValueList<int>& fields ) +{ + QValueList<int>::ConstIterator i = fields.begin(); + for ( ; i != fields.end(); ++i ) + { + switch(*i) + { + case CF_DOMAIN : + out << cookie->domain(); + break; + case CF_NAME : + out << cookie->name(); + break; + case CF_PATH : + out << cookie->path(); + break; + case CF_HOST : + out << cookie->host(); + break; + case CF_VALUE : + out << cookie->value(); + break; + case CF_EXPIRE : + out << QString::number(cookie->expireDate()); + break; + case CF_PROVER : + out << QString::number(cookie->protocolVersion()); + break; + case CF_SECURE : + out << QString::number( cookie->isSecure() ? 1 : 0 ); + break; + default : + out << QString::null; + } + } +} + +bool KCookieServer::cookieMatches( KHttpCookiePtr c, + QString domain, QString fqdn, + QString path, QString name ) +{ + if( c ) + { + bool hasDomain = !domain.isEmpty(); + return + ((hasDomain && c->domain() == domain) || + fqdn == c->host()) && + (c->path() == path) && + (c->name() == name) && + (!c->isExpired(time(0))); + } + return false; +} + +// DCOP function +QString +KCookieServer::findCookies(QString url) +{ + return findCookies(url, 0); +} + +// DCOP function +QString +KCookieServer::findCookies(QString url, long windowId) +{ + if (cookiesPending(url)) + { + CookieRequest *request = new CookieRequest; + request->client = callingDcopClient(); + request->transaction = request->client->beginTransaction(); + request->url = url; + request->DOM = false; + request->windowId = windowId; + mRequestList->append( request ); + return QString::null; // Talk to you later :-) + } + + QString cookies = mCookieJar->findCookies(url, false, windowId); + + if (mCookieJar->changed()) + saveCookieJar(); + + return cookies; +} + +// DCOP function +QStringList +KCookieServer::findDomains() +{ + QStringList result; + const QStringList domains = mCookieJar->getDomainList(); + for ( QStringList::ConstIterator domIt = domains.begin(); + domIt != domains.end(); ++domIt ) + { + // Ignore domains that have policy set for but contain + // no cookies whatsoever... + const KHttpCookieList* list = mCookieJar->getCookieList(*domIt, ""); + if ( list && !list->isEmpty() ) + result << *domIt; + } + return result; +} + +// DCOP function +QStringList +KCookieServer::findCookies(QValueList<int> fields, + QString domain, + QString fqdn, + QString path, + QString name) +{ + QStringList result; + bool allDomCookies = name.isEmpty(); + + const KHttpCookieList* list = mCookieJar->getCookieList(domain, fqdn); + if ( list && !list->isEmpty() ) + { + QPtrListIterator<KHttpCookie>it( *list ); + for ( ; it.current(); ++it ) + { + if ( !allDomCookies ) + { + if ( cookieMatches(it.current(), domain, fqdn, path, name) ) + { + putCookie(result, it.current(), fields); + break; + } + } + else + putCookie(result, it.current(), fields); + } + } + return result; +} + +// DCOP function +QString +KCookieServer::findDOMCookies(QString url) +{ + return findDOMCookies(url, 0); +} + +// DCOP function +QString +KCookieServer::findDOMCookies(QString url, long windowId) +{ + // We don't wait for pending cookies because it locks up konqueror + // which can cause a deadlock if it happens to have a popup-menu up. + // Instead we just return pending cookies as if they had been accepted already. + KHttpCookieList pendingCookies; + cookiesPending(url, &pendingCookies); + + return mCookieJar->findCookies(url, true, windowId, &pendingCookies); +} + +// DCOP function +void +KCookieServer::addCookies(QString arg1, QCString arg2, long arg3) +{ + addCookies(arg1, arg2, arg3, false); +} + +// DCOP function +void +KCookieServer::deleteCookie(QString domain, QString fqdn, + QString path, QString name) +{ + const KHttpCookieList* list = mCookieJar->getCookieList( domain, fqdn ); + if ( list && !list->isEmpty() ) + { + QPtrListIterator<KHttpCookie>it (*list); + for ( ; it.current(); ++it ) + { + if( cookieMatches(it.current(), domain, fqdn, path, name) ) + { + mCookieJar->eatCookie( it.current() ); + saveCookieJar(); + break; + } + } + } +} + +// DCOP function +void +KCookieServer::deleteCookiesFromDomain(QString domain) +{ + mCookieJar->eatCookiesForDomain(domain); + saveCookieJar(); +} + + +// Qt function +void +KCookieServer::slotDeleteSessionCookies( long windowId ) +{ + deleteSessionCookies(windowId); +} + +// DCOP function +void +KCookieServer::deleteSessionCookies( long windowId ) +{ + mCookieJar->eatSessionCookies( windowId ); + saveCookieJar(); +} + +void +KCookieServer::deleteSessionCookiesFor(QString fqdn, long windowId) +{ + mCookieJar->eatSessionCookies( fqdn, windowId ); + saveCookieJar(); +} + +// DCOP function +void +KCookieServer::deleteAllCookies() +{ + mCookieJar->eatAllCookies(); + saveCookieJar(); +} + +// DCOP function +void +KCookieServer::addDOMCookies(QString arg1, QCString arg2, long arg3) +{ + addCookies(arg1, arg2, arg3, true); +} + +// DCOP function +void +KCookieServer::setDomainAdvice(QString url, QString advice) +{ + QString fqdn; + QString dummy; + if (KCookieJar::parseURL(url, fqdn, dummy)) + { + QStringList domains; + mCookieJar->extractDomains(fqdn, domains); + + mCookieJar->setDomainAdvice(domains[domains.count() > 3 ? 3 : 0], + KCookieJar::strToAdvice(advice)); + // Save the cookie config if it has changed + mCookieJar->saveConfig( mConfig ); + } +} + +// DCOP function +QString +KCookieServer::getDomainAdvice(QString url) +{ + KCookieAdvice advice = KCookieDunno; + QString fqdn; + QString dummy; + if (KCookieJar::parseURL(url, fqdn, dummy)) + { + QStringList domains; + mCookieJar->extractDomains(fqdn, domains); + + QStringList::ConstIterator it = domains.begin(); + while ( (advice == KCookieDunno) && (it != domains.end()) ) + { + // Always check advice in both ".domain" and "domain". Note + // that we only want to check "domain" if it matches the + // fqdn of the requested URL. + if ( (*it)[0] == '.' || (*it) == fqdn ) + advice = mCookieJar->getDomainAdvice(*it); + ++it; + } + if (advice == KCookieDunno) + advice = mCookieJar->getGlobalAdvice(); + } + return KCookieJar::adviceToStr(advice); +} + +// DCOP function +void +KCookieServer::reloadPolicy() +{ + mCookieJar->loadConfig( mConfig, true ); +} + +// DCOP function +void +KCookieServer::shutdown() +{ + deleteLater(); +} + +#include "kcookieserver.moc" + diff --git a/kioslave/http/kcookiejar/kcookieserver.h b/kioslave/http/kcookiejar/kcookieserver.h new file mode 100644 index 000000000..bcd7fa530 --- /dev/null +++ b/kioslave/http/kcookiejar/kcookieserver.h @@ -0,0 +1,98 @@ +/* + This file is part of the KDE File Manager + + Copyright (C) 1998 Waldo Bastian (bastian@kde.org) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + + This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +//---------------------------------------------------------------------------- +// +// KDE Cookie Server +// $Id$ + +#ifndef KCOOKIESERVER_H +#define KCOOKIESERVER_H + +#include <qstringlist.h> +#include <kded/kdedmodule.h> + +class KHttpCookieList; +class KCookieJar; +class KHttpCookie; +class QTimer; +class RequestList; +class DCOPClient; +class KConfig; + +class KCookieServer : public KDEDModule +{ + Q_OBJECT + K_DCOP +public: + KCookieServer(const QCString &); + ~KCookieServer(); + +k_dcop: + QString findCookies(QString); + QString findCookies(QString, long); + QStringList findDomains(); + QStringList findCookies(QValueList<int>,QString,QString,QString,QString); + QString findDOMCookies(QString); + QString findDOMCookies(QString, long); + void addCookies(QString, QCString, long); + void deleteCookie(QString, QString, QString, QString); + void deleteCookiesFromDomain(QString); + void deleteSessionCookies(long); + void deleteSessionCookiesFor(QString, long); + void deleteAllCookies(); + void addDOMCookies(QString, QCString, long); + /** + * Sets the cookie policy for the domain associated with the specified URL. + */ + void setDomainAdvice(QString url, QString advice); + /** + * Returns the cookie policy in effect for the specified URL. + */ + QString getDomainAdvice(QString url); + void reloadPolicy(); + void shutdown(); + +public: + bool cookiesPending(const QString &url, KHttpCookieList *cookieList=0); + void addCookies(const QString &url, const QCString &cookieHeader, + long windowId, bool useDOMFormat); + void checkCookies(KHttpCookieList *cookieList); + +public slots: + void slotSave(); + void slotDeleteSessionCookies(long); + +protected: + KCookieJar *mCookieJar; + KHttpCookieList *mPendingCookies; + RequestList *mRequestList; + QTimer *mTimer; + bool mAdvicePending; + DCOPClient *mOldCookieServer; + KConfig *mConfig; + +private: + virtual int newInstance(QValueList<QCString>) { return 0; } + bool cookieMatches(KHttpCookie*, QString, QString, QString, QString); + void putCookie(QStringList&, KHttpCookie*, const QValueList<int>&); + void saveCookieJar(); +}; + +#endif diff --git a/kioslave/http/kcookiejar/kcookiewin.cpp b/kioslave/http/kcookiejar/kcookiewin.cpp new file mode 100644 index 000000000..5c68f8c1e --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiewin.cpp @@ -0,0 +1,382 @@ +/* +This file is part of KDE + + Copyright (C) 2000- Waldo Bastian <bastian@kde.org> + Copyright (C) 2000- Dawit Alemayehu <adawit@kde.org> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +//---------------------------------------------------------------------------- +// +// KDE File Manager -- HTTP Cookie Dialogs +// $Id$ + +// The purpose of the QT_NO_TOOLTIP and QT_NO_WHATSTHIS ifdefs is because +// this file is also used in Konqueror/Embedded. One of the aims of +// Konqueror/Embedded is to be a small as possible to fit on embedded +// devices. For this it's also useful to strip out unneeded features of +// Qt, like for example QToolTip or QWhatsThis. The availability (or the +// lack thereof) can be determined using these preprocessor defines. +// The same applies to the QT_NO_ACCEL ifdef below. I hope it doesn't make +// too much trouble... (Simon) + +#include <qhbox.h> +#include <qvbox.h> +#include <qaccel.h> +#include <qlabel.h> +#include <qwidget.h> +#include <qlayout.h> +#include <qgroupbox.h> +#include <qdatetime.h> +#include <qmessagebox.h> +#include <qpushbutton.h> +#include <qradiobutton.h> +#include <qvbuttongroup.h> + +#ifndef QT_NO_TOOLTIP +#include <qtooltip.h> +#endif + +#ifndef QT_NO_WHATSTHIS +#include <qwhatsthis.h> +#endif + +#include <kidna.h> +#include <kwin.h> +#include <klocale.h> +#include <kglobal.h> +#include <kurllabel.h> +#include <klineedit.h> +#include <kiconloader.h> +#include <kapplication.h> + +#ifdef Q_WS_X11 +#include <X11/Xlib.h> +#endif + +#include "kcookiejar.h" +#include "kcookiewin.h" + +KCookieWin::KCookieWin( QWidget *parent, KHttpCookieList cookieList, + int defaultButton, bool showDetails ) + :KDialog( parent, "cookiealert", true ) +{ +#ifndef Q_WS_QWS //FIXME(E): Implement for Qt Embedded + setCaption( i18n("Cookie Alert") ); + setIcon( SmallIcon("cookie") ); + // all cookies in the list should have the same window at this time, so let's take the first +# ifdef Q_WS_X11 + if( cookieList.first()->windowIds().count() > 0 ) + { + XSetTransientForHint( qt_xdisplay(), winId(), cookieList.first()->windowIds().first()); + } + else + { + // No window associated... make sure the user notices our dialog. + KWin::setState( winId(), NET::KeepAbove ); + kapp->updateUserTimestamp(); + } +# endif +#endif + // Main widget's layout manager... + QVBoxLayout* vlayout = new QVBoxLayout( this, KDialog::marginHint(), KDialog::spacingHint() ); + vlayout->setResizeMode( QLayout::Fixed ); + + // Cookie image and message to user + QHBox* hBox = new QHBox( this ); + hBox->setSpacing( KDialog::spacingHint() ); + QLabel* icon = new QLabel( hBox ); + icon->setPixmap( QMessageBox::standardIcon(QMessageBox::Warning) ); + icon->setAlignment( Qt::AlignCenter ); + icon->setFixedSize( 2*icon->sizeHint() ); + + int count = cookieList.count(); + + QVBox* vBox = new QVBox( hBox ); + QString txt = i18n("You received a cookie from", + "You received %n cookies from", count); + QLabel* lbl = new QLabel( txt, vBox ); + lbl->setAlignment( Qt::AlignCenter ); + KHttpCookiePtr cookie = cookieList.first(); + + QString host (cookie->host()); + int pos = host.find(':'); + if ( pos > 0 ) + { + QString portNum = host.left(pos); + host.remove(0, pos+1); + host += ':'; + host += portNum; + } + + txt = QString("<b>%1</b>").arg( KIDNA::toUnicode(host) ); + if (cookie->isCrossDomain()) + txt += i18n(" <b>[Cross Domain!]</b>"); + lbl = new QLabel( txt, vBox ); + lbl->setAlignment( Qt::AlignCenter ); + lbl = new QLabel( i18n("Do you want to accept or reject?"), vBox ); + lbl->setAlignment( Qt::AlignCenter ); + vlayout->addWidget( hBox, 0, Qt::AlignLeft ); + + // Cookie Details dialog... + m_detailView = new KCookieDetail( cookieList, count, this ); + vlayout->addWidget( m_detailView ); + m_showDetails = showDetails; + m_showDetails ? m_detailView->show():m_detailView->hide(); + + // Cookie policy choice... + m_btnGrp = new QVButtonGroup( i18n("Apply Choice To"), this ); + m_btnGrp->setRadioButtonExclusive( true ); + + txt = (count == 1)? i18n("&Only this cookie") : i18n("&Only these cookies"); + QRadioButton* rb = new QRadioButton( txt, m_btnGrp ); +#ifndef QT_NO_WHATSTHIS + QWhatsThis::add( rb, i18n("Select this option to accept/reject only this cookie. " + "You will be prompted if another cookie is received. " + "<em>(see WebBrowsing/Cookies in the Control Center)</em>." ) ); +#endif + m_btnGrp->insert( rb ); + rb = new QRadioButton( i18n("All cookies from this do&main"), m_btnGrp ); +#ifndef QT_NO_WHATSTHIS + QWhatsThis::add( rb, i18n("Select this option to accept/reject all cookies from " + "this site. Choosing this option will add a new policy for " + "the site this cookie originated from. This policy will be " + "permanent until you manually change it from the Control Center " + "<em>(see WebBrowsing/Cookies in the Control Center)</em>.") ); +#endif + m_btnGrp->insert( rb ); + rb = new QRadioButton( i18n("All &cookies"), m_btnGrp ); +#ifndef QT_NO_WHATSTHIS + QWhatsThis::add( rb, i18n("Select this option to accept/reject all cookies from " + "anywhere. Choosing this option will change the global " + "cookie policy set in the Control Center for all cookies " + "<em>(see WebBrowsing/Cookies in the Control Center)</em>.") ); +#endif + m_btnGrp->insert( rb ); + vlayout->addWidget( m_btnGrp ); + + if ( defaultButton > -1 && defaultButton < 3 ) + m_btnGrp->setButton( defaultButton ); + else + m_btnGrp->setButton( 1 ); + + // Accept/Reject buttons + QWidget* bbox = new QWidget( this ); + QBoxLayout* bbLay = new QHBoxLayout( bbox ); + bbLay->setSpacing( KDialog::spacingHint() ); + QPushButton* btn = new QPushButton( i18n("&Accept"), bbox ); + btn->setDefault( true ); + btn->setFocus(); + connect( btn, SIGNAL(clicked()), SLOT(accept()) ); + bbLay->addWidget( btn ); + btn = new QPushButton( i18n("&Reject"), bbox ); + connect( btn, SIGNAL(clicked()), SLOT(reject()) ); + bbLay->addWidget( btn ); + bbLay->addStretch( 1 ); +#ifndef QT_NO_ACCEL + QAccel* a = new QAccel( this ); + a->connectItem( a->insertItem(Qt::Key_Escape), btn, SLOT(animateClick()) ); +#endif + + m_button = new QPushButton( bbox ); + m_button->setText( m_showDetails ? i18n("&Details <<"):i18n("&Details >>") ); + connect( m_button, SIGNAL(clicked()), SLOT(slotCookieDetails()) ); + bbLay->addWidget( m_button ); +#ifndef QT_NO_WHATSTHIS + QWhatsThis::add( m_button, i18n("See or modify the cookie information") ); +#endif + + + vlayout->addWidget( bbox ); + setFixedSize( sizeHint() ); +} + +KCookieWin::~KCookieWin() +{ +} + +void KCookieWin::slotCookieDetails() +{ + if ( m_detailView->isVisible() ) + { + m_detailView->setMaximumSize( 0, 0 ); + m_detailView->adjustSize(); + m_detailView->hide(); + m_button->setText( i18n( "&Details >>" ) ); + m_showDetails = false; + } + else + { + m_detailView->setMaximumSize( 1000, 1000 ); + m_detailView->adjustSize(); + m_detailView->show(); + m_button->setText( i18n( "&Details <<" ) ); + m_showDetails = true; + } +} + +KCookieAdvice KCookieWin::advice( KCookieJar *cookiejar, KHttpCookie* cookie ) +{ + int result = exec(); + + cookiejar->setShowCookieDetails ( m_showDetails ); + + KCookieAdvice advice = (result==QDialog::Accepted) ? KCookieAccept:KCookieReject; + + int preferredPolicy = m_btnGrp->id( m_btnGrp->selected() ); + cookiejar->setPreferredDefaultPolicy( preferredPolicy ); + + switch ( preferredPolicy ) + { + case 2: + cookiejar->setGlobalAdvice( advice ); + break; + case 1: + cookiejar->setDomainAdvice( cookie, advice ); + break; + case 0: + default: + break; + } + return advice; +} + +KCookieDetail::KCookieDetail( KHttpCookieList cookieList, int cookieCount, + QWidget* parent, const char* name ) + :QGroupBox( parent, name ) +{ + setTitle( i18n("Cookie Details") ); + QGridLayout* grid = new QGridLayout( this, 9, 2, + KDialog::spacingHint(), + KDialog::marginHint() ); + grid->addRowSpacing( 0, fontMetrics().lineSpacing() ); + grid->setColStretch( 1, 3 ); + + QLabel* label = new QLabel( i18n("Name:"), this ); + grid->addWidget( label, 1, 0 ); + m_name = new KLineEdit( this ); + m_name->setReadOnly( true ); + m_name->setMaximumWidth( fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_name, 1 ,1 ); + + //Add the value + label = new QLabel( i18n("Value:"), this ); + grid->addWidget( label, 2, 0 ); + m_value = new KLineEdit( this ); + m_value->setReadOnly( true ); + m_value->setMaximumWidth( fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_value, 2, 1); + + label = new QLabel( i18n("Expires:"), this ); + grid->addWidget( label, 3, 0 ); + m_expires = new KLineEdit( this ); + m_expires->setReadOnly( true ); + m_expires->setMaximumWidth(fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_expires, 3, 1); + + label = new QLabel( i18n("Path:"), this ); + grid->addWidget( label, 4, 0 ); + m_path = new KLineEdit( this ); + m_path->setReadOnly( true ); + m_path->setMaximumWidth( fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_path, 4, 1); + + label = new QLabel( i18n("Domain:"), this ); + grid->addWidget( label, 5, 0 ); + m_domain = new KLineEdit( this ); + m_domain->setReadOnly( true ); + m_domain->setMaximumWidth( fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_domain, 5, 1); + + label = new QLabel( i18n("Exposure:"), this ); + grid->addWidget( label, 6, 0 ); + m_secure = new KLineEdit( this ); + m_secure->setReadOnly( true ); + m_secure->setMaximumWidth( fontMetrics().maxWidth() * 25 ); + grid->addWidget( m_secure, 6, 1 ); + + if ( cookieCount > 1 ) + { + QPushButton* btnNext = new QPushButton( i18n("Next cookie","&Next >>"), this ); + btnNext->setFixedSize( btnNext->sizeHint() ); + grid->addMultiCellWidget( btnNext, 8, 8, 0, 1 ); + connect( btnNext, SIGNAL(clicked()), SLOT(slotNextCookie()) ); +#ifndef QT_NO_TOOLTIP + QToolTip::add( btnNext, i18n("Show details of the next cookie") ); +#endif + } + m_cookieList = cookieList; + m_cookie = 0; + slotNextCookie(); +} + +KCookieDetail::~KCookieDetail() +{ +} + +void KCookieDetail::slotNextCookie() +{ + KHttpCookiePtr cookie = m_cookieList.first(); + if (m_cookie) while(cookie) + { + if (cookie == m_cookie) + { + cookie = m_cookieList.next(); + break; + } + cookie = m_cookieList.next(); + } + m_cookie = cookie; + if (!m_cookie) + m_cookie = m_cookieList.first(); + + if ( m_cookie ) + { + m_name->setText( m_cookie->name() ); + m_value->setText( ( m_cookie->value() ) ); + if ( m_cookie->domain().isEmpty() ) + m_domain->setText( i18n("Not specified") ); + else + m_domain->setText( m_cookie->domain() ); + m_path->setText( m_cookie->path() ); + QDateTime cookiedate; + cookiedate.setTime_t( m_cookie->expireDate() ); + if ( m_cookie->expireDate() ) + m_expires->setText( KGlobal::locale()->formatDateTime(cookiedate) ); + else + m_expires->setText( i18n("End of Session") ); + QString sec; + if (m_cookie->isSecure()) + { + if (m_cookie->isHttpOnly()) + sec = i18n("Secure servers only"); + else + sec = i18n("Secure servers, page scripts"); + } + else + { + if (m_cookie->isHttpOnly()) + sec = i18n("Servers"); + else + sec = i18n("Servers, page scripts"); + } + m_secure->setText( sec ); + } +} + +#include "kcookiewin.moc" diff --git a/kioslave/http/kcookiejar/kcookiewin.h b/kioslave/http/kcookiejar/kcookiewin.h new file mode 100644 index 000000000..30e92e7e0 --- /dev/null +++ b/kioslave/http/kcookiejar/kcookiewin.h @@ -0,0 +1,84 @@ +/* + This file is part of the KDE File Manager + + Copyright (C) 1998- Waldo Bastian (bastian@kde.org) + Copyright (C) 2000- Dawit Alemayehu (adawit@kde.org) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +//---------------------------------------------------------------------------- +// +// KDE File Manager -- HTTP Cookie Dialogs +// $Id$ + +#ifndef _KCOOKIEWIN_H_ +#define _KCOOKIEWIN_H_ + +#include <qgroupbox.h> + +#include <kdialog.h> +#include "kcookiejar.h" + +class KLineEdit; +class QPushButton; +class QVButtonGroup; +class KURLLabel; + +class KCookieDetail : public QGroupBox +{ + Q_OBJECT + +public : + KCookieDetail( KHttpCookieList cookieList, int cookieCount, QWidget *parent=0, + const char *name=0 ); + ~KCookieDetail(); + +private : + KLineEdit* m_name; + KLineEdit* m_value; + KLineEdit* m_expires; + KLineEdit* m_domain; + KLineEdit* m_path; + KLineEdit* m_secure; + + KHttpCookieList m_cookieList; + KHttpCookiePtr m_cookie; + +private slots: + void slotNextCookie(); +}; + +class KCookieWin : public KDialog +{ + Q_OBJECT + +public : + KCookieWin( QWidget *parent, KHttpCookieList cookieList, int defaultButton=0, + bool showDetails=false ); + ~KCookieWin(); + + KCookieAdvice advice( KCookieJar *cookiejar, KHttpCookie* cookie ); + +private : + QPushButton* m_button; + QVButtonGroup* m_btnGrp; + KCookieDetail* m_detailView; + bool m_showDetails; + +private slots: + void slotCookieDetails(); +}; +#endif diff --git a/kioslave/http/kcookiejar/main.cpp b/kioslave/http/kcookiejar/main.cpp new file mode 100644 index 000000000..1e943b939 --- /dev/null +++ b/kioslave/http/kcookiejar/main.cpp @@ -0,0 +1,92 @@ +/* +This file is part of KDE + + Copyright (C) 1998-2000 Waldo Bastian (bastian@kde.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include <dcopclient.h> +#include <kcmdlineargs.h> +#include <klocale.h> +#include <kapplication.h> + +static const char description[] = + I18N_NOOP("HTTP Cookie Daemon"); + +static const char version[] = "1.0"; + +static const KCmdLineOptions options[] = +{ + { "shutdown", I18N_NOOP("Shut down cookie jar"), 0 }, + { "remove <domain>", I18N_NOOP("Remove cookies for domain"), 0 }, + { "remove-all", I18N_NOOP("Remove all cookies"), 0 }, + { "reload-config", I18N_NOOP("Reload configuration file"), 0 }, + KCmdLineLastOption +}; + +extern "C" KDE_EXPORT int kdemain(int argc, char *argv[]) +{ + KLocale::setMainCatalogue("kdelibs"); + KCmdLineArgs::init(argc, argv, "kcookiejar", I18N_NOOP("HTTP cookie daemon"), + description, version); + + KCmdLineArgs::addCmdLineOptions( options ); + + KInstance a("kcookiejar"); + + kapp->dcopClient()->attach(); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + QCString replyType; + QByteArray replyData; + if (args->isSet("remove-all")) + { + kapp->dcopClient()->call( "kded", "kcookiejar", "deleteAllCookies()", QByteArray(), replyType, replyData); + } + if (args->isSet("remove")) + { + QString domain = args->getOption("remove"); + QByteArray params; + QDataStream stream(params, IO_WriteOnly); + stream << domain; + kapp->dcopClient()->call( "kded", "kcookiejar", "deleteCookiesFromDomain(QString)", params, replyType, replyData); + } + if (args->isSet("shutdown")) + { + QCString module = "kcookiejar"; + QByteArray params; + QDataStream stream(params, IO_WriteOnly); + stream << module; + kapp->dcopClient()->call( "kded", "kded", "unloadModule(QCString)", params, replyType, replyData); + } + else if(args->isSet("reload-config")) + { + kapp->dcopClient()->call( "kded", "kcookiejar", "reloadPolicy()", QByteArray(), replyType, replyData); + } + else + { + QCString module = "kcookiejar"; + QByteArray params; + QDataStream stream(params, IO_WriteOnly); + stream << module; + kapp->dcopClient()->call( "kded", "kded", "loadModule(QCString)", params, replyType, replyData); + } + + return 0; +} diff --git a/kioslave/http/kcookiejar/netscape_cookie_spec.html b/kioslave/http/kcookiejar/netscape_cookie_spec.html new file mode 100644 index 000000000..eb190f2e3 --- /dev/null +++ b/kioslave/http/kcookiejar/netscape_cookie_spec.html @@ -0,0 +1,331 @@ +<HTML> +<HEAD> +<TITLE>Client Side State - HTTP Cookies</TITLE> +</HEAD> + +<BODY BGCOLOR="#ffffff" LINK="#0000ff" VLINK="#ff0000" ALINK="#ff0000" TEXT="#000000" > + + +<CENTER> +<!-- BANNER:s3 --> +<A HREF="/maps/banners/documentation_s3.map"><IMG SRC="/images/banners/documentation_s3.gif" ALT="Documentation" BORDER=0 WIDTH=612 HEIGHT=50 ISMAP USEMAP="#banner_nav"></A> +<MAP NAME="banner_nav"> +<AREA SHAPE=RECT COORDS="62,11,91,40" HREF="/"> +<AREA SHAPE=RECT COORDS="153,41,221,50" HREF="/"> +<AREA SHAPE=RECT COORDS="298,8,374,34" HREF="/support/index.html"> +<AREA SHAPE=RECT COORDS="381,15,586,43" HREF="http://help.netscape.com/browse/index.html"> +<AREA SHAPE=default NOHREF> +</MAP> + +<!-- BANNER:s3 --> + +<H2> +<FONT SIZE=+3>P</FONT>ERSISTENT +<FONT SIZE=+3>C</FONT>LIENT +<FONT SIZE=+3>S</FONT>TATE<BR> +<FONT SIZE=+3>HTTP C</FONT>OOKIES +</H2> + +<H3>Preliminary Specification - Use with caution</H3> +</CENTER> + +<HR SIZE=4> + +<CENTER> +<H3> +<FONT SIZE=+2>I</FONT>NTRODUCTION +</H3> +</CENTER> + +Cookies are a general mechanism which server side connections (such as +CGI scripts) can use to both store and retrieve information on the +client side of the connection. The addition of a simple, persistent, +client-side state significantly extends the capabilities of Web-based +client/server applications.<P> + +<CENTER> +<H3> +<FONT SIZE=+2>O</FONT>VERVIEW +</H3> +</CENTER> + +A server, when returning an HTTP object to a client, may also send a +piece of state information which the client will store. Included in that +state object is a description of the range of URLs for which that state is +valid. Any future HTTP requests made by the client which fall in that +range will include a transmittal of the current value of the state +object from the client back to the server. The state object is called +a <B>cookie</B>, for no compelling reason. <P> +This simple mechanism provides a powerful new tool which enables a host +of new types of applications to be written for web-based environments. +Shopping applications can now store information about the currently +selected items, for fee services can send back registration information +and free the client from retyping a user-id on next connection, +sites can store per-user preferences on the client, and have the client supply +those preferences every time that site is connected to. + +<CENTER> +<H3> +<FONT SIZE=+2>S</FONT>PECIFICATION +</H3> +</CENTER> + +A cookie is introduced to the client by including a <B>Set-Cookie</B> +header as part of an HTTP response, typically this will be generated +by a CGI script. + +<H3>Syntax of the Set-Cookie HTTP Response Header</H3> + +This is the format a CGI script would use to add to the HTTP headers +a new piece of data which is to be stored by the client for later retrieval. + +<PRE> +Set-Cookie: <I>NAME</I>=<I>VALUE</I>; expires=<I>DATE</I>; +path=<I>PATH</I>; domain=<I>DOMAIN_NAME</I>; secure +</PRE> +<DL> +<DT> <I>NAME</I>=<I>VALUE</I><DD> +This string is a sequence of characters excluding semi-colon, comma and white +space. If there is a need to place such data in the name or value, some +encoding method such as URL style %XX encoding is recommended, though no +encoding is defined or required. <P> This is the only required attribute +on the <B>Set-Cookie</B> header. <P> +<DT><B>expires</B>=<I>DATE</I> +<DD> +The <B>expires</B> attribute specifies a date string that +defines the valid life time of that cookie. Once the expiration +date has been reached, the cookie will no longer be stored or +given out. <P> +The date string is formatted as: +<BLOCKQUOTE> <TT>Wdy, DD-Mon-YYYY HH:MM:SS GMT</TT></BLOCKQUOTE> +This is based on +<A TARGET="_top" HREF="http://ds.internic.net/rfc/rfc822.txt">RFC 822</A>, +<A TARGET="_top" HREF="http://ds.internic.net/rfc/rfc850.txt">RFC 850</A>, +<A TARGET="_top" HREF="http://www.w3.org/hypertext/WWW/Protocols/rfc1036/rfc1036.html#z6"> +RFC 1036</A>, and +<A TARGET="_top" HREF="http://ds1.internic.net/rfc/rfc1123.txt"> +RFC 1123</A>, +with the variations that the only legal time zone is <B>GMT</B> and +the separators between the elements of the date must be dashes. +<P> +<B>expires</B> is an optional attribute. If not specified, the cookie will +expire when the user's session ends. <P> +<B>Note:</B> There is a bug in Netscape Navigator version 1.1 and earlier. +Only cookies whose <B>path</B> attribute is set explicitly to "/" will +be properly saved between sessions if they have an <B>expires</B> +attribute.<P> + +<DT> <B>domain</B>=<I>DOMAIN_NAME</I> +<DD> +When searching the cookie list for valid cookies, a comparison of the +<B>domain</B> +attributes of the cookie is made with the Internet domain name of the +host from which the URL will be fetched. If there is a tail match, +then the cookie will go through <B>path</B> matching to see if it +should be sent. "Tail matching" means that <B>domain</B> attribute +is matched against the tail of the fully qualified domain name of +the host. A <B>domain</B> attribute of "acme.com" would match +host names "anvil.acme.com" as well as "shipping.crate.acme.com". <P> + +Only hosts within the specified domain +can set a cookie for a domain and domains must have at least two (2) +or three (3) periods in them to prevent domains of the form: +".com", ".edu", and "va.us". Any domain that fails within +one of the seven special top level domains listed below only require +two periods. Any other domain requires at least three. The +seven special top level domains are: "COM", "EDU", "NET", "ORG", +"GOV", "MIL", and "INT". + + <P> +The default value of <B>domain</B> is the host name of the server +which generated the cookie response. <P> +<DT> <B>path</B>=<I>PATH</I> +<DD> +The <B>path</B> attribute is used to specify the subset of URLs in a +domain for +which the cookie is valid. If a cookie has already passed <B>domain</B> +matching, then the pathname component +of the URL is compared with the path attribute, and if there is +a match, the cookie is considered valid and is sent along with +the URL request. The path "/foo" +would match "/foobar" and "/foo/bar.html". The path "/" is the most +general path. <P> +If the <B>path</B> is not specified, it as assumed to be the same path +as the document being described by the header which contains the cookie. +<P> +<DT> <B>secure</B> +<DD> +If a cookie is marked <B>secure</B>, it will only be transmitted if the +communications channel with the host is a secure one. Currently +this means that secure cookies will only be sent to HTTPS (HTTP over SSL) +servers. <P> +If <B>secure</B> is not specified, a cookie is considered safe to be sent +in the clear over unsecured channels. +</DL> + +<H3>Syntax of the Cookie HTTP Request Header</H3> + +When requesting a URL from an HTTP server, the browser will match +the URL against all cookies and if any of them match, a line +containing the name/value pairs of all matching cookies will +be included in the HTTP request. Here is the format of that line: +<PRE> +Cookie: <I>NAME1=OPAQUE_STRING1</I>; <I>NAME2=OPAQUE_STRING2 ...</I> +</PRE> + +<H3>Additional Notes</H3> + +<UL> +<LI>Multiple <B>Set-Cookie</B> headers can be issued in a single server +response. +<p> +<LI>Instances of the same path and name will overwrite each other, with the +latest instance taking precedence. Instances of the same path but +different names will add additional mappings. +<p> +<LI>Setting the path to a higher-level value does not override other more +specific path mappings. If there are multiple matches for a given cookie +name, but with separate paths, all the matching cookies will be sent. +(See examples below.) +<p> +<LI>The +expires header lets the client know when it is safe to purge the mapping +but the client is not required to do so. A client may also delete a +cookie before it's expiration date arrives if the number of cookies +exceeds its internal limits. +<p> +<LI>When sending cookies to a server, all cookies with a more specific +path mapping should be sent before cookies with less specific path +mappings. For example, a cookie "name1=foo" with a path mapping +of "/" should be sent after a cookie "name1=foo2" with +a path mapping of "/bar" if they are both to be sent. +<p> +<LI>There are limitations on the number of cookies that a client +can store at any one time. This is a specification of the minimum +number of cookies that a client should be prepared to receive and +store. + +<UL> + <LI>300 total cookies + <LI>4 kilobytes per cookie, where the name and the OPAQUE_STRING + combine to form the 4 kilobyte limit. + <LI>20 cookies per server or domain. (note that completely + specified hosts and domains are treated as separate entities + and have a 20 cookie limitation for each, not combined) +</UL> +Servers should not expect clients to be able to exceed these limits. +When the 300 cookie limit or the 20 cookie per server limit +is exceeded, clients should delete the least recently used cookie. +When a cookie larger than 4 kilobytes is encountered the cookie +should be trimmed to fit, but the name should remain intact +as long as it is less than 4 kilobytes. + <P> +<LI>If a CGI script wishes to delete a cookie, it can do so by +returning a cookie with the same name, and an <B>expires</B> time +which is in the past. The path and name must match exactly +in order for the expiring cookie to replace the valid cookie. +This requirement makes it difficult for anyone but the originator +of a cookie to delete a cookie. +<P><LI>When caching HTTP, as a proxy server might do, the <B>Set-cookie</B> +response header should never be cached. +<P><LI>If a proxy server receives a response which +contains a <B>Set-cookie</B> header, it should propagate the <B>Set-cookie</B> +header to the client, regardless of whether the response was 304 +(Not Modified) or 200 (OK). +<P>Similarly, if a client request contains a Cookie: header, it +should be forwarded through a proxy, even if the conditional +If-modified-since request is being made. +</UL> + +<CENTER> +<H3> +<FONT SIZE=+2>E</FONT>XAMPLES +</H3> +</CENTER> + +Here are some sample exchanges which are designed to illustrate the use +of cookies. +<H3>First Example transaction sequence:</H3> +<DL> +<dt>Client requests a document, and receives in the response:<dd> +<PRE> +Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT</PRE> +<dt>When client requests a URL in path "/" on this server, it sends:<DD> +<PRE>Cookie: CUSTOMER=WILE_E_COYOTE</PRE> +<dt>Client requests a document, and receives in the response:<dd> +<PRE>Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/</PRE> +<dt>When client requests a URL in path "/" on this server, it sends:<dd> +<PRE>Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001</PRE> +<dt>Client receives:<dd> +<PRE>Set-Cookie: SHIPPING=FEDEX; path=/foo</PRE> +<dt>When client requests a URL in path "/" on this server, it sends:<dd> +<PRE>Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001</PRE> +<dt>When client requests a URL in path "/foo" on this server, it sends:<dd> +<PRE>Cookie: CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001; SHIPPING=FEDEX</PRE> +</DL> +<H3>Second Example transaction sequence:</H3> +<DL> +<dt>Assume all mappings from above have been cleared.<p> +<dt>Client receives:<dd> +<PRE>Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/</PRE> +<dt>When client requests a URL in path "/" on this server, it sends:<dd> +<PRE>Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001</PRE> +<dt>Client receives:<dd> +<PRE>Set-Cookie: PART_NUMBER=RIDING_ROCKET_0023; path=/ammo</PRE> +<dt>When client requests a URL in path "/ammo" on this server, it sends:<dd> +<PRE>Cookie: PART_NUMBER=RIDING_ROCKET_0023; PART_NUMBER=ROCKET_LAUNCHER_0001</PRE> +<dd>NOTE: There are two name/value pairs named "PART_NUMBER" due to the +inheritance +of the "/" mapping in addition to the "/ammo" mapping. +</DL> + +<HR SIZE=4> +<P> + +<CENTER> + + +<!-- footer --> +<TABLE WIDTH=600 BORDER=0 CELLPADDING=0 CELLSPACING=0> +<TR> +<TD WIDTH=600 HEIGHT=8><HR SIZE=1 NOSHADE></TD></TR> +<TR><TD ALIGN=LEFT VALIGN=TOP><FONT FACE="sans-serif, Arial, Helvetica" SIZE=-2><A HREF="http://home.netscape.com/misc/nav_redir/help.html" TARGET="_top">Help</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/site_map.html" TARGET="_top">Site Map</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/howtoget.html" TARGET="_top">How to Get Netscape Products</A> | <A HREF="http://home.netscape.com/misc/nav_redir/ad.html" TARGET="_top">Advertise With Us</A> | <A HREF="http://home.netscape.com/misc/nav_redir/addsite.html" TARGET="_top">Add Site</A> | <A HREF="http://home.netscape.com/misc/nav_redir/custom_browser.html" TARGET="_top">Custom Browser Program</A></FONT></TD></TR> +<TR> +<TD WIDTH=600 HEIGHT=8 COLSPAN=0></TD> +</TR> + +<TR> +<TD ALIGN=LEFT VALIGN=TOP> +<!-- Channels --> +<FONT FACE="sans-serif, Arial, Helvetica" SIZE=-2><A HREF="http://home.netscape.com/misc/nav_redir/channels/autos.html" TARGET="_top">Autos</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/business.html" TARGET="_top">Business</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/computers_internet.html" TARGET="_top">Computing & Internet</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/entertainment.html" TARGET="_top">Entertainment</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/kids_family.html" TARGET="_top">Family</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/games.html" TARGET="_top">Games</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/health.html" TARGET="_top">Health</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/lifestyles.html" TARGET="_top">Lifestyles</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/local.html" TARGET="_top">Local</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/netscape.html" TARGET="_top">Netscape</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/open_directory.html">Netscape Open Directory</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/news.html" TARGET="_top">News</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/personalize_finance.html" TARGET="_top">Personal Finance</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/real_estate.html" TARGET="_top">Real Estate</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/education.html" TARGET="_top">Research & Learn</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/shopping.html" TARGET="_top">Shopping</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/smallbiz.html" TARGET="_top">Small Business</A> | <A +HREF="http://home.netscape.com/misc/nav_redir/channels/sports.html" TARGET="_top">Sports</A> | <A HREF="http://home.netscape.com/misc/nav_redir/channels/travel.html" TARGET="_top">Travel</A></FONT></TD></TR> +</TABLE> + +<TABLE WIDTH=600 BORDER=0 CELLPADDING=0 CELLSPACING=0> +<TR><TD WIDTH=600 HEIGHT=8 COLSPAN=0></TD></TR> +<TR> +<TD WIDTH=600 COLSPAN=5 VALIGN=TOP ALIGN=LEFT> +<FONT FACE="sans-serif, Arial, Helvetica" SIZE=-2> +© 1999 Netscape, All Rights Reserved. <A HREF="http://home.netscape.com/legal_notices/index.html">Legal & Privacy Notices</A><BR>This site powered by <A HREF="http://home.netscape.com/comprod/server_central/index.html" TARGET="_top">Netscape SuiteSpot servers</A>.</FONT></TD> +</TR> +</TABLE> +<!-- end footer --> + + + + +</CENTER> +<P> + + + +</BODY> +</HTML>
\ No newline at end of file diff --git a/kioslave/http/kcookiejar/rfc2109 b/kioslave/http/kcookiejar/rfc2109 new file mode 100644 index 000000000..432fdcc6e --- /dev/null +++ b/kioslave/http/kcookiejar/rfc2109 @@ -0,0 +1,1179 @@ + + + + + + +Network Working Group D. Kristol +Request for Comments: 2109 Bell Laboratories, Lucent Technologies +Category: Standards Track L. Montulli + Netscape Communications + February 1997 + + + HTTP State Management Mechanism + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +1. ABSTRACT + + This document specifies a way to create a stateful session with HTTP + requests and responses. It describes two new headers, Cookie and + Set-Cookie, which carry state information between participating + origin servers and user agents. The method described here differs + from Netscape's Cookie proposal, but it can interoperate with + HTTP/1.0 user agents that use Netscape's method. (See the HISTORICAL + section.) + +2. TERMINOLOGY + + The terms user agent, client, server, proxy, and origin server have + the same meaning as in the HTTP/1.0 specification. + + Fully-qualified host name (FQHN) means either the fully-qualified + domain name (FQDN) of a host (i.e., a completely specified domain + name ending in a top-level domain such as .com or .uk), or the + numeric Internet Protocol (IP) address of a host. The fully + qualified domain name is preferred; use of numeric IP addresses is + strongly discouraged. + + The terms request-host and request-URI refer to the values the client + would send to the server as, respectively, the host (but not port) + and abs_path portions of the absoluteURI (http_URL) of the HTTP + request line. Note that request-host must be a FQHN. + + + + + + + + +Kristol & Montulli Standards Track [Page 1] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + Hosts names can be specified either as an IP address or a FQHN + string. Sometimes we compare one host name with another. Host A's + name domain-matches host B's if + + * both host names are IP addresses and their host name strings match + exactly; or + + * both host names are FQDN strings and their host name strings match + exactly; or + + * A is a FQDN string and has the form NB, where N is a non-empty name + string, B has the form .B', and B' is a FQDN string. (So, x.y.com + domain-matches .y.com but not y.com.) + + Note that domain-match is not a commutative operation: a.b.c.com + domain-matches .c.com, but not the reverse. + + Because it was used in Netscape's original implementation of state + management, we will use the term cookie to refer to the state + information that passes between an origin server and user agent, and + that gets stored by the user agent. + +3. STATE AND SESSIONS + + This document describes a way to create stateful sessions with HTTP + requests and responses. Currently, HTTP servers respond to each + client request without relating that request to previous or + subsequent requests; the technique allows clients and servers that + wish to exchange state information to place HTTP requests and + responses within a larger context, which we term a "session". This + context might be used to create, for example, a "shopping cart", in + which user selections can be aggregated before purchase, or a + magazine browsing system, in which a user's previous reading affects + which offerings are presented. + + There are, of course, many different potential contexts and thus many + different potential types of session. The designers' paradigm for + sessions created by the exchange of cookies has these key attributes: + + 1. Each session has a beginning and an end. + + 2. Each session is relatively short-lived. + + 3. Either the user agent or the origin server may terminate a + session. + + 4. The session is implicit in the exchange of state information. + + + + +Kristol & Montulli Standards Track [Page 2] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +4. OUTLINE + + We outline here a way for an origin server to send state information + to the user agent, and for the user agent to return the state + information to the origin server. The goal is to have a minimal + impact on HTTP and user agents. Only origin servers that need to + maintain sessions would suffer any significant impact, and that + impact can largely be confined to Common Gateway Interface (CGI) + programs, unless the server provides more sophisticated state + management support. (See Implementation Considerations, below.) + +4.1 Syntax: General + + The two state management headers, Set-Cookie and Cookie, have common + syntactic properties involving attribute-value pairs. The following + grammar uses the notation, and tokens DIGIT (decimal digits) and + token (informally, a sequence of non-special, non-white space + characters) from the HTTP/1.1 specification [RFC 2068] to describe + their syntax. + + av-pairs = av-pair *(";" av-pair) + av-pair = attr ["=" value] ; optional value + attr = token + value = word + word = token | quoted-string + + Attributes (names) (attr) are case-insensitive. White space is + permitted between tokens. Note that while the above syntax + description shows value as optional, most attrs require them. + + NOTE: The syntax above allows whitespace between the attribute and + the = sign. + +4.2 Origin Server Role + +4.2.1 General + + The origin server initiates a session, if it so desires. (Note that + "session" here does not refer to a persistent network connection but + to a logical session created from HTTP requests and responses. The + presence or absence of a persistent connection should have no effect + on the use of cookie-derived sessions). To initiate a session, the + origin server returns an extra response header to the client, Set- + Cookie. (The details follow later.) + + A user agent returns a Cookie request header (see below) to the + origin server if it chooses to continue a session. The origin server + may ignore it or use it to determine the current state of the + + + +Kristol & Montulli Standards Track [Page 3] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + session. It may send back to the client a Set-Cookie response header + with the same or different information, or it may send no Set-Cookie + header at all. The origin server effectively ends a session by + sending the client a Set-Cookie header with Max-Age=0. + + Servers may return a Set-Cookie response headers with any response. + User agents should send Cookie request headers, subject to other + rules detailed below, with every request. + + An origin server may include multiple Set-Cookie headers in a + response. Note that an intervening gateway could fold multiple such + headers into a single header. + +4.2.2 Set-Cookie Syntax + + The syntax for the Set-Cookie response header is + + set-cookie = "Set-Cookie:" cookies + cookies = 1#cookie + cookie = NAME "=" VALUE *(";" cookie-av) + NAME = attr + VALUE = value + cookie-av = "Comment" "=" value + | "Domain" "=" value + | "Max-Age" "=" value + | "Path" "=" value + | "Secure" + | "Version" "=" 1*DIGIT + + Informally, the Set-Cookie response header comprises the token Set- + Cookie:, followed by a comma-separated list of one or more cookies. + Each cookie begins with a NAME=VALUE pair, followed by zero or more + semi-colon-separated attribute-value pairs. The syntax for + attribute-value pairs was shown earlier. The specific attributes and + the semantics of their values follows. The NAME=VALUE attribute- + value pair must come first in each cookie. The others, if present, + can occur in any order. If an attribute appears more than once in a + cookie, the behavior is undefined. + + NAME=VALUE + Required. The name of the state information ("cookie") is NAME, + and its value is VALUE. NAMEs that begin with $ are reserved for + other uses and must not be used by applications. + + + + + + + + +Kristol & Montulli Standards Track [Page 4] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + The VALUE is opaque to the user agent and may be anything the + origin server chooses to send, possibly in a server-selected + printable ASCII encoding. "Opaque" implies that the content is of + interest and relevance only to the origin server. The content + may, in fact, be readable by anyone that examines the Set-Cookie + header. + + Comment=comment + Optional. Because cookies can contain private information about a + user, the Cookie attribute allows an origin server to document its + intended use of a cookie. The user can inspect the information to + decide whether to initiate or continue a session with this cookie. + + Domain=domain + Optional. The Domain attribute specifies the domain for which the + cookie is valid. An explicitly specified domain must always start + with a dot. + + Max-Age=delta-seconds + Optional. The Max-Age attribute defines the lifetime of the + cookie, in seconds. The delta-seconds value is a decimal non- + negative integer. After delta-seconds seconds elapse, the client + should discard the cookie. A value of zero means the cookie + should be discarded immediately. + + Path=path + Optional. The Path attribute specifies the subset of URLs to + which this cookie applies. + + Secure + Optional. The Secure attribute (with no value) directs the user + agent to use only (unspecified) secure means to contact the origin + server whenever it sends back this cookie. + + The user agent (possibly under the user's control) may determine + what level of security it considers appropriate for "secure" + cookies. The Secure attribute should be considered security + advice from the server to the user agent, indicating that it is in + the session's interest to protect the cookie contents. + + Version=version + Required. The Version attribute, a decimal integer, identifies to + which version of the state management specification the cookie + conforms. For this specification, Version=1 applies. + + + + + + + +Kristol & Montulli Standards Track [Page 5] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +4.2.3 Controlling Caching + + An origin server must be cognizant of the effect of possible caching + of both the returned resource and the Set-Cookie header. Caching + "public" documents is desirable. For example, if the origin server + wants to use a public document such as a "front door" page as a + sentinel to indicate the beginning of a session for which a Set- + Cookie response header must be generated, the page should be stored + in caches "pre-expired" so that the origin server will see further + requests. "Private documents", for example those that contain + information strictly private to a session, should not be cached in + shared caches. + + If the cookie is intended for use by a single user, the Set-cookie + header should not be cached. A Set-cookie header that is intended to + be shared by multiple users may be cached. + + The origin server should send the following additional HTTP/1.1 + response headers, depending on circumstances: + + * To suppress caching of the Set-Cookie header: Cache-control: no- + cache="set-cookie". + + and one of the following: + + * To suppress caching of a private document in shared caches: Cache- + control: private. + + * To allow caching of a document and require that it be validated + before returning it to the client: Cache-control: must-revalidate. + + * To allow caching of a document, but to require that proxy caches + (not user agent caches) validate it before returning it to the + client: Cache-control: proxy-revalidate. + + * To allow caching of a document and request that it be validated + before returning it to the client (by "pre-expiring" it): + Cache-control: max-age=0. Not all caches will revalidate the + document in every case. + + HTTP/1.1 servers must send Expires: old-date (where old-date is a + date long in the past) on responses containing Set-Cookie response + headers unless they know for certain (by out of band means) that + there are no downsteam HTTP/1.0 proxies. HTTP/1.1 servers may send + other Cache-Control directives that permit caching by HTTP/1.1 + proxies in addition to the Expires: old-date directive; the Cache- + Control directive will override the Expires: old-date for HTTP/1.1 + proxies. + + + +Kristol & Montulli Standards Track [Page 6] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +4.3 User Agent Role + +4.3.1 Interpreting Set-Cookie + + The user agent keeps separate track of state information that arrives + via Set-Cookie response headers from each origin server (as + distinguished by name or IP address and port). The user agent + applies these defaults for optional attributes that are missing: + + VersionDefaults to "old cookie" behavior as originally specified by + Netscape. See the HISTORICAL section. + + Domain Defaults to the request-host. (Note that there is no dot at + the beginning of request-host.) + + Max-AgeThe default behavior is to discard the cookie when the user + agent exits. + + Path Defaults to the path of the request URL that generated the + Set-Cookie response, up to, but not including, the + right-most /. + + Secure If absent, the user agent may send the cookie over an + insecure channel. + +4.3.2 Rejecting Cookies + + To prevent possible security or privacy violations, a user agent + rejects a cookie (shall not store its information) if any of the + following is true: + + * The value for the Path attribute is not a prefix of the request- + URI. + + * The value for the Domain attribute contains no embedded dots or + does not start with a dot. + + * The value for the request-host does not domain-match the Domain + attribute. + + * The request-host is a FQDN (not IP address) and has the form HD, + where D is the value of the Domain attribute, and H is a string + that contains one or more dots. + + Examples: + + * A Set-Cookie from request-host y.x.foo.com for Domain=.foo.com + would be rejected, because H is y.x and contains a dot. + + + +Kristol & Montulli Standards Track [Page 7] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + * A Set-Cookie from request-host x.foo.com for Domain=.foo.com would + be accepted. + + * A Set-Cookie with Domain=.com or Domain=.com., will always be + rejected, because there is no embedded dot. + + * A Set-Cookie with Domain=ajax.com will be rejected because the + value for Domain does not begin with a dot. + +4.3.3 Cookie Management + + If a user agent receives a Set-Cookie response header whose NAME is + the same as a pre-existing cookie, and whose Domain and Path + attribute values exactly (string) match those of a pre-existing + cookie, the new cookie supersedes the old. However, if the Set- + Cookie has a value for Max-Age of zero, the (old and new) cookie is + discarded. Otherwise cookies accumulate until they expire (resources + permitting), at which time they are discarded. + + Because user agents have finite space in which to store cookies, they + may also discard older cookies to make space for newer ones, using, + for example, a least-recently-used algorithm, along with constraints + on the maximum number of cookies that each origin server may set. + + If a Set-Cookie response header includes a Comment attribute, the + user agent should store that information in a human-readable form + with the cookie and should display the comment text as part of a + cookie inspection user interface. + + User agents should allow the user to control cookie destruction. An + infrequently-used cookie may function as a "preferences file" for + network applications, and a user may wish to keep it even if it is + the least-recently-used cookie. One possible implementation would be + an interface that allows the permanent storage of a cookie through a + checkbox (or, conversely, its immediate destruction). + + Privacy considerations dictate that the user have considerable + control over cookie management. The PRIVACY section contains more + information. + +4.3.4 Sending Cookies to the Origin Server + + When it sends a request to an origin server, the user agent sends a + Cookie request header to the origin server if it has cookies that are + applicable to the request, based on + + * the request-host; + + + + +Kristol & Montulli Standards Track [Page 8] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + * the request-URI; + + * the cookie's age. + + The syntax for the header is: + + cookie = "Cookie:" cookie-version + 1*((";" | ",") cookie-value) + cookie-value = NAME "=" VALUE [";" path] [";" domain] + cookie-version = "$Version" "=" value + NAME = attr + VALUE = value + path = "$Path" "=" value + domain = "$Domain" "=" value + + The value of the cookie-version attribute must be the value from the + Version attribute, if any, of the corresponding Set-Cookie response + header. Otherwise the value for cookie-version is 0. The value for + the path attribute must be the value from the Path attribute, if any, + of the corresponding Set-Cookie response header. Otherwise the + attribute should be omitted from the Cookie request header. The + value for the domain attribute must be the value from the Domain + attribute, if any, of the corresponding Set-Cookie response header. + Otherwise the attribute should be omitted from the Cookie request + header. + + Note that there is no Comment attribute in the Cookie request header + corresponding to the one in the Set-Cookie response header. The user + agent does not return the comment information to the origin server. + + The following rules apply to choosing applicable cookie-values from + among all the cookies the user agent has. + + Domain Selection + The origin server's fully-qualified host name must domain-match + the Domain attribute of the cookie. + + Path Selection + The Path attribute of the cookie must match a prefix of the + request-URI. + + Max-Age Selection + Cookies that have expired should have been discarded and thus + are not forwarded to an origin server. + + + + + + + +Kristol & Montulli Standards Track [Page 9] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + If multiple cookies satisfy the criteria above, they are ordered in + the Cookie header such that those with more specific Path attributes + precede those with less specific. Ordering with respect to other + attributes (e.g., Domain) is unspecified. + + Note: For backward compatibility, the separator in the Cookie header + is semi-colon (;) everywhere. A server should also accept comma (,) + as the separator between cookie-values for future compatibility. + +4.3.5 Sending Cookies in Unverifiable Transactions + + Users must have control over sessions in order to ensure privacy. + (See PRIVACY section below.) To simplify implementation and to + prevent an additional layer of complexity where adequate safeguards + exist, however, this document distinguishes between transactions that + are verifiable and those that are unverifiable. A transaction is + verifiable if the user has the option to review the request-URI prior + to its use in the transaction. A transaction is unverifiable if the + user does not have that option. Unverifiable transactions typically + arise when a user agent automatically requests inlined or embedded + entities or when it resolves redirection (3xx) responses from an + origin server. Typically the origin transaction, the transaction + that the user initiates, is verifiable, and that transaction may + directly or indirectly induce the user agent to make unverifiable + transactions. + + When it makes an unverifiable transaction, a user agent must enable a + session only if a cookie with a domain attribute D was sent or + received in its origin transaction, such that the host name in the + Request-URI of the unverifiable transaction domain-matches D. + + This restriction prevents a malicious service author from using + unverifiable transactions to induce a user agent to start or continue + a session with a server in a different domain. The starting or + continuation of such sessions could be contrary to the privacy + expectations of the user, and could also be a security problem. + + User agents may offer configurable options that allow the user agent, + or any autonomous programs that the user agent executes, to ignore + the above rule, so long as these override options default to "off". + + Many current user agents already provide a review option that would + render many links verifiable. For instance, some user agents display + the URL that would be referenced for a particular link when the mouse + pointer is placed over that link. The user can therefore determine + whether to visit that site before causing the browser to do so. + (Though not implemented on current user agents, a similar technique + could be used for a button used to submit a form -- the user agent + + + +Kristol & Montulli Standards Track [Page 10] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + could display the action to be taken if the user were to select that + button.) However, even this would not make all links verifiable; for + example, links to automatically loaded images would not normally be + subject to "mouse pointer" verification. + + Many user agents also provide the option for a user to view the HTML + source of a document, or to save the source to an external file where + it can be viewed by another application. While such an option does + provide a crude review mechanism, some users might not consider it + acceptable for this purpose. + +4.4 How an Origin Server Interprets the Cookie Header + + A user agent returns much of the information in the Set-Cookie header + to the origin server when the Path attribute matches that of a new + request. When it receives a Cookie header, the origin server should + treat cookies with NAMEs whose prefix is $ specially, as an attribute + for the adjacent cookie. The value for such a NAME is to be + interpreted as applying to the lexically (left-to-right) most recent + cookie whose name does not have the $ prefix. If there is no + previous cookie, the value applies to the cookie mechanism as a + whole. For example, consider the cookie + + Cookie: $Version="1"; Customer="WILE_E_COYOTE"; + $Path="/acme" + + $Version applies to the cookie mechanism as a whole (and gives the + version number for the cookie mechanism). $Path is an attribute + whose value (/acme) defines the Path attribute that was used when the + Customer cookie was defined in a Set-Cookie response header. + +4.5 Caching Proxy Role + + One reason for separating state information from both a URL and + document content is to facilitate the scaling that caching permits. + To support cookies, a caching proxy must obey these rules already in + the HTTP specification: + + * Honor requests from the cache, if possible, based on cache validity + rules. + + * Pass along a Cookie request header in any request that the proxy + must make of another server. + + * Return the response to the client. Include any Set-Cookie response + header. + + + + + +Kristol & Montulli Standards Track [Page 11] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + * Cache the received response subject to the control of the usual + headers, such as Expires, Cache-control: no-cache, and Cache- + control: private, + + * Cache the Set-Cookie subject to the control of the usual header, + Cache-control: no-cache="set-cookie". (The Set-Cookie header + should usually not be cached.) + + Proxies must not introduce Set-Cookie (Cookie) headers of their own + in proxy responses (requests). + +5. EXAMPLES + +5.1 Example 1 + + Most detail of request and response headers has been omitted. Assume + the user agent has no stored cookies. + + 1. User Agent -> Server + + POST /acme/login HTTP/1.1 + [form data] + + User identifies self via a form. + + 2. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie: Customer="WILE_E_COYOTE"; Version="1"; Path="/acme" + + Cookie reflects user's identity. + + 3. User Agent -> Server + + POST /acme/pickitem HTTP/1.1 + Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme" + [form data] + + User selects an item for "shopping basket." + + 4. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie: Part_Number="Rocket_Launcher_0001"; Version="1"; + Path="/acme" + + Shopping basket contains an item. + + + + +Kristol & Montulli Standards Track [Page 12] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + 5. User Agent -> Server + + POST /acme/shipping HTTP/1.1 + Cookie: $Version="1"; + Customer="WILE_E_COYOTE"; $Path="/acme"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme" + [form data] + + User selects shipping method from form. + + 6. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie: Shipping="FedEx"; Version="1"; Path="/acme" + + New cookie reflects shipping method. + + 7. User Agent -> Server + + POST /acme/process HTTP/1.1 + Cookie: $Version="1"; + Customer="WILE_E_COYOTE"; $Path="/acme"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme"; + Shipping="FedEx"; $Path="/acme" + [form data] + + User chooses to process order. + + 8. Server -> User Agent + + HTTP/1.1 200 OK + + Transaction is complete. + + The user agent makes a series of requests on the origin server, after + each of which it receives a new cookie. All the cookies have the + same Path attribute and (default) domain. Because the request URLs + all have /acme as a prefix, and that matches the Path attribute, each + request contains all the cookies received so far. + +5.2 Example 2 + + This example illustrates the effect of the Path attribute. All + detail of request and response headers has been omitted. Assume the + user agent has no stored cookies. + + Imagine the user agent has received, in response to earlier requests, + the response headers + + + +Kristol & Montulli Standards Track [Page 13] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + Set-Cookie: Part_Number="Rocket_Launcher_0001"; Version="1"; + Path="/acme" + + and + + Set-Cookie: Part_Number="Riding_Rocket_0023"; Version="1"; + Path="/acme/ammo" + + A subsequent request by the user agent to the (same) server for URLs + of the form /acme/ammo/... would include the following request + header: + + Cookie: $Version="1"; + Part_Number="Riding_Rocket_0023"; $Path="/acme/ammo"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme" + + Note that the NAME=VALUE pair for the cookie with the more specific + Path attribute, /acme/ammo, comes before the one with the less + specific Path attribute, /acme. Further note that the same cookie + name appears more than once. + + A subsequent request by the user agent to the (same) server for a URL + of the form /acme/parts/ would include the following request header: + + Cookie: $Version="1"; Part_Number="Rocket_Launcher_0001"; $Path="/acme" + + Here, the second cookie's Path attribute /acme/ammo is not a prefix + of the request URL, /acme/parts/, so the cookie does not get + forwarded to the server. + +6. IMPLEMENTATION CONSIDERATIONS + + Here we speculate on likely or desirable details for an origin server + that implements state management. + +6.1 Set-Cookie Content + + An origin server's content should probably be divided into disjoint + application areas, some of which require the use of state + information. The application areas can be distinguished by their + request URLs. The Set-Cookie header can incorporate information + about the application areas by setting the Path attribute for each + one. + + The session information can obviously be clear or encoded text that + describes state. However, if it grows too large, it can become + unwieldy. Therefore, an implementor might choose for the session + information to be a key to a server-side resource. Of course, using + + + +Kristol & Montulli Standards Track [Page 14] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + a database creates some problems that this state management + specification was meant to avoid, namely: + + 1. keeping real state on the server side; + + 2. how and when to garbage-collect the database entry, in case the + user agent terminates the session by, for example, exiting. + +6.2 Stateless Pages + + Caching benefits the scalability of WWW. Therefore it is important + to reduce the number of documents that have state embedded in them + inherently. For example, if a shopping-basket-style application + always displays a user's current basket contents on each page, those + pages cannot be cached, because each user's basket's contents would + be different. On the other hand, if each page contains just a link + that allows the user to "Look at My Shopping Basket", the page can be + cached. + +6.3 Implementation Limits + + Practical user agent implementations have limits on the number and + size of cookies that they can store. In general, user agents' cookie + support should have no fixed limits. They should strive to store as + many frequently-used cookies as possible. Furthermore, general-use + user agents should provide each of the following minimum capabilities + individually, although not necessarily simultaneously: + + * at least 300 cookies + + * at least 4096 bytes per cookie (as measured by the size of the + characters that comprise the cookie non-terminal in the syntax + description of the Set-Cookie header) + + * at least 20 cookies per unique host or domain name + + User agents created for specific purposes or for limited-capacity + devices should provide at least 20 cookies of 4096 bytes, to ensure + that the user can interact with a session-based origin server. + + The information in a Set-Cookie response header must be retained in + its entirety. If for some reason there is inadequate space to store + the cookie, it must be discarded, not truncated. + + Applications should use as few and as small cookies as possible, and + they should cope gracefully with the loss of a cookie. + + + + + +Kristol & Montulli Standards Track [Page 15] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +6.3.1 Denial of Service Attacks + + User agents may choose to set an upper bound on the number of cookies + to be stored from a given host or domain name or on the size of the + cookie information. Otherwise a malicious server could attempt to + flood a user agent with many cookies, or large cookies, on successive + responses, which would force out cookies the user agent had received + from other servers. However, the minima specified above should still + be supported. + +7. PRIVACY + +7.1 User Agent Control + + An origin server could create a Set-Cookie header to track the path + of a user through the server. Users may object to this behavior as + an intrusive accumulation of information, even if their identity is + not evident. (Identity might become evident if a user subsequently + fills out a form that contains identifying information.) This state + management specification therefore requires that a user agent give + the user control over such a possible intrusion, although the + interface through which the user is given this control is left + unspecified. However, the control mechanisms provided shall at least + allow the user + + * to completely disable the sending and saving of cookies. + + * to determine whether a stateful session is in progress. + + * to control the saving of a cookie on the basis of the cookie's + Domain attribute. + + Such control could be provided by, for example, mechanisms + + * to notify the user when the user agent is about to send a cookie + to the origin server, offering the option not to begin a session. + + * to display a visual indication that a stateful session is in + progress. + + * to let the user decide which cookies, if any, should be saved + when the user concludes a window or user agent session. + + * to let the user examine the contents of a cookie at any time. + + A user agent usually begins execution with no remembered state + information. It should be possible to configure a user agent never + to send Cookie headers, in which case it can never sustain state with + + + +Kristol & Montulli Standards Track [Page 16] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + an origin server. (The user agent would then behave like one that is + unaware of how to handle Set-Cookie response headers.) + + When the user agent terminates execution, it should let the user + discard all state information. Alternatively, the user agent may ask + the user whether state information should be retained; the default + should be "no". If the user chooses to retain state information, it + would be restored the next time the user agent runs. + + NOTE: User agents should probably be cautious about using files to + store cookies long-term. If a user runs more than one instance of + the user agent, the cookies could be commingled or otherwise messed + up. + +7.2 Protocol Design + + The restrictions on the value of the Domain attribute, and the rules + concerning unverifiable transactions, are meant to reduce the ways + that cookies can "leak" to the "wrong" site. The intent is to + restrict cookies to one, or a closely related set of hosts. + Therefore a request-host is limited as to what values it can set for + Domain. We consider it acceptable for hosts host1.foo.com and + host2.foo.com to share cookies, but not a.com and b.com. + + Similarly, a server can only set a Path for cookies that are related + to the request-URI. + +8. SECURITY CONSIDERATIONS + +8.1 Clear Text + + The information in the Set-Cookie and Cookie headers is unprotected. + Two consequences are: + + 1. Any sensitive information that is conveyed in them is exposed + to intruders. + + 2. A malicious intermediary could alter the headers as they travel + in either direction, with unpredictable results. + + These facts imply that information of a personal and/or financial + nature should only be sent over a secure channel. For less sensitive + information, or when the content of the header is a database key, an + origin server should be vigilant to prevent a bad Cookie value from + causing failures. + + + + + + +Kristol & Montulli Standards Track [Page 17] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +8.2 Cookie Spoofing + + Proper application design can avoid spoofing attacks from related + domains. Consider: + + 1. User agent makes request to victim.cracker.edu, gets back + cookie session_id="1234" and sets the default domain + victim.cracker.edu. + + 2. User agent makes request to spoof.cracker.edu, gets back + cookie session-id="1111", with Domain=".cracker.edu". + + 3. User agent makes request to victim.cracker.edu again, and + passes + + Cookie: $Version="1"; + session_id="1234"; + session_id="1111"; $Domain=".cracker.edu" + + The server at victim.cracker.edu should detect that the second + cookie was not one it originated by noticing that the Domain + attribute is not for itself and ignore it. + +8.3 Unexpected Cookie Sharing + + A user agent should make every attempt to prevent the sharing of + session information between hosts that are in different domains. + Embedded or inlined objects may cause particularly severe privacy + problems if they can be used to share cookies between disparate + hosts. For example, a malicious server could embed cookie + information for host a.com in a URI for a CGI on host b.com. User + agent implementors are strongly encouraged to prevent this sort of + exchange whenever possible. + +9. OTHER, SIMILAR, PROPOSALS + + Three other proposals have been made to accomplish similar goals. + This specification is an amalgam of Kristol's State-Info proposal and + Netscape's Cookie proposal. + + Brian Behlendorf proposed a Session-ID header that would be user- + agent-initiated and could be used by an origin server to track + "clicktrails". It would not carry any origin-server-defined state, + however. Phillip Hallam-Baker has proposed another client-defined + session ID mechanism for similar purposes. + + + + + + +Kristol & Montulli Standards Track [Page 18] + +RFC 2109 HTTP State Management Mechanism February 1997 + + + While both session IDs and cookies can provide a way to sustain + stateful sessions, their intended purpose is different, and, + consequently, the privacy requirements for them are different. A + user initiates session IDs to allow servers to track progress through + them, or to distinguish multiple users on a shared machine. Cookies + are server-initiated, so the cookie mechanism described here gives + users control over something that would otherwise take place without + the users' awareness. Furthermore, cookies convey rich, server- + selected information, whereas session IDs comprise user-selected, + simple information. + +10. HISTORICAL + +10.1 Compatibility With Netscape's Implementation + + HTTP/1.0 clients and servers may use Set-Cookie and Cookie headers + that reflect Netscape's original cookie proposal. These notes cover + inter-operation between "old" and "new" cookies. + +10.1.1 Extended Cookie Header + + This proposal adds attribute-value pairs to the Cookie request header + in a compatible way. An "old" client that receives a "new" cookie + will ignore attributes it does not understand; it returns what it + does understand to the origin server. A "new" client always sends + cookies in the new form. + + An "old" server that receives a "new" cookie will see what it thinks + are many cookies with names that begin with a $, and it will ignore + them. (The "old" server expects these cookies to be separated by + semi-colon, not comma.) A "new" server can detect cookies that have + passed through an "old" client, because they lack a $Version + attribute. + +10.1.2 Expires and Max-Age + + Netscape's original proposal defined an Expires header that took a + date value in a fixed-length variant format in place of Max-Age: + + Wdy, DD-Mon-YY HH:MM:SS GMT + + Note that the Expires date format contains embedded spaces, and that + "old" cookies did not have quotes around values. Clients that + implement to this specification should be aware of "old" cookies and + Expires. + + + + + + +Kristol & Montulli Standards Track [Page 19] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +10.1.3 Punctuation + + In Netscape's original proposal, the values in attribute-value pairs + did not accept "-quoted strings. Origin servers should be cautious + about sending values that require quotes unless they know the + receiving user agent understands them (i.e., "new" cookies). A + ("new") user agent should only use quotes around values in Cookie + headers when the cookie's version(s) is (are) all compliant with this + specification or later. + + In Netscape's original proposal, no whitespace was permitted around + the = that separates attribute-value pairs. Therefore such + whitespace should be used with caution in new implementations. + +10.2 Caching and HTTP/1.0 + + Some caches, such as those conforming to HTTP/1.0, will inevitably + cache the Set-Cookie header, because there was no mechanism to + suppress caching of headers prior to HTTP/1.1. This caching can lead + to security problems. Documents transmitted by an origin server + along with Set-Cookie headers will usually either be uncachable, or + will be "pre-expired". As long as caches obey instructions not to + cache documents (following Expires: <a date in the past> or Pragma: + no-cache (HTTP/1.0), or Cache-control: no-cache (HTTP/1.1)) + uncachable documents present no problem. However, pre-expired + documents may be stored in caches. They require validation (a + conditional GET) on each new request, but some cache operators loosen + the rules for their caches, and sometimes serve expired documents + without first validating them. This combination of factors can lead + to cookies meant for one user later being sent to another user. The + Set-Cookie header is stored in the cache, and, although the document + is stale (expired), the cache returns the document in response to + later requests, including cached headers. + +11. ACKNOWLEDGEMENTS + + This document really represents the collective efforts of the + following people, in addition to the authors: Roy Fielding, Marc + Hedlund, Ted Hardie, Koen Holtman, Shel Kaphan, Rohit Khare. + + + + + + + + + + + + +Kristol & Montulli Standards Track [Page 20] + +RFC 2109 HTTP State Management Mechanism February 1997 + + +12. AUTHORS' ADDRESSES + + David M. Kristol + Bell Laboratories, Lucent Technologies + 600 Mountain Ave. Room 2A-227 + Murray Hill, NJ 07974 + + Phone: (908) 582-2250 + Fax: (908) 582-5809 + EMail: dmk@bell-labs.com + + + Lou Montulli + Netscape Communications Corp. + 501 E. Middlefield Rd. + Mountain View, CA 94043 + + Phone: (415) 528-2600 + EMail: montulli@netscape.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Kristol & Montulli Standards Track [Page 21] + diff --git a/kioslave/http/kcookiejar/rfc2965 b/kioslave/http/kcookiejar/rfc2965 new file mode 100644 index 000000000..8a4d02b17 --- /dev/null +++ b/kioslave/http/kcookiejar/rfc2965 @@ -0,0 +1,1459 @@ + + + + + + +Network Working Group D. Kristol +Request for Comments: 2965 Bell Laboratories, Lucent Technologies +Obsoletes: 2109 L. Montulli +Category: Standards Track Epinions.com, Inc. + October 2000 + + + HTTP State Management Mechanism + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2000). All Rights Reserved. + +IESG Note + + The IESG notes that this mechanism makes use of the .local top-level + domain (TLD) internally when handling host names that don't contain + any dots, and that this mechanism might not work in the expected way + should an actual .local TLD ever be registered. + +Abstract + + This document specifies a way to create a stateful session with + Hypertext Transfer Protocol (HTTP) requests and responses. It + describes three new headers, Cookie, Cookie2, and Set-Cookie2, which + carry state information between participating origin servers and user + agents. The method described here differs from Netscape's Cookie + proposal [Netscape], but it can interoperate with HTTP/1.0 user + agents that use Netscape's method. (See the HISTORICAL section.) + + This document reflects implementation experience with RFC 2109 and + obsoletes it. + +1. TERMINOLOGY + + The terms user agent, client, server, proxy, origin server, and + http_URL have the same meaning as in the HTTP/1.1 specification + [RFC2616]. The terms abs_path and absoluteURI have the same meaning + as in the URI Syntax specification [RFC2396]. + + + + +Kristol & Montulli Standards Track [Page 1] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Host name (HN) means either the host domain name (HDN) or the numeric + Internet Protocol (IP) address of a host. The fully qualified domain + name is preferred; use of numeric IP addresses is strongly + discouraged. + + The terms request-host and request-URI refer to the values the client + would send to the server as, respectively, the host (but not port) + and abs_path portions of the absoluteURI (http_URL) of the HTTP + request line. Note that request-host is a HN. + + The term effective host name is related to host name. If a host name + contains no dots, the effective host name is that name with the + string .local appended to it. Otherwise the effective host name is + the same as the host name. Note that all effective host names + contain at least one dot. + + The term request-port refers to the port portion of the absoluteURI + (http_URL) of the HTTP request line. If the absoluteURI has no + explicit port, the request-port is the HTTP default, 80. The + request-port of a cookie is the request-port of the request in which + a Set-Cookie2 response header was returned to the user agent. + + Host names can be specified either as an IP address or a HDN string. + Sometimes we compare one host name with another. (Such comparisons + SHALL be case-insensitive.) Host A's name domain-matches host B's if + + * their host name strings string-compare equal; or + + * A is a HDN string and has the form NB, where N is a non-empty + name string, B has the form .B', and B' is a HDN string. (So, + x.y.com domain-matches .Y.com but not Y.com.) + + Note that domain-match is not a commutative operation: a.b.c.com + domain-matches .c.com, but not the reverse. + + The reach R of a host name H is defined as follows: + + * If + + - H is the host domain name of a host; and, + + - H has the form A.B; and + + - A has no embedded (that is, interior) dots; and + + - B has at least one embedded dot, or B is the string "local". + then the reach of H is .B. + + + + +Kristol & Montulli Standards Track [Page 2] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + * Otherwise, the reach of H is H. + + For two strings that represent paths, P1 and P2, P1 path-matches P2 + if P2 is a prefix of P1 (including the case where P1 and P2 string- + compare equal). Thus, the string /tec/waldo path-matches /tec. + + Because it was used in Netscape's original implementation of state + management, we will use the term cookie to refer to the state + information that passes between an origin server and user agent, and + that gets stored by the user agent. + +1.1 Requirements + + The key words "MAY", "MUST", "MUST NOT", "OPTIONAL", "RECOMMENDED", + "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT" in this + document are to be interpreted as described in RFC 2119 [RFC2119]. + +2. STATE AND SESSIONS + + This document describes a way to create stateful sessions with HTTP + requests and responses. Currently, HTTP servers respond to each + client request without relating that request to previous or + subsequent requests; the state management mechanism allows clients + and servers that wish to exchange state information to place HTTP + requests and responses within a larger context, which we term a + "session". This context might be used to create, for example, a + "shopping cart", in which user selections can be aggregated before + purchase, or a magazine browsing system, in which a user's previous + reading affects which offerings are presented. + + Neither clients nor servers are required to support cookies. A + server MAY refuse to provide content to a client that does not return + the cookies it sends. + +3. DESCRIPTION + + We describe here a way for an origin server to send state information + to the user agent, and for the user agent to return the state + information to the origin server. The goal is to have a minimal + impact on HTTP and user agents. + +3.1 Syntax: General + + The two state management headers, Set-Cookie2 and Cookie, have common + syntactic properties involving attribute-value pairs. The following + grammar uses the notation, and tokens DIGIT (decimal digits), token + + + + + +Kristol & Montulli Standards Track [Page 3] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + (informally, a sequence of non-special, non-white space characters), + and http_URL from the HTTP/1.1 specification [RFC2616] to describe + their syntax. + + av-pairs = av-pair *(";" av-pair) + av-pair = attr ["=" value] ; optional value + attr = token + value = token | quoted-string + + Attributes (names) (attr) are case-insensitive. White space is + permitted between tokens. Note that while the above syntax + description shows value as optional, most attrs require them. + + NOTE: The syntax above allows whitespace between the attribute and + the = sign. + +3.2 Origin Server Role + + 3.2.1 General The origin server initiates a session, if it so + desires. To do so, it returns an extra response header to the + client, Set-Cookie2. (The details follow later.) + + A user agent returns a Cookie request header (see below) to the + origin server if it chooses to continue a session. The origin server + MAY ignore it or use it to determine the current state of the + session. It MAY send back to the client a Set-Cookie2 response + header with the same or different information, or it MAY send no + Set-Cookie2 header at all. The origin server effectively ends a + session by sending the client a Set-Cookie2 header with Max-Age=0. + + Servers MAY return Set-Cookie2 response headers with any response. + User agents SHOULD send Cookie request headers, subject to other + rules detailed below, with every request. + + An origin server MAY include multiple Set-Cookie2 headers in a + response. Note that an intervening gateway could fold multiple such + headers into a single header. + + + + + + + + + + + + + + +Kristol & Montulli Standards Track [Page 4] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + 3.2.2 Set-Cookie2 Syntax The syntax for the Set-Cookie2 response + header is + + set-cookie = "Set-Cookie2:" cookies + cookies = 1#cookie + cookie = NAME "=" VALUE *(";" set-cookie-av) + NAME = attr + VALUE = value + set-cookie-av = "Comment" "=" value + | "CommentURL" "=" <"> http_URL <"> + | "Discard" + | "Domain" "=" value + | "Max-Age" "=" value + | "Path" "=" value + | "Port" [ "=" <"> portlist <"> ] + | "Secure" + | "Version" "=" 1*DIGIT + portlist = 1#portnum + portnum = 1*DIGIT + + Informally, the Set-Cookie2 response header comprises the token Set- + Cookie2:, followed by a comma-separated list of one or more cookies. + Each cookie begins with a NAME=VALUE pair, followed by zero or more + semi-colon-separated attribute-value pairs. The syntax for + attribute-value pairs was shown earlier. The specific attributes and + the semantics of their values follows. The NAME=VALUE attribute- + value pair MUST come first in each cookie. The others, if present, + can occur in any order. If an attribute appears more than once in a + cookie, the client SHALL use only the value associated with the first + appearance of the attribute; a client MUST ignore values after the + first. + + The NAME of a cookie MAY be the same as one of the attributes in this + specification. However, because the cookie's NAME must come first in + a Set-Cookie2 response header, the NAME and its VALUE cannot be + confused with an attribute-value pair. + + NAME=VALUE + REQUIRED. The name of the state information ("cookie") is NAME, + and its value is VALUE. NAMEs that begin with $ are reserved and + MUST NOT be used by applications. + + The VALUE is opaque to the user agent and may be anything the + origin server chooses to send, possibly in a server-selected + printable ASCII encoding. "Opaque" implies that the content is of + interest and relevance only to the origin server. The content + may, in fact, be readable by anyone that examines the Set-Cookie2 + header. + + + +Kristol & Montulli Standards Track [Page 5] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Comment=value + OPTIONAL. Because cookies can be used to derive or store private + information about a user, the value of the Comment attribute + allows an origin server to document how it intends to use the + cookie. The user can inspect the information to decide whether to + initiate or continue a session with this cookie. Characters in + value MUST be in UTF-8 encoding. [RFC2279] + + CommentURL="http_URL" + OPTIONAL. Because cookies can be used to derive or store private + information about a user, the CommentURL attribute allows an + origin server to document how it intends to use the cookie. The + user can inspect the information identified by the URL to decide + whether to initiate or continue a session with this cookie. + + Discard + OPTIONAL. The Discard attribute instructs the user agent to + discard the cookie unconditionally when the user agent terminates. + + Domain=value + OPTIONAL. The value of the Domain attribute specifies the domain + for which the cookie is valid. If an explicitly specified value + does not start with a dot, the user agent supplies a leading dot. + + Max-Age=value + OPTIONAL. The value of the Max-Age attribute is delta-seconds, + the lifetime of the cookie in seconds, a decimal non-negative + integer. To handle cached cookies correctly, a client SHOULD + calculate the age of the cookie according to the age calculation + rules in the HTTP/1.1 specification [RFC2616]. When the age is + greater than delta-seconds seconds, the client SHOULD discard the + cookie. A value of zero means the cookie SHOULD be discarded + immediately. + + Path=value + OPTIONAL. The value of the Path attribute specifies the subset of + URLs on the origin server to which this cookie applies. + + Port[="portlist"] + OPTIONAL. The Port attribute restricts the port to which a cookie + may be returned in a Cookie request header. Note that the syntax + REQUIREs quotes around the OPTIONAL portlist even if there is only + one portnum in portlist. + + + + + + + + +Kristol & Montulli Standards Track [Page 6] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Secure + OPTIONAL. The Secure attribute (with no value) directs the user + agent to use only (unspecified) secure means to contact the origin + server whenever it sends back this cookie, to protect the + confidentially and authenticity of the information in the cookie. + + The user agent (possibly with user interaction) MAY determine what + level of security it considers appropriate for "secure" cookies. + The Secure attribute should be considered security advice from the + server to the user agent, indicating that it is in the session's + interest to protect the cookie contents. When it sends a "secure" + cookie back to a server, the user agent SHOULD use no less than + the same level of security as was used when it received the cookie + from the server. + + Version=value + REQUIRED. The value of the Version attribute, a decimal integer, + identifies the version of the state management specification to + which the cookie conforms. For this specification, Version=1 + applies. + + 3.2.3 Controlling Caching An origin server must be cognizant of the + effect of possible caching of both the returned resource and the + Set-Cookie2 header. Caching "public" documents is desirable. For + example, if the origin server wants to use a public document such as + a "front door" page as a sentinel to indicate the beginning of a + session for which a Set-Cookie2 response header must be generated, + the page SHOULD be stored in caches "pre-expired" so that the origin + server will see further requests. "Private documents", for example + those that contain information strictly private to a session, SHOULD + NOT be cached in shared caches. + + If the cookie is intended for use by a single user, the Set-Cookie2 + header SHOULD NOT be cached. A Set-Cookie2 header that is intended + to be shared by multiple users MAY be cached. + + The origin server SHOULD send the following additional HTTP/1.1 + response headers, depending on circumstances: + + * To suppress caching of the Set-Cookie2 header: + + Cache-control: no-cache="set-cookie2" + + and one of the following: + + * To suppress caching of a private document in shared caches: + + Cache-control: private + + + +Kristol & Montulli Standards Track [Page 7] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + * To allow caching of a document and require that it be validated + before returning it to the client: + + Cache-Control: must-revalidate, max-age=0 + + * To allow caching of a document, but to require that proxy + caches (not user agent caches) validate it before returning it + to the client: + + Cache-Control: proxy-revalidate, max-age=0 + + * To allow caching of a document and request that it be validated + before returning it to the client (by "pre-expiring" it): + + Cache-control: max-age=0 + + Not all caches will revalidate the document in every case. + + HTTP/1.1 servers MUST send Expires: old-date (where old-date is a + date long in the past) on responses containing Set-Cookie2 response + headers unless they know for certain (by out of band means) that + there are no HTTP/1.0 proxies in the response chain. HTTP/1.1 + servers MAY send other Cache-Control directives that permit caching + by HTTP/1.1 proxies in addition to the Expires: old-date directive; + the Cache-Control directive will override the Expires: old-date for + HTTP/1.1 proxies. + +3.3 User Agent Role + + 3.3.1 Interpreting Set-Cookie2 The user agent keeps separate track + of state information that arrives via Set-Cookie2 response headers + from each origin server (as distinguished by name or IP address and + port). The user agent MUST ignore attribute-value pairs whose + attribute it does not recognize. The user agent applies these + defaults for optional attributes that are missing: + + Discard The default behavior is dictated by the presence or absence + of a Max-Age attribute. + + Domain Defaults to the effective request-host. (Note that because + there is no dot at the beginning of effective request-host, + the default Domain can only domain-match itself.) + + Max-Age The default behavior is to discard the cookie when the user + agent exits. + + Path Defaults to the path of the request URL that generated the + Set-Cookie2 response, up to and including the right-most /. + + + +Kristol & Montulli Standards Track [Page 8] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Port The default behavior is that a cookie MAY be returned to any + request-port. + + Secure If absent, the user agent MAY send the cookie over an + insecure channel. + + 3.3.2 Rejecting Cookies To prevent possible security or privacy + violations, a user agent rejects a cookie according to rules below. + The goal of the rules is to try to limit the set of servers for which + a cookie is valid, based on the values of the Path, Domain, and Port + attributes and the request-URI, request-host and request-port. + + A user agent rejects (SHALL NOT store its information) if the Version + attribute is missing. Moreover, a user agent rejects (SHALL NOT + store its information) if any of the following is true of the + attributes explicitly present in the Set-Cookie2 response header: + + * The value for the Path attribute is not a prefix of the + request-URI. + + * The value for the Domain attribute contains no embedded dots, + and the value is not .local. + + * The effective host name that derives from the request-host does + not domain-match the Domain attribute. + + * The request-host is a HDN (not IP address) and has the form HD, + where D is the value of the Domain attribute, and H is a string + that contains one or more dots. + + * The Port attribute has a "port-list", and the request-port was + not in the list. + + Examples: + + * A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com + would be rejected, because H is y.x and contains a dot. + + * A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com + would be accepted. + + * A Set-Cookie2 with Domain=.com or Domain=.com., will always be + rejected, because there is no embedded dot. + + * A Set-Cookie2 with Domain=ajax.com will be accepted, and the + value for Domain will be taken to be .ajax.com, because a dot + gets prepended to the value. + + + + +Kristol & Montulli Standards Track [Page 9] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + * A Set-Cookie2 with Port="80,8000" will be accepted if the + request was made to port 80 or 8000 and will be rejected + otherwise. + + * A Set-Cookie2 from request-host example for Domain=.local will + be accepted, because the effective host name for the request- + host is example.local, and example.local domain-matches .local. + + 3.3.3 Cookie Management If a user agent receives a Set-Cookie2 + response header whose NAME is the same as that of a cookie it has + previously stored, the new cookie supersedes the old when: the old + and new Domain attribute values compare equal, using a case- + insensitive string-compare; and, the old and new Path attribute + values string-compare equal (case-sensitive). However, if the Set- + Cookie2 has a value for Max-Age of zero, the (old and new) cookie is + discarded. Otherwise a cookie persists (resources permitting) until + whichever happens first, then gets discarded: its Max-Age lifetime is + exceeded; or, if the Discard attribute is set, the user agent + terminates the session. + + Because user agents have finite space in which to store cookies, they + MAY also discard older cookies to make space for newer ones, using, + for example, a least-recently-used algorithm, along with constraints + on the maximum number of cookies that each origin server may set. + + If a Set-Cookie2 response header includes a Comment attribute, the + user agent SHOULD store that information in a human-readable form + with the cookie and SHOULD display the comment text as part of a + cookie inspection user interface. + + If a Set-Cookie2 response header includes a CommentURL attribute, the + user agent SHOULD store that information in a human-readable form + with the cookie, or, preferably, SHOULD allow the user to follow the + http_URL link as part of a cookie inspection user interface. + + The cookie inspection user interface may include a facility whereby a + user can decide, at the time the user agent receives the Set-Cookie2 + response header, whether or not to accept the cookie. A potentially + confusing situation could arise if the following sequence occurs: + + * the user agent receives a cookie that contains a CommentURL + attribute; + + * the user agent's cookie inspection interface is configured so + that it presents a dialog to the user before the user agent + accepts the cookie; + + + + + +Kristol & Montulli Standards Track [Page 10] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + * the dialog allows the user to follow the CommentURL link when + the user agent receives the cookie; and, + + * when the user follows the CommentURL link, the origin server + (or another server, via other links in the returned content) + returns another cookie. + + The user agent SHOULD NOT send any cookies in this context. The user + agent MAY discard any cookie it receives in this context that the + user has not, through some user agent mechanism, deemed acceptable. + + User agents SHOULD allow the user to control cookie destruction, but + they MUST NOT extend the cookie's lifetime beyond that controlled by + the Discard and Max-Age attributes. An infrequently-used cookie may + function as a "preferences file" for network applications, and a user + may wish to keep it even if it is the least-recently-used cookie. One + possible implementation would be an interface that allows the + permanent storage of a cookie through a checkbox (or, conversely, its + immediate destruction). + + Privacy considerations dictate that the user have considerable + control over cookie management. The PRIVACY section contains more + information. + + 3.3.4 Sending Cookies to the Origin Server When it sends a request + to an origin server, the user agent includes a Cookie request header + if it has stored cookies that are applicable to the request, based on + + * the request-host and request-port; + + * the request-URI; + + * the cookie's age. + + The syntax for the header is: + +cookie = "Cookie:" cookie-version 1*((";" | ",") cookie-value) +cookie-value = NAME "=" VALUE [";" path] [";" domain] [";" port] +cookie-version = "$Version" "=" value +NAME = attr +VALUE = value +path = "$Path" "=" value +domain = "$Domain" "=" value +port = "$Port" [ "=" <"> value <"> ] + + The value of the cookie-version attribute MUST be the value from the + Version attribute of the corresponding Set-Cookie2 response header. + Otherwise the value for cookie-version is 0. The value for the path + + + +Kristol & Montulli Standards Track [Page 11] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + attribute MUST be the value from the Path attribute, if one was + present, of the corresponding Set-Cookie2 response header. Otherwise + the attribute SHOULD be omitted from the Cookie request header. The + value for the domain attribute MUST be the value from the Domain + attribute, if one was present, of the corresponding Set-Cookie2 + response header. Otherwise the attribute SHOULD be omitted from the + Cookie request header. + + The port attribute of the Cookie request header MUST mirror the Port + attribute, if one was present, in the corresponding Set-Cookie2 + response header. That is, the port attribute MUST be present if the + Port attribute was present in the Set-Cookie2 header, and it MUST + have the same value, if any. Otherwise, if the Port attribute was + absent from the Set-Cookie2 header, the attribute likewise MUST be + omitted from the Cookie request header. + + Note that there is neither a Comment nor a CommentURL attribute in + the Cookie request header corresponding to the ones in the Set- + Cookie2 response header. The user agent does not return the comment + information to the origin server. + + The user agent applies the following rules to choose applicable + cookie-values to send in Cookie request headers from among all the + cookies it has received. + + Domain Selection + The origin server's effective host name MUST domain-match the + Domain attribute of the cookie. + + Port Selection + There are three possible behaviors, depending on the Port + attribute in the Set-Cookie2 response header: + + 1. By default (no Port attribute), the cookie MAY be sent to any + port. + + 2. If the attribute is present but has no value (e.g., Port), the + cookie MUST only be sent to the request-port it was received + from. + + 3. If the attribute has a port-list, the cookie MUST only be + returned if the new request-port is one of those listed in + port-list. + + Path Selection + The request-URI MUST path-match the Path attribute of the cookie. + + + + + +Kristol & Montulli Standards Track [Page 12] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Max-Age Selection + Cookies that have expired should have been discarded and thus are + not forwarded to an origin server. + + If multiple cookies satisfy the criteria above, they are ordered in + the Cookie header such that those with more specific Path attributes + precede those with less specific. Ordering with respect to other + attributes (e.g., Domain) is unspecified. + + Note: For backward compatibility, the separator in the Cookie header + is semi-colon (;) everywhere. A server SHOULD also accept comma (,) + as the separator between cookie-values for future compatibility. + + 3.3.5 Identifying What Version is Understood: Cookie2 The Cookie2 + request header facilitates interoperation between clients and servers + that understand different versions of the cookie specification. When + the client sends one or more cookies to an origin server, if at least + one of those cookies contains a $Version attribute whose value is + different from the version that the client understands, then the + client MUST also send a Cookie2 request header, the syntax for which + is + + cookie2 = "Cookie2:" cookie-version + + Here the value for cookie-version is the highest version of cookie + specification (currently 1) that the client understands. The client + needs to send at most one such request header per request. + + 3.3.6 Sending Cookies in Unverifiable Transactions Users MUST have + control over sessions in order to ensure privacy. (See PRIVACY + section below.) To simplify implementation and to prevent an + additional layer of complexity where adequate safeguards exist, + however, this document distinguishes between transactions that are + verifiable and those that are unverifiable. A transaction is + verifiable if the user, or a user-designated agent, has the option to + review the request-URI prior to its use in the transaction. A + transaction is unverifiable if the user does not have that option. + Unverifiable transactions typically arise when a user agent + automatically requests inlined or embedded entities or when it + resolves redirection (3xx) responses from an origin server. + Typically the origin transaction, the transaction that the user + initiates, is verifiable, and that transaction may directly or + indirectly induce the user agent to make unverifiable transactions. + + An unverifiable transaction is to a third-party host if its request- + host U does not domain-match the reach R of the request-host O in the + origin transaction. + + + + +Kristol & Montulli Standards Track [Page 13] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + When it makes an unverifiable transaction, a user agent MUST disable + all cookie processing (i.e., MUST NOT send cookies, and MUST NOT + accept any received cookies) if the transaction is to a third-party + host. + + This restriction prevents a malicious service author from using + unverifiable transactions to induce a user agent to start or continue + a session with a server in a different domain. The starting or + continuation of such sessions could be contrary to the privacy + expectations of the user, and could also be a security problem. + + User agents MAY offer configurable options that allow the user agent, + or any autonomous programs that the user agent executes, to ignore + the above rule, so long as these override options default to "off". + + (N.B. Mechanisms may be proposed that will automate overriding the + third-party restrictions under controlled conditions.) + + Many current user agents already provide a review option that would + render many links verifiable. For instance, some user agents display + the URL that would be referenced for a particular link when the mouse + pointer is placed over that link. The user can therefore determine + whether to visit that site before causing the browser to do so. + (Though not implemented on current user agents, a similar technique + could be used for a button used to submit a form -- the user agent + could display the action to be taken if the user were to select that + button.) However, even this would not make all links verifiable; for + example, links to automatically loaded images would not normally be + subject to "mouse pointer" verification. + + Many user agents also provide the option for a user to view the HTML + source of a document, or to save the source to an external file where + it can be viewed by another application. While such an option does + provide a crude review mechanism, some users might not consider it + acceptable for this purpose. + +3.4 How an Origin Server Interprets the Cookie Header + + A user agent returns much of the information in the Set-Cookie2 + header to the origin server when the request-URI path-matches the + Path attribute of the cookie. When it receives a Cookie header, the + origin server SHOULD treat cookies with NAMEs whose prefix is $ + specially, as an attribute for the cookie. + + + + + + + + +Kristol & Montulli Standards Track [Page 14] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +3.5 Caching Proxy Role + + One reason for separating state information from both a URL and + document content is to facilitate the scaling that caching permits. + To support cookies, a caching proxy MUST obey these rules already in + the HTTP specification: + + * Honor requests from the cache, if possible, based on cache + validity rules. + + * Pass along a Cookie request header in any request that the + proxy must make of another server. + + * Return the response to the client. Include any Set-Cookie2 + response header. + + * Cache the received response subject to the control of the usual + headers, such as Expires, + + Cache-control: no-cache + + and + + Cache-control: private + + * Cache the Set-Cookie2 subject to the control of the usual + header, + + Cache-control: no-cache="set-cookie2" + + (The Set-Cookie2 header should usually not be cached.) + + Proxies MUST NOT introduce Set-Cookie2 (Cookie) headers of their own + in proxy responses (requests). + +4. EXAMPLES + +4.1 Example 1 + + Most detail of request and response headers has been omitted. Assume + the user agent has no stored cookies. + + 1. User Agent -> Server + + POST /acme/login HTTP/1.1 + [form data] + + User identifies self via a form. + + + +Kristol & Montulli Standards Track [Page 15] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + 2. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie2: Customer="WILE_E_COYOTE"; Version="1"; Path="/acme" + + Cookie reflects user's identity. + + 3. User Agent -> Server + + POST /acme/pickitem HTTP/1.1 + Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme" + [form data] + + User selects an item for "shopping basket". + + 4. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie2: Part_Number="Rocket_Launcher_0001"; Version="1"; + Path="/acme" + + Shopping basket contains an item. + + 5. User Agent -> Server + + POST /acme/shipping HTTP/1.1 + Cookie: $Version="1"; + Customer="WILE_E_COYOTE"; $Path="/acme"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme" + [form data] + + User selects shipping method from form. + + 6. Server -> User Agent + + HTTP/1.1 200 OK + Set-Cookie2: Shipping="FedEx"; Version="1"; Path="/acme" + + New cookie reflects shipping method. + + 7. User Agent -> Server + + POST /acme/process HTTP/1.1 + Cookie: $Version="1"; + Customer="WILE_E_COYOTE"; $Path="/acme"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme"; + Shipping="FedEx"; $Path="/acme" + [form data] + + + +Kristol & Montulli Standards Track [Page 16] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + User chooses to process order. + + 8. Server -> User Agent + + HTTP/1.1 200 OK + + Transaction is complete. + + The user agent makes a series of requests on the origin server, after + each of which it receives a new cookie. All the cookies have the + same Path attribute and (default) domain. Because the request-URIs + all path-match /acme, the Path attribute of each cookie, each request + contains all the cookies received so far. + +4.2 Example 2 + + This example illustrates the effect of the Path attribute. All + detail of request and response headers has been omitted. Assume the + user agent has no stored cookies. + + Imagine the user agent has received, in response to earlier requests, + the response headers + + Set-Cookie2: Part_Number="Rocket_Launcher_0001"; Version="1"; + Path="/acme" + + and + + Set-Cookie2: Part_Number="Riding_Rocket_0023"; Version="1"; + Path="/acme/ammo" + + A subsequent request by the user agent to the (same) server for URLs + of the form /acme/ammo/... would include the following request + header: + + Cookie: $Version="1"; + Part_Number="Riding_Rocket_0023"; $Path="/acme/ammo"; + Part_Number="Rocket_Launcher_0001"; $Path="/acme" + + Note that the NAME=VALUE pair for the cookie with the more specific + Path attribute, /acme/ammo, comes before the one with the less + specific Path attribute, /acme. Further note that the same cookie + name appears more than once. + + A subsequent request by the user agent to the (same) server for a URL + of the form /acme/parts/ would include the following request header: + + + + + +Kristol & Montulli Standards Track [Page 17] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Cookie: $Version="1"; Part_Number="Rocket_Launcher_0001"; + $Path="/acme" + + Here, the second cookie's Path attribute /acme/ammo is not a prefix + of the request URL, /acme/parts/, so the cookie does not get + forwarded to the server. + +5. IMPLEMENTATION CONSIDERATIONS + + Here we provide guidance on likely or desirable details for an origin + server that implements state management. + +5.1 Set-Cookie2 Content + + An origin server's content should probably be divided into disjoint + application areas, some of which require the use of state + information. The application areas can be distinguished by their + request URLs. The Set-Cookie2 header can incorporate information + about the application areas by setting the Path attribute for each + one. + + The session information can obviously be clear or encoded text that + describes state. However, if it grows too large, it can become + unwieldy. Therefore, an implementor might choose for the session + information to be a key to a server-side resource. Of course, using + a database creates some problems that this state management + specification was meant to avoid, namely: + + 1. keeping real state on the server side; + + 2. how and when to garbage-collect the database entry, in case the + user agent terminates the session by, for example, exiting. + +5.2 Stateless Pages + + Caching benefits the scalability of WWW. Therefore it is important + to reduce the number of documents that have state embedded in them + inherently. For example, if a shopping-basket-style application + always displays a user's current basket contents on each page, those + pages cannot be cached, because each user's basket's contents would + be different. On the other hand, if each page contains just a link + that allows the user to "Look at My Shopping Basket", the page can be + cached. + + + + + + + + +Kristol & Montulli Standards Track [Page 18] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +5.3 Implementation Limits + + Practical user agent implementations have limits on the number and + size of cookies that they can store. In general, user agents' cookie + support should have no fixed limits. They should strive to store as + many frequently-used cookies as possible. Furthermore, general-use + user agents SHOULD provide each of the following minimum capabilities + individually, although not necessarily simultaneously: + + * at least 300 cookies + + * at least 4096 bytes per cookie (as measured by the characters + that comprise the cookie non-terminal in the syntax description + of the Set-Cookie2 header, and as received in the Set-Cookie2 + header) + + * at least 20 cookies per unique host or domain name + + User agents created for specific purposes or for limited-capacity + devices SHOULD provide at least 20 cookies of 4096 bytes, to ensure + that the user can interact with a session-based origin server. + + The information in a Set-Cookie2 response header MUST be retained in + its entirety. If for some reason there is inadequate space to store + the cookie, it MUST be discarded, not truncated. + + Applications should use as few and as small cookies as possible, and + they should cope gracefully with the loss of a cookie. + + 5.3.1 Denial of Service Attacks User agents MAY choose to set an + upper bound on the number of cookies to be stored from a given host + or domain name or on the size of the cookie information. Otherwise a + malicious server could attempt to flood a user agent with many + cookies, or large cookies, on successive responses, which would force + out cookies the user agent had received from other servers. However, + the minima specified above SHOULD still be supported. + +6. PRIVACY + + Informed consent should guide the design of systems that use cookies. + A user should be able to find out how a web site plans to use + information in a cookie and should be able to choose whether or not + those policies are acceptable. Both the user agent and the origin + server must assist informed consent. + + + + + + + +Kristol & Montulli Standards Track [Page 19] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +6.1 User Agent Control + + An origin server could create a Set-Cookie2 header to track the path + of a user through the server. Users may object to this behavior as + an intrusive accumulation of information, even if their identity is + not evident. (Identity might become evident, for example, if a user + subsequently fills out a form that contains identifying information.) + This state management specification therefore requires that a user + agent give the user control over such a possible intrusion, although + the interface through which the user is given this control is left + unspecified. However, the control mechanisms provided SHALL at least + allow the user + + * to completely disable the sending and saving of cookies. + + * to determine whether a stateful session is in progress. + + * to control the saving of a cookie on the basis of the cookie's + Domain attribute. + + Such control could be provided, for example, by mechanisms + + * to notify the user when the user agent is about to send a + cookie to the origin server, to offer the option not to begin a + session. + + * to display a visual indication that a stateful session is in + progress. + + * to let the user decide which cookies, if any, should be saved + when the user concludes a window or user agent session. + + * to let the user examine and delete the contents of a cookie at + any time. + + A user agent usually begins execution with no remembered state + information. It SHOULD be possible to configure a user agent never + to send Cookie headers, in which case it can never sustain state with + an origin server. (The user agent would then behave like one that is + unaware of how to handle Set-Cookie2 response headers.) + + When the user agent terminates execution, it SHOULD let the user + discard all state information. Alternatively, the user agent MAY ask + the user whether state information should be retained; the default + should be "no". If the user chooses to retain state information, it + would be restored the next time the user agent runs. + + + + + +Kristol & Montulli Standards Track [Page 20] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + NOTE: User agents should probably be cautious about using files to + store cookies long-term. If a user runs more than one instance of + the user agent, the cookies could be commingled or otherwise + corrupted. + +6.2 Origin Server Role + + An origin server SHOULD promote informed consent by adding CommentURL + or Comment information to the cookies it sends. CommentURL is + preferred because of the opportunity to provide richer information in + a multiplicity of languages. + +6.3 Clear Text + + The information in the Set-Cookie2 and Cookie headers is unprotected. + As a consequence: + + 1. Any sensitive information that is conveyed in them is exposed + to intruders. + + 2. A malicious intermediary could alter the headers as they travel + in either direction, with unpredictable results. + + These facts imply that information of a personal and/or financial + nature should only be sent over a secure channel. For less sensitive + information, or when the content of the header is a database key, an + origin server should be vigilant to prevent a bad Cookie value from + causing failures. + + A user agent in a shared user environment poses a further risk. + Using a cookie inspection interface, User B could examine the + contents of cookies that were saved when User A used the machine. + +7. SECURITY CONSIDERATIONS + +7.1 Protocol Design + + The restrictions on the value of the Domain attribute, and the rules + concerning unverifiable transactions, are meant to reduce the ways + that cookies can "leak" to the "wrong" site. The intent is to + restrict cookies to one host, or a closely related set of hosts. + Therefore a request-host is limited as to what values it can set for + Domain. We consider it acceptable for hosts host1.foo.com and + host2.foo.com to share cookies, but not a.com and b.com. + + Similarly, a server can set a Path only for cookies that are related + to the request-URI. + + + + +Kristol & Montulli Standards Track [Page 21] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +7.2 Cookie Spoofing + + Proper application design can avoid spoofing attacks from related + domains. Consider: + + 1. User agent makes request to victim.cracker.edu, gets back + cookie session_id="1234" and sets the default domain + victim.cracker.edu. + + 2. User agent makes request to spoof.cracker.edu, gets back cookie + session-id="1111", with Domain=".cracker.edu". + + 3. User agent makes request to victim.cracker.edu again, and + passes + + Cookie: $Version="1"; session_id="1234", + $Version="1"; session_id="1111"; $Domain=".cracker.edu" + + The server at victim.cracker.edu should detect that the second + cookie was not one it originated by noticing that the Domain + attribute is not for itself and ignore it. + +7.3 Unexpected Cookie Sharing + + A user agent SHOULD make every attempt to prevent the sharing of + session information between hosts that are in different domains. + Embedded or inlined objects may cause particularly severe privacy + problems if they can be used to share cookies between disparate + hosts. For example, a malicious server could embed cookie + information for host a.com in a URI for a CGI on host b.com. User + agent implementors are strongly encouraged to prevent this sort of + exchange whenever possible. + +7.4 Cookies For Account Information + + While it is common practice to use them this way, cookies are not + designed or intended to be used to hold authentication information, + such as account names and passwords. Unless such cookies are + exchanged over an encrypted path, the account information they + contain is highly vulnerable to perusal and theft. + +8. OTHER, SIMILAR, PROPOSALS + + Apart from RFC 2109, three other proposals have been made to + accomplish similar goals. This specification began as an amalgam of + Kristol's State-Info proposal [DMK95] and Netscape's Cookie proposal + [Netscape]. + + + + +Kristol & Montulli Standards Track [Page 22] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + Brian Behlendorf proposed a Session-ID header that would be user- + agent-initiated and could be used by an origin server to track + "clicktrails". It would not carry any origin-server-defined state, + however. Phillip Hallam-Baker has proposed another client-defined + session ID mechanism for similar purposes. + + While both session IDs and cookies can provide a way to sustain + stateful sessions, their intended purpose is different, and, + consequently, the privacy requirements for them are different. A + user initiates session IDs to allow servers to track progress through + them, or to distinguish multiple users on a shared machine. Cookies + are server-initiated, so the cookie mechanism described here gives + users control over something that would otherwise take place without + the users' awareness. Furthermore, cookies convey rich, server- + selected information, whereas session IDs comprise user-selected, + simple information. + +9. HISTORICAL + +9.1 Compatibility with Existing Implementations + + Existing cookie implementations, based on the Netscape specification, + use the Set-Cookie (not Set-Cookie2) header. User agents that + receive in the same response both a Set-Cookie and Set-Cookie2 + response header for the same cookie MUST discard the Set-Cookie + information and use only the Set-Cookie2 information. Furthermore, a + user agent MUST assume, if it received a Set-Cookie2 response header, + that the sending server complies with this document and will + understand Cookie request headers that also follow this + specification. + + New cookies MUST replace both equivalent old- and new-style cookies. + That is, if a user agent that follows both this specification and + Netscape's original specification receives a Set-Cookie2 response + header, and the NAME and the Domain and Path attributes match (per + the Cookie Management section) a Netscape-style cookie, the + Netscape-style cookie MUST be discarded, and the user agent MUST + retain only the cookie adhering to this specification. + + Older user agents that do not understand this specification, but that + do understand Netscape's original specification, will not recognize + the Set-Cookie2 response header and will receive and send cookies + according to the older specification. + + + + + + + + +Kristol & Montulli Standards Track [Page 23] + +RFC 2965 HTTP State Management Mechanism October 2000 + + + A user agent that supports both this specification and Netscape-style + cookies SHOULD send a Cookie request header that follows the older + Netscape specification if it received the cookie in a Set-Cookie + response header and not in a Set-Cookie2 response header. However, + it SHOULD send the following request header as well: + + Cookie2: $Version="1" + + The Cookie2 header advises the server that the user agent understands + new-style cookies. If the server understands new-style cookies, as + well, it SHOULD continue the stateful session by sending a Set- + Cookie2 response header, rather than Set-Cookie. A server that does + not understand new-style cookies will simply ignore the Cookie2 + request header. + +9.2 Caching and HTTP/1.0 + + Some caches, such as those conforming to HTTP/1.0, will inevitably + cache the Set-Cookie2 and Set-Cookie headers, because there was no + mechanism to suppress caching of headers prior to HTTP/1.1. This + caching can lead to security problems. Documents transmitted by an + origin server along with Set-Cookie2 and Set-Cookie headers usually + either will be uncachable, or will be "pre-expired". As long as + caches obey instructions not to cache documents (following Expires: + <a date in the past> or Pragma: no-cache (HTTP/1.0), or Cache- + control: no-cache (HTTP/1.1)) uncachable documents present no + problem. However, pre-expired documents may be stored in caches. + They require validation (a conditional GET) on each new request, but + some cache operators loosen the rules for their caches, and sometimes + serve expired documents without first validating them. This + combination of factors can lead to cookies meant for one user later + being sent to another user. The Set-Cookie2 and Set-Cookie headers + are stored in the cache, and, although the document is stale + (expired), the cache returns the document in response to later + requests, including cached headers. + +10. ACKNOWLEDGEMENTS + + This document really represents the collective efforts of the HTTP + Working Group of the IETF and, particularly, the following people, in + addition to the authors: Roy Fielding, Yaron Goland, Marc Hedlund, + Ted Hardie, Koen Holtman, Shel Kaphan, Rohit Khare, Foteos Macrides, + David W. Morris. + + + + + + + + +Kristol & Montulli Standards Track [Page 24] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +11. AUTHORS' ADDRESSES + + David M. Kristol + Bell Laboratories, Lucent Technologies + 600 Mountain Ave. Room 2A-333 + Murray Hill, NJ 07974 + + Phone: (908) 582-2250 + Fax: (908) 582-1239 + EMail: dmk@bell-labs.com + + + Lou Montulli + Epinions.com, Inc. + 2037 Landings Dr. + Mountain View, CA 94301 + + EMail: lou@montulli.org + +12. REFERENCES + + [DMK95] Kristol, D.M., "Proposed HTTP State-Info Mechanism", + available at <http://portal.research.bell- + labs.com/~dmk/state-info.html>, September, 1995. + + [Netscape] "Persistent Client State -- HTTP Cookies", available at + <http://www.netscape.com/newsref/std/cookie_spec.html>, + undated. + + [RFC2109] Kristol, D. and L. Montulli, "HTTP State Management + Mechanism", RFC 2109, February 1997. + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC2279] Yergeau, F., "UTF-8, a transformation format of Unicode + and ISO-10646", RFC 2279, January 1998. + + [RFC2396] Berners-Lee, T., Fielding, R. and L. Masinter, "Uniform + Resource Identifiers (URI): Generic Syntax", RFC 2396, + August 1998. + + [RFC2616] Fielding, R., Gettys, J., Mogul, J., Frystyk, H. and T. + Berners-Lee, "Hypertext Transfer Protocol -- HTTP/1.1", + RFC 2616, June 1999. + + + + + + +Kristol & Montulli Standards Track [Page 25] + +RFC 2965 HTTP State Management Mechanism October 2000 + + +13. Full Copyright Statement + + Copyright (C) The Internet Society (2000). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + + + + + + + + + + + + + +Kristol & Montulli Standards Track [Page 26] + diff --git a/kioslave/http/kcookiejar/tests/Makefile.am b/kioslave/http/kcookiejar/tests/Makefile.am new file mode 100644 index 000000000..b79dd10fb --- /dev/null +++ b/kioslave/http/kcookiejar/tests/Makefile.am @@ -0,0 +1,18 @@ +# $Id$ +# Makefile.am of kdebase/kioslave/http + +INCLUDES= $(all_includes) + +####### Files + +check_PROGRAMS = kcookiejartest + +kcookiejartest_SOURCES = kcookiejartest.cpp +kcookiejartest_LDADD = $(LIB_KIO) +kcookiejartest_LDFLAGS = $(all_libraries) $(KDE_RPATH) + +check-local: kcookiejartest + ./kcookiejartest $(srcdir)/cookie.test + ./kcookiejartest $(srcdir)/cookie_rfc.test + ./kcookiejartest $(srcdir)/cookie_saving.test + ./kcookiejartest $(srcdir)/cookie_settings.test diff --git a/kioslave/http/kcookiejar/tests/cookie.test b/kioslave/http/kcookiejar/tests/cookie.test new file mode 100644 index 000000000..6619bf82d --- /dev/null +++ b/kioslave/http/kcookiejar/tests/cookie.test @@ -0,0 +1,162 @@ +## Check setting of cookies +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/"; expires=%NEXTYEAR% +CHECK http://w.y.z/ Cookie: some_value=value1 +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value2; Path="/" +CHECK http://a.b.c/ Cookie: some_value=value2 +## Check if clearing cookie jar works +CLEAR COOKIES +CHECK http://w.y.z/ +CHECK http://a.b.c/ +## Check cookie syntax +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value with spaces +CHECK http://w.y.z/ Cookie: some_value=value with spaces +COOKIE ASK http://a.b.c/ Set-Cookie: some_value="quoted value" +CHECK http://a.b.c/ Cookie: some_value="quoted value" +# Without a = sign, the cookie gets interpreted as the value for a cookie with no name +# This is what IE and Netscape does +COOKIE ASK http://a.b.c/ Set-Cookie: some_value +CHECK http://a.b.c/ Cookie: some_value; some_value="quoted value" +COOKIE ASK http://a.b.c/ Set-Cookie: some_other_value +CHECK http://a.b.c/ Cookie: some_other_value; some_value="quoted value" +CLEAR COOKIES +# This doesn't work with old-style netscape cookies, it should work with RFC2965 cookies +COOKIE ASK http://a.b.c/ Set-Cookie: some_value="quoted value; and such" +# IE & Netscape does this: +CHECK http://a.b.c/ Cookie: some_value="quoted value +# Mozilla does: +# CHECK http://a.b.c/ Cookie: some_value="quoted value; and such" +# COOKIE ASK http://a.b.c/ Set-Cookie: some_value="quoted value; +# CHECK http://a.b.c/ Cookie: some_value= +# Note that we parse RFC2965 cookies like Mozilla does +CLEAR COOKIES +## Check if deleting cookies works +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/"; expires=%NEXTYEAR% +CHECK http://w.y.z/ Cookie: some_value=value1 +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/"; expires=%LASTYEAR% +CHECK http://w.y.z/ +## Check if updating cookies works +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value2; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value3; Path="/"; expires=%NEXTYEAR% +CHECK http://w.y.z/ Cookie: some_value=value3 +## Check if multiple cookies work +COOKIE ASK http://w.y.z/ Set-Cookie: some_value2=foobar; Path="/"; expires=%NEXTYEAR% +CHECK http://w.y.z/ Cookie: some_value2=foobar; some_value=value3 +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=; Path="/"; expires=%LASTYEAR% +CHECK http://w.y.z/ Cookie: some_value2=foobar +CLEAR COOKIES +## Check if path restrictions work +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/Foo"; expires=%NEXTYEAR% +CHECK http://w.y.z/ +CHECK http://w.y.z/Foo Cookie: some_value=value1 +CHECK http://w.y.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y.z/Foo/bar Cookie: some_value=value1 +CLEAR COOKIES +## Check if default path works +# RFC2965 says that we should default to the URL path, but netscape cookies default to / +COOKIE ASK http://w.y.z/Foo/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +CHECK http://w.y.z/ +CHECK http://w.y.z/Foo Cookie: some_value=value1 +CHECK http://w.y.z/FooBar +CHECK http://w.y.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y.z/Foo/bar Cookie: some_value=value1 +CLEAR COOKIES +## Check if cookies are correctly ordered based on path +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/Foo"; expires=%NEXTYEAR% +COOKIE ASK http://w.y.z/ Set-Cookie: some_value2=value2; Path="/Foo/Bar"; expires=%NEXTYEAR% +CHECK http://w.y.z/Foo/Bar Cookie: some_value2=value2; some_value=value1 +COOKIE ASK http://w.y.z/ Set-Cookie: some_value3=value3; Path="/"; expires=%NEXTYEAR% +CHECK http://w.y.z/Foo/Bar Cookie: some_value2=value2; some_value=value1; some_value3=value3 +CLEAR COOKIES +## Check cookies with same name but different paths +COOKIE ASK http://w.y.z/Foo/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +COOKIE ASK http://w.y.z/Bar/ Set-Cookie: some_value=value2; expires=%NEXTYEAR% +CHECK http://w.y.z/Foo/Bar Cookie: some_value=value1 +CHECK http://w.y.z/Bar/Foo Cookie: some_value=value2 +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value3; expires=%NEXTYEAR% +CHECK http://w.y.z/Foo/Bar Cookie: some_value=value1; some_value=value3 +## Check secure cookie handling +COOKIE ASK https://secure.y.z/ Set-Cookie: some_value2=value2; Path="/"; expires=%NEXTYEAR%; secure +CHECK https://secure.y.z/Foo/bar Cookie: some_value2=value2 +CHECK http://secure.y.z/Foo/bar +CLEAR COOKIES +COOKIE ASK http://secure.y.z/ Set-Cookie: some_value3=value3; Path="/"; expires=%NEXTYEAR%; secure +CHECK https://secure.y.z/Foo/bar Cookie: some_value3=value3 +CHECK http://secure.y.z/Foo/bar +CLEAR COOKIES +## Check domain restrictions #1 +COOKIE ASK http://www.acme.com/ Set-Cookie: some_value=value1; Domain=".acme.com"; expires=%NEXTYEAR% +CHECK http://www.acme.com/ Cookie: some_value=value1 +CHECK http://www.abc.com/ +CHECK http://frop.acme.com/ Cookie: some_value=value1 +CLEAR COOKIES +## Check domain restrictions #2 +COOKIE ASK http://novell.com/ Set-Cookie: some_value=value1; Domain=".novell.com"; expires=%NEXTYEAR% +CHECK http://novell.com/ Cookie: some_value=value1 +CHECK http://www.novell.com/ Cookie: some_value=value1 +CLEAR COOKIES +COOKIE ASK http://novell.com/ Set-Cookie: some_value=value1; Domain="novell.com"; expires=%NEXTYEAR% +CHECK http://novell.com/ Cookie: some_value=value1 +CHECK http://www.novell.com/ Cookie: some_value=value1 +CLEAR COOKIES +## Check domain restrictions #3 +COOKIE ASK http://novell.com/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +CHECK http://novell.com/ Cookie: some_value=value1 +# FIXME: Allegedly IE sends cookies to sub-domains as well! +# See e.g. https://bugzilla.mozilla.org/show_bug.cgi?id=223027 +CHECK http://www.novell.com/ +CLEAR COOKIES +## Check domain restrictions #4 +COOKIE ASK http://novell.com/ Set-Cookie: some_value=value1; Domain=".com"; expires=%NEXTYEAR% +CHECK http://novell.com/ Cookie: some_value=value1 +# If the specified domain is too broad, we default to host only +CHECK http://www.novell.com/ +CHECK http://com/ +CHECK http://sun.com/ +## Check domain restrictions #5 +CLEAR COOKIES +COOKIE ASK http://novell.co.uk/ Set-Cookie: some_value=value1; Domain=".co.uk"; expires=%NEXTYEAR% +CHECK http://novell.co.uk/ Cookie: some_value=value1 +# If the specified domain is too broad, we default to host only +CHECK http://www.novell.co.uk/ +CHECK http://co.uk/ +CHECK http://sun.co.uk/ +COOKIE ASK http://x.y.z.foobar.com/ Set-Cookie: set_by=x.y.z.foobar.com; Domain=".foobar.com"; expires=%NEXTYEAR% +CHECK http://x.y.z.foobar.com/ Cookie: set_by=x.y.z.foobar.com +CHECK http://y.z.foobar.com/ Cookie: set_by=x.y.z.foobar.com +CHECK http://z.foobar.com/ Cookie: set_by=x.y.z.foobar.com +CHECK http://www.foobar.com/ Cookie: set_by=x.y.z.foobar.com +CHECK http://foobar.com/ Cookie: set_by=x.y.z.foobar.com +CLEAR COOKIES +## Check domain restrictions #6 +COOKIE ASK http://x.y.z.frop.com/ Set-Cookie: set_by=x.y.z.frop.com; Domain=".foobar.com"; expires=%NEXTYEAR% +COOKIE ASK http://x.y.z.frop.com/ Set-Cookie: set_by2=x.y.z.frop.com; Domain=".com"; expires=%NEXTYEAR% +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ +CLEAR COOKIES +## Check domain restrictions #7 +COOKIE ASK http://frop.com/ Set-Cookie: set_by=x.y.z.frop.com; Domain=".foobar.com"; expires=%NEXTYEAR% +COOKIE ASK http://frop.com/ Set-Cookie: set_by2=x.y.z.frop.com; Domain=".com"; expires=%NEXTYEAR% +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ +CLEAR COOKIES +## Check domain restrictions #8 +CONFIG AcceptSessionCookies true +COOKIE ACCEPT http://www.foobar.com Set-Cookie: from=foobar.com; domain=bar.com; Path="/" +CHECK http://bar.com +CLEAR COOKIES +## Check cookies with IP address hostnames +COOKIE ASK http://192.168.0.1 Set-Cookie: name1=value1; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://192.168.0.1 Set-Cookie: name11=value11; domain="test.local"; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://192.168.0.1:8080 Set-Cookie: name2=value2; Path="/"; expires=%NEXTYEAR% +COOKIE ASK https://192.168.0.1 Set-Cookie: name3=value3; Path="/"; expires=%NEXTYEAR%; secure +CHECK http://192.168.0.1 Cookie: name11=value11; name1=value1 +CHECK http://192.168.0.1:8080 Cookie: name2=value2 +CHECK https://192.168.0.1 Cookie: name3=value3; name11=value11; name1=value1 +CHECK http://192.168.0.10 +CHECK http://192.168.0 diff --git a/kioslave/http/kcookiejar/tests/cookie_rfc.test b/kioslave/http/kcookiejar/tests/cookie_rfc.test new file mode 100644 index 000000000..e1d8a40de --- /dev/null +++ b/kioslave/http/kcookiejar/tests/cookie_rfc.test @@ -0,0 +1,148 @@ +## Check setting of cookies +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value="value1"; Version=1; Path="/"; Max-Age=3600 +# Although the examples in RFC2965 uses $Version="1" the syntax description suggests that +# such quotes are not allowed, KDE BR59990 reports that the Sun Java server fails to handle +# cookies that use $Version="1" +CHECK http://w.y.z/ Cookie: $Version=1; some_value="value1"; $Path="/" +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value="value2"; Version=1; Path="/" +CHECK http://a.b.c/ Cookie: $Version=1; some_value="value2"; $Path="/" +## Check if clearing cookie jar works +CLEAR COOKIES +CHECK http://w.y.z/ +CHECK http://a.b.c/ +## Check cookie syntax +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value="value with spaces"; Version=1 +CHECK http://w.y.z/ Cookie: $Version=1; some_value="value with spaces" +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value ="extra space 1"; Version=1 +CHECK http://w.y.z/ Cookie: $Version=1; some_value="extra space 1" +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value= "extra space 2"; Version=1 +CHECK http://w.y.z/ Cookie: $Version=1; some_value="extra space 2" +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=unquoted; Version=1 +CHECK http://a.b.c/ Cookie: $Version=1; some_value=unquoted +# Note that we parse this different for Netscape-style cookies! +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value="quoted value; and such"; Version=1; +CHECK http://a.b.c/ Cookie: $Version=1; some_value="quoted value; and such" +CLEAR COOKIES +## Check if deleting cookies works #1 +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value="value1"; Version=1; Path="/"; Max-Age=3600 +CHECK http://w.y.z/ Cookie: $Version=1; some_value="value1"; $Path="/" +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value1; Version=1; Path="/"; Max-Age=0 +CHECK http://w.y.z/ +## Check if updating cookies works +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value2; Version=1; Path="/"; Max-Age=3600 +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value3; Version=1; Path="/"; Max-Age=3600 +CHECK http://w.y.z/ Cookie: $Version=1; some_value=value3; $Path="/" +## Check if multiple cookies work +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value2=foobar; Version=1; Path="/"; Max-Age=3600 +CHECK http://w.y.z/ Cookie: $Version=1; some_value2=foobar; $Path="/"; some_value=value3; $Path="/" +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=; Version=1; Path="/"; Max-Age=0 +CHECK http://w.y.z/ Cookie: $Version=1; some_value2=foobar; $Path="/" +CLEAR COOKIES +## Check if we prepend domain with a dot +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value2; Version=1; Path="/"; Domain=.y.z; Max-Age=3600 +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value3; Version=1; Path="/"; Domain=y.z.; Max-Age=3600 +CHECK http://w.y.z/ Cookie: $Version=1; some_value=value3; $Path="/"; $Domain=".y.z" +CLEAR COOKIES +## Check if multiple cookies on a single line work +## FIXME +#COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value3; Version=1; Path="/"; Max-Age=3600, some_value2=foobar; Version=1; Path="/"; Max-Age=3600 +# CHECK http://w.y.z/ Cookie: $Version=1; some_value2=foobar; $Path="/"; some_value=value3; $Path="/" +# COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=; Version=1; Path="/"; Max-Age=0 +# CHECK http://w.y.z/ Cookie: $Version=1; some_value2=foobar; $Path="/" +CLEAR COOKIES +## Check if path restrictions work +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value1; Version=1; Path="/Foo"; Max-Age=3600 +CHECK http://w.y.z/ +CHECK http://w.y.z/Foo Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y.z/Foo/ Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y.z/Foo/bar Cookie: $Version=1; some_value=value1; $Path="/Foo" +CLEAR COOKIES +## Check if default path works +# RFC2965 says that we should default to the URL path +COOKIE ASK http://w.y.z/Foo/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +CHECK http://w.y.z/ +CHECK http://w.y.z/Foo Cookie: $Version=1; some_value=value1 +CHECK http://w.y.z/FooBar +CHECK http://w.y.z/Foo/ Cookie: $Version=1; some_value=value1 +CHECK http://w.y.z/Foo/bar Cookie: $Version=1; some_value=value1 +CLEAR COOKIES +## Check if cookies are correctly ordered based on path +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value1; Version=1; Path="/Foo"; Max-Age=3600 +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value2=value2; Version=1; Path="/Foo/Bar"; Max-Age=3600 +CHECK http://w.y.z/Foo/Bar Cookie: $Version=1; some_value2=value2; $Path="/Foo/Bar"; some_value=value1; $Path="/Foo" +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value3=value3; Version=1; Path="/"; Max-Age=3600 +CHECK http://w.y.z/Foo/Bar Cookie: $Version=1; some_value2=value2; $Path="/Foo/Bar"; some_value=value1; $Path="/Foo"; some_value3=value3; $Path="/" +CLEAR COOKIES +## Check cookies with same name but different paths +COOKIE ASK http://w.y.z/Foo/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +COOKIE ASK http://w.y.z/Bar/ Set-Cookie2: some_value=value2; Version=1; Max-Age=3600 +CHECK http://w.y.z/Foo/Bar Cookie: $Version=1; some_value=value1 +CHECK http://w.y.z/Bar/Foo Cookie: $Version=1; some_value=value2 +COOKIE ASK http://w.y.z/ Set-Cookie2: some_value=value3; Version=1; Max-Age=3600 +CHECK http://w.y.z/Foo/Bar Cookie: $Version=1; some_value=value1; some_value=value3 +## Check secure cookie handling +COOKIE ASK https://secure.y.z/ Set-Cookie2: some_value2=value2; Version=1; Path="/"; Max-Age=3600; Secure +CHECK https://secure.y.z/Foo/bar Cookie: $Version=1; some_value2=value2; $Path="/" +CHECK http://secure.y.z/Foo/bar +CLEAR COOKIES +COOKIE ASK http://secure.y.z/ Set-Cookie2: some_value3=value3; Version=1; Path="/"; Max-Age=3600; Secure +CHECK https://secure.y.z/Foo/bar Cookie: $Version=1; some_value3=value3; $Path="/" +CHECK http://secure.y.z/Foo/bar +CLEAR COOKIES +## Check domain restrictions #1 +COOKIE ASK http://www.acme.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".acme.com"; Max-Age=3600 +CHECK http://www.acme.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme.com" +CHECK http://www.abc.com/ +CHECK http://frop.acme.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme.com" +CLEAR COOKIES +## Check domain restrictions #2 +COOKIE ASK http://novell.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".novell.com"; Max-Age=3600 +CHECK http://novell.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell.com" +CHECK http://www.novell.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell.com" +CLEAR COOKIES +## Check domain restrictions #3 +COOKIE ASK http://novell.com/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +CHECK http://novell.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell.com/ +CLEAR COOKIES +## Check domain restrictions #4 +COOKIE ASK http://novell.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".com"; Max-Age=3600 +# If the specified domain is too broad, we ignore the Domain +# FIXME: RFC2965 says we should ignore the cookie completely +CHECK http://novell.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell.com/ +CHECK http://com/ +CHECK http://sun.com/ +## Check domain restrictions #5 +CLEAR COOKIES +COOKIE ASK http://novell.co.uk/ Set-Cookie2: some_value=value1; Version=1; Domain=".co.uk"; Max-Age=3600 +# If the specified domain is too broad, we default to host only +# FIXME: RFC2965 says we should ignore the cookie completely +CHECK http://novell.co.uk/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell.co.uk/ +CHECK http://co.uk/ +CHECK http://sun.co.uk/ +COOKIE ASK http://x.y.z.foobar.com/ Set-Cookie2: set_by=x.y.z.foobar.com; Version=1; Domain=".foobar.com"; Max-Age=3600 +CHECK http://x.y.z.foobar.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar.com" +CHECK http://y.z.foobar.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar.com" +CHECK http://z.foobar.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar.com" +CHECK http://www.foobar.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar.com" +CHECK http://foobar.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar.com" +CLEAR COOKIES +## Check domain restrictions #6 +COOKIE ASK http://x.y.z.frop.com/ Set-Cookie2: set_by=x.y.z.frop.com; Version=1; Domain=".foobar.com"; Max-Age=3600 +COOKIE ASK http://x.y.z.frop.com/ Set-Cookie2: set_by2=x.y.z.frop.com; Version=1; Domain=".com"; Max-Age=3600 +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ +CLEAR COOKIES +## Check domain restrictions #7 +COOKIE ASK http://frop.com/ Set-Cookie2: set_by=x.y.z.frop.com; Version=1; Domain=".foobar.com"; Max-Age=3600 +COOKIE ASK http://frop.com/ Set-Cookie2: set_by2=x.y.z.frop.com; Version=1; Domain=".com"; Max-Age=3600 +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ diff --git a/kioslave/http/kcookiejar/tests/cookie_saving.test b/kioslave/http/kcookiejar/tests/cookie_saving.test new file mode 100644 index 000000000..cb9f34c42 --- /dev/null +++ b/kioslave/http/kcookiejar/tests/cookie_saving.test @@ -0,0 +1,430 @@ +## Check setting of cookies +COOKIE ASK http://w.y.z/ Set-Cookie: some_value=value1; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value2; Path="/" +## Check if clearing cookie jar works +CLEAR COOKIES +## Check cookie syntax +COOKIE ASK http://w.y1.z/ Set-Cookie: some_value=value with spaces; expires=%NEXTYEAR% +COOKIE ASK http://a.b1.c/ Set-Cookie: some_value="quoted value"; expires=%NEXTYEAR% +# Without a = sign, the cookie gets interpreted as the value for a cookie with no name +# This is what IE and Netscape does +COOKIE ASK http://a.b1.c/ Set-Cookie: some_value +COOKIE ASK http://a.b1.c/ Set-Cookie: some_other_value; expires=%NEXTYEAR% +# This doesn't work with old-style netscape cookies, it should work with RFC2965 cookies +COOKIE ASK http://a.b2.c/ Set-Cookie: some_value="quoted value; and such"; expires=%NEXTYEAR% +# IE & Netscape does this: +## Check if deleting cookies works +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value=value1; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value=value1; Path="/"; expires=%LASTYEAR% +## Check if updating cookies works +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value=value2; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value=value3; Path="/"; expires=%NEXTYEAR% +## Check if multiple cookies work +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value2=foobar; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://w.y3.z/ Set-Cookie: some_value=; Path="/"; expires=%LASTYEAR% +## Check if path restrictions work +COOKIE ASK http://w.y4.z/ Set-Cookie: some_value=value1; Path="/Foo"; expires=%NEXTYEAR% +## Check if default path works +COOKIE ASK http://w.y5.z/Foo/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +## Check if cookies are correctly ordered based on path +COOKIE ASK http://w.y6.z/ Set-Cookie: some_value=value1; Path="/Foo"; expires=%NEXTYEAR% +COOKIE ASK http://w.y6.z/ Set-Cookie: some_value2=value2; Path="/Foo/Bar"; expires=%NEXTYEAR% +COOKIE ASK http://w.y6.z/ Set-Cookie: some_value3=value3; Path="/"; expires=%NEXTYEAR% +## Check cookies with same name but different paths +COOKIE ASK http://w.y7.z/Foo/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +COOKIE ASK http://w.y7.z/Bar/ Set-Cookie: some_value=value2; expires=%NEXTYEAR% +COOKIE ASK http://w.y7.z/ Set-Cookie: some_value=value3; expires=%NEXTYEAR% +## Check secure cookie handling +COOKIE ASK https://secure.y7.z/ Set-Cookie: some_value2=value2; Path="/"; expires=%NEXTYEAR%; secure +COOKIE ASK http://secure.y8.z/ Set-Cookie: some_value3=value3; Path="/"; expires=%NEXTYEAR%; secure +## Check domain restrictions #1 +COOKIE ASK http://www.acme9.com/ Set-Cookie: some_value=value1; Domain=".acme9.com"; expires=%NEXTYEAR% +## Check domain restrictions #2 +COOKIE ASK http://novell10.com/ Set-Cookie: some_value=value1; Domain=".novell10.com"; expires=%NEXTYEAR% +COOKIE ASK http://novell11.com/ Set-Cookie: some_value=value1; Domain="novell11.com"; expires=%NEXTYEAR% +## Check domain restrictions #3 +COOKIE ASK http://novell12.com/ Set-Cookie: some_value=value1; expires=%NEXTYEAR% +## Check domain restrictions #4 +COOKIE ASK http://novell13.com/ Set-Cookie: some_value=value1; Domain=".com"; expires=%NEXTYEAR% +# If the specified domain is too broad, we default to host only +## Check domain restrictions #5 +COOKIE ASK http://novell14.co.uk/ Set-Cookie: some_value=value1; Domain=".co.uk"; expires=%NEXTYEAR% +COOKIE ASK http://x.y.z.foobar14.com/ Set-Cookie: set_by=x.y.z.foobar14.com; Domain=".foobar14.com"; expires=%NEXTYEAR% +## Check domain restrictions #6 +COOKIE ASK http://x.y.z.frop15.com/ Set-Cookie: set_by=x.y.z.frop15.com; Domain=".foobar15.com"; expires=%NEXTYEAR% +COOKIE ASK http://x.y.z.frop15.com/ Set-Cookie: set_by2=x.y.z.frop15.com; Domain=".com"; expires=%NEXTYEAR% +## Check domain restrictions #7 +COOKIE ASK http://frop16.com/ Set-Cookie: set_by=x.y.z.frop16.com; Domain=".foobar16.com"; expires=%NEXTYEAR% +COOKIE ASK http://frop16.com/ Set-Cookie: set_by2=x.y.z.frop16.com; Domain=".com"; expires=%NEXTYEAR% +## RFC Cookies +## Check setting of cookies +COOKIE ASK http://w.y20.z/ Set-Cookie2: some_value="value1"; Version=1; Path="/"; Max-Age=3600 +# Although the examples in RFC2965 uses $Version="1" the syntax description suggests that +# such quotes are not allowed, KDE BR59990 reports that the Sun Java server fails to handle +# cookies that use $Version="1" +COOKIE ASK http://a.b20.c/ Set-Cookie2: some_value="value2"; Version=1; Path="/"; Max-Age=3600 +## Check cookie syntax +COOKIE ASK http://w.y21.z/ Set-Cookie2: some_value="value with spaces"; Version=1; Max-Age=3600 +COOKIE ASK http://w.y21.z/ Set-Cookie2: some_value ="extra space 1"; Version=1; Max-Age=3600 +COOKIE ASK http://w.y21.z/ Set-Cookie2: some_value= "extra space 2"; Version=1; Max-Age=3600 +COOKIE ASK http://a.b21.c/ Set-Cookie2: some_value=unquoted; Version=1; Max-Age=3600 +# Note that we parse this different for Netscape-style cookies! +COOKIE ASK http://a.b21.c/ Set-Cookie2: some_value="quoted value; and such"; Version=1; Max-Age=3600 +## Check if deleting cookies works #1 +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value="value1"; Version=1; Path="/"; Max-Age=3600 +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value=value1; Version=1; Path="/"; Max-Age=0 +## Check if updating cookies works +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value=value2; Version=1; Path="/"; Max-Age=3600 +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value=value3; Version=1; Path="/"; Max-Age=3600 +## Check if multiple cookies work +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value2=foobar; Version=1; Path="/"; Max-Age=3600 +COOKIE ASK http://w.y22.z/ Set-Cookie2: some_value=; Version=1; Path="/"; Max-Age=0 +## Check if path restrictions work +COOKIE ASK http://w.y23.z/ Set-Cookie2: some_value=value1; Version=1; Path="/Foo"; Max-Age=3600 +## Check if default path works +# RFC2965 says that we should default to the URL path +COOKIE ASK http://w.y24.z/Foo/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +## Check if cookies are correctly ordered based on path +COOKIE ASK http://w.y25.z/ Set-Cookie2: some_value=value1; Version=1; Path="/Foo"; Max-Age=3600 +COOKIE ASK http://w.y25.z/ Set-Cookie2: some_value2=value2; Version=1; Path="/Foo/Bar"; Max-Age=3600 +COOKIE ASK http://w.y25.z/ Set-Cookie2: some_value3=value3; Version=1; Path="/"; Max-Age=3600 +## Check cookies with same name but different paths +COOKIE ASK http://w.y26.z/Foo/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +COOKIE ASK http://w.y26.z/Bar/ Set-Cookie2: some_value=value2; Version=1; Max-Age=3600 +COOKIE ASK http://w.y26.z/ Set-Cookie2: some_value=value3; Version=1; Max-Age=3600 +## Check secure cookie handling +COOKIE ASK https://secure.y26.z/ Set-Cookie2: some_value2=value2; Version=1; Path="/"; Max-Age=3600; Secure +COOKIE ASK http://secure.y27.z/ Set-Cookie2: some_value3=value3; Version=1; Path="/"; Max-Age=3600; Secure +## Check domain restrictions #1 +COOKIE ASK http://www.acme28.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".acme28.com"; Max-Age=3600 +## Check domain restrictions #2 +COOKIE ASK http://novell29.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".novell29.com"; Max-Age=3600 +## Check domain restrictions #3 +COOKIE ASK http://novell30.com/ Set-Cookie2: some_value=value1; Version=1; Max-Age=3600 +## Check domain restrictions #4 +COOKIE ASK http://novell31.com/ Set-Cookie2: some_value=value1; Version=1; Domain=".com"; Max-Age=3600 +# If the specified domain is too broad, we ignore the Domain +# FIXME: RFC2965 says we should ignore the cookie completely +## Check domain restrictions #5 +COOKIE ASK http://novell32.co.uk/ Set-Cookie2: some_value=value1; Version=1; Domain=".co.uk"; Max-Age=3600 +# If the specified domain is too broad, we default to host only +# FIXME: RFC2965 says we should ignore the cookie completely +COOKIE ASK http://x.y.z.foobar33.com/ Set-Cookie2: set_by=x.y.z.foobar.com; Version=1; Domain=".foobar33.com"; Max-Age=3600 +## Check domain restrictions #6 +COOKIE ASK http://x.y.z.frop34.com/ Set-Cookie2: set_by=x.y.z.frop.com; Version=1; Domain=".foobar.com"; Max-Age=3600 +COOKIE ASK http://x.y.z.frop34.com/ Set-Cookie2: set_by2=x.y.z.frop.com; Version=1; Domain=".com"; Max-Age=3600 +## Check domain restrictions #7 +COOKIE ASK http://frop35.com/ Set-Cookie2: set_by=x.y.z.frop.com; Version=1; Domain=".foobar.com"; Max-Age=3600 +COOKIE ASK http://frop35.com/ Set-Cookie2: set_by2=x.y.z.frop.com; Version=1; Domain=".com"; Max-Age=3600 + +## Check results +CHECK http://w.y.z/ +CHECK http://a.b.c/ +CHECK http://w.y1.z/ Cookie: some_value=value with spaces +CHECK http://a.b1.c/ Cookie: some_other_value; some_value="quoted value" +CHECK http://a.b2.c/ Cookie: some_value="quoted value +CHECK http://w.y3.z/ Cookie: some_value2=foobar +CHECK http://w.y4.z/ +CHECK http://w.y4.z/Foo Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y5.z/ +CHECK http://w.y5.z/Foo Cookie: some_value=value1 +CHECK http://w.y5.z/FooBar +CHECK http://w.y5.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y5.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y6.z/Foo/Bar Cookie: some_value2=value2; some_value=value1; some_value3=value3 +CHECK http://w.y7.z/Bar/Foo Cookie: some_value=value2; some_value=value3 +CHECK http://w.y7.z/Foo/Bar Cookie: some_value=value1; some_value=value3 +CHECK https://secure.y7.z/Foo/bar Cookie: some_value2=value2 +CHECK http://secure.y7.z/Foo/bar +CHECK https://secure.y8.z/Foo/bar Cookie: some_value3=value3 +CHECK http://secure.y8.z/Foo/bar +CHECK http://www.acme9.com/ Cookie: some_value=value1 +CHECK http://www.abc9.com/ +CHECK http://frop.acme9.com/ Cookie: some_value=value1 +CHECK http://novell10.com/ Cookie: some_value=value1 +CHECK http://www.novell10.com/ Cookie: some_value=value1 +CHECK http://novell11.com/ Cookie: some_value=value1 +CHECK http://www.novell11.com/ Cookie: some_value=value1 +CHECK http://novell12.com/ Cookie: some_value=value1 +CHECK http://www.novell12.com/ +CHECK http://novell13.com/ Cookie: some_value=value1 +CHECK http://www.novell13.com/ +CHECK http://com/ +CHECK http://sun13.com/ +CHECK http://novell14.co.uk/ Cookie: some_value=value1 +CHECK http://www.novell14.co.uk/ +CHECK http://co.uk/ +CHECK http://sun14.co.uk/ +CHECK http://x.y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://www.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://x.y.z.foobar15.com/ +CHECK http://y.z.foobar15.com/ +CHECK http://z.foobar15.com/ +CHECK http://www.foobar15.com/ +CHECK http://foobar15.com/ +CHECK http://x.y.z.foobar16.com/ +CHECK http://y.z.foobar16.com/ +CHECK http://z.foobar16.com/ +CHECK http://www.foobar16.com/ +CHECK http://foobar16.com/ +## Check results for RFC cookies +CHECK http://w.y20.z/ Cookie: $Version=1; some_value="value1"; $Path="/" +CHECK http://a.b20.c/ Cookie: $Version=1; some_value="value2"; $Path="/" +CHECK http://w.y21.z/ Cookie: $Version=1; some_value="extra space 2" +CHECK http://a.b21.c/ Cookie: $Version=1; some_value="quoted value; and such" +CHECK http://w.y22.z/ Cookie: $Version=1; some_value2=foobar; $Path="/" +CHECK http://w.y23.z/ +CHECK http://w.y23.z/Foo Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/ Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/bar Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y24.z/ +CHECK http://w.y24.z/Foo Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/FooBar +CHECK http://w.y24.z/Foo/ Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/Foo/bar Cookie: $Version=1; some_value=value1 +CHECK http://w.y25.z/Foo/Bar Cookie: $Version=1; some_value2=value2; $Path="/Foo/Bar"; some_value=value1; $Path="/Foo"; some_value3=value3; $Path="/" +CHECK http://w.y26.z/Bar/Foo Cookie: $Version=1; some_value=value2; some_value=value3 +CHECK http://w.y26.z/Foo/Bar Cookie: $Version=1; some_value=value1; some_value=value3 +CHECK https://secure.y26.z/Foo/bar Cookie: $Version=1; some_value2=value2; $Path="/" +CHECK http://secure.y26.z/Foo/bar +CHECK https://secure.y27.z/Foo/bar Cookie: $Version=1; some_value3=value3; $Path="/" +CHECK http://secure.y27.z/Foo/bar +CHECK http://www.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://www.abc28.com/ +CHECK http://frop.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://www.novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://novell30.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell30.com/ +CHECK http://novell31.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell31.com/ +CHECK http://com/ +CHECK http://sun31.com/ +CHECK http://novell32.co.uk/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell32.co.uk/ +CHECK http://co.uk/ +CHECK http://sun32.co.uk/ +CHECK http://x.y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://www.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ + + +SAVE +## Check result after saving +CHECK http://w.y.z/ +CHECK http://a.b.c/ +CHECK http://w.y1.z/ Cookie: some_value=value with spaces +CHECK http://a.b1.c/ Cookie: some_other_value; some_value="quoted value" +CHECK http://a.b2.c/ Cookie: some_value="quoted value +CHECK http://w.y3.z/ Cookie: some_value2=foobar +CHECK http://w.y4.z/ +CHECK http://w.y4.z/Foo Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y5.z/ +CHECK http://w.y5.z/Foo Cookie: some_value=value1 +CHECK http://w.y5.z/FooBar +CHECK http://w.y5.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y5.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y6.z/Foo/Bar Cookie: some_value2=value2; some_value=value1; some_value3=value3 +CHECK http://w.y7.z/Bar/Foo Cookie: some_value=value2; some_value=value3 +CHECK http://w.y7.z/Foo/Bar Cookie: some_value=value1; some_value=value3 +CHECK https://secure.y7.z/Foo/bar Cookie: some_value2=value2 +CHECK http://secure.y7.z/Foo/bar +CHECK https://secure.y8.z/Foo/bar Cookie: some_value3=value3 +CHECK http://secure.y8.z/Foo/bar +CHECK http://www.acme9.com/ Cookie: some_value=value1 +CHECK http://www.abc9.com/ +CHECK http://frop.acme9.com/ Cookie: some_value=value1 +CHECK http://novell10.com/ Cookie: some_value=value1 +CHECK http://www.novell10.com/ Cookie: some_value=value1 +CHECK http://novell11.com/ Cookie: some_value=value1 +CHECK http://www.novell11.com/ Cookie: some_value=value1 +CHECK http://novell12.com/ Cookie: some_value=value1 +CHECK http://www.novell12.com/ +CHECK http://novell13.com/ Cookie: some_value=value1 +CHECK http://www.novell13.com/ +CHECK http://com/ +CHECK http://sun13.com/ +CHECK http://novell14.co.uk/ Cookie: some_value=value1 +CHECK http://www.novell14.co.uk/ +CHECK http://co.uk/ +CHECK http://sun14.co.uk/ +CHECK http://x.y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://www.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://x.y.z.foobar15.com/ +CHECK http://y.z.foobar15.com/ +CHECK http://z.foobar15.com/ +CHECK http://www.foobar15.com/ +CHECK http://foobar15.com/ +CHECK http://x.y.z.foobar16.com/ +CHECK http://y.z.foobar16.com/ +CHECK http://z.foobar16.com/ +CHECK http://www.foobar16.com/ +CHECK http://foobar16.com/ +## Check result for RFC cookies after saving +CHECK http://w.y20.z/ Cookie: $Version=1; some_value="value1"; $Path="/" +CHECK http://a.b20.c/ Cookie: $Version=1; some_value="value2"; $Path="/" +CHECK http://w.y21.z/ Cookie: $Version=1; some_value="extra space 2" +CHECK http://a.b21.c/ Cookie: $Version=1; some_value="quoted value; and such" +CHECK http://w.y22.z/ Cookie: $Version=1; some_value2=foobar; $Path="/" +CHECK http://w.y23.z/ +CHECK http://w.y23.z/Foo Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/ Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/bar Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y24.z/ +CHECK http://w.y24.z/Foo Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/FooBar +CHECK http://w.y24.z/Foo/ Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/Foo/bar Cookie: $Version=1; some_value=value1 +CHECK http://w.y25.z/Foo/Bar Cookie: $Version=1; some_value2=value2; $Path="/Foo/Bar"; some_value=value1; $Path="/Foo"; some_value3=value3; $Path="/" +CHECK http://w.y26.z/Bar/Foo Cookie: $Version=1; some_value=value2; some_value=value3 +CHECK http://w.y26.z/Foo/Bar Cookie: $Version=1; some_value=value1; some_value=value3 +CHECK https://secure.y26.z/Foo/bar Cookie: $Version=1; some_value2=value2; $Path="/" +CHECK http://secure.y26.z/Foo/bar +CHECK https://secure.y27.z/Foo/bar Cookie: $Version=1; some_value3=value3; $Path="/" +CHECK http://secure.y27.z/Foo/bar +CHECK http://www.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://www.abc28.com/ +CHECK http://frop.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://www.novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://novell30.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell30.com/ +CHECK http://novell31.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell31.com/ +CHECK http://com/ +CHECK http://sun31.com/ +CHECK http://novell32.co.uk/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell32.co.uk/ +CHECK http://co.uk/ +CHECK http://sun32.co.uk/ +CHECK http://x.y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://www.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ + +SAVE +## Check result after saving a second time +CHECK http://w.y.z/ +CHECK http://a.b.c/ +CHECK http://w.y1.z/ Cookie: some_value=value with spaces +CHECK http://a.b1.c/ Cookie: some_other_value; some_value="quoted value" +CHECK http://a.b2.c/ Cookie: some_value="quoted value +CHECK http://w.y3.z/ Cookie: some_value2=foobar +CHECK http://w.y4.z/ +CHECK http://w.y4.z/Foo Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y4.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y5.z/ +CHECK http://w.y5.z/Foo Cookie: some_value=value1 +CHECK http://w.y5.z/FooBar +CHECK http://w.y5.z/Foo/ Cookie: some_value=value1 +CHECK http://w.y5.z/Foo/bar Cookie: some_value=value1 +CHECK http://w.y6.z/Foo/Bar Cookie: some_value2=value2; some_value=value1; some_value3=value3 +CHECK http://w.y7.z/Bar/Foo Cookie: some_value=value2; some_value=value3 +CHECK http://w.y7.z/Foo/Bar Cookie: some_value=value1; some_value=value3 +CHECK https://secure.y7.z/Foo/bar Cookie: some_value2=value2 +CHECK http://secure.y7.z/Foo/bar +CHECK https://secure.y8.z/Foo/bar Cookie: some_value3=value3 +CHECK http://secure.y8.z/Foo/bar +CHECK http://www.acme9.com/ Cookie: some_value=value1 +CHECK http://www.abc9.com/ +CHECK http://frop.acme9.com/ Cookie: some_value=value1 +CHECK http://novell10.com/ Cookie: some_value=value1 +CHECK http://www.novell10.com/ Cookie: some_value=value1 +CHECK http://novell11.com/ Cookie: some_value=value1 +CHECK http://www.novell11.com/ Cookie: some_value=value1 +CHECK http://novell12.com/ Cookie: some_value=value1 +CHECK http://www.novell12.com/ +CHECK http://novell13.com/ Cookie: some_value=value1 +CHECK http://www.novell13.com/ +CHECK http://com/ +CHECK http://sun13.com/ +CHECK http://novell14.co.uk/ Cookie: some_value=value1 +CHECK http://www.novell14.co.uk/ +CHECK http://co.uk/ +CHECK http://sun14.co.uk/ +CHECK http://x.y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://y.z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://z.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://www.foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://foobar14.com/ Cookie: set_by=x.y.z.foobar14.com +CHECK http://x.y.z.foobar15.com/ +CHECK http://y.z.foobar15.com/ +CHECK http://z.foobar15.com/ +CHECK http://www.foobar15.com/ +CHECK http://foobar15.com/ +CHECK http://x.y.z.foobar16.com/ +CHECK http://y.z.foobar16.com/ +CHECK http://z.foobar16.com/ +CHECK http://www.foobar16.com/ +CHECK http://foobar16.com/ +## Check result for rfc cookies after saving a second time +CHECK http://w.y20.z/ Cookie: $Version=1; some_value="value1"; $Path="/" +CHECK http://a.b20.c/ Cookie: $Version=1; some_value="value2"; $Path="/" +CHECK http://w.y21.z/ Cookie: $Version=1; some_value="extra space 2" +CHECK http://a.b21.c/ Cookie: $Version=1; some_value="quoted value; and such" +CHECK http://w.y22.z/ Cookie: $Version=1; some_value2=foobar; $Path="/" +CHECK http://w.y23.z/ +CHECK http://w.y23.z/Foo Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/ Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y23.z/Foo/bar Cookie: $Version=1; some_value=value1; $Path="/Foo" +CHECK http://w.y24.z/ +CHECK http://w.y24.z/Foo Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/FooBar +CHECK http://w.y24.z/Foo/ Cookie: $Version=1; some_value=value1 +CHECK http://w.y24.z/Foo/bar Cookie: $Version=1; some_value=value1 +CHECK http://w.y25.z/Foo/Bar Cookie: $Version=1; some_value2=value2; $Path="/Foo/Bar"; some_value=value1; $Path="/Foo"; some_value3=value3; $Path="/" +CHECK http://w.y26.z/Bar/Foo Cookie: $Version=1; some_value=value2; some_value=value3 +CHECK http://w.y26.z/Foo/Bar Cookie: $Version=1; some_value=value1; some_value=value3 +CHECK https://secure.y26.z/Foo/bar Cookie: $Version=1; some_value2=value2; $Path="/" +CHECK http://secure.y26.z/Foo/bar +CHECK https://secure.y27.z/Foo/bar Cookie: $Version=1; some_value3=value3; $Path="/" +CHECK http://secure.y27.z/Foo/bar +CHECK http://www.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://www.abc28.com/ +CHECK http://frop.acme28.com/ Cookie: $Version=1; some_value=value1; $Domain=".acme28.com" +CHECK http://novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://www.novell29.com/ Cookie: $Version=1; some_value=value1; $Domain=".novell29.com" +CHECK http://novell30.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell30.com/ +CHECK http://novell31.com/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell31.com/ +CHECK http://com/ +CHECK http://sun31.com/ +CHECK http://novell32.co.uk/ Cookie: $Version=1; some_value=value1 +CHECK http://www.novell32.co.uk/ +CHECK http://co.uk/ +CHECK http://sun32.co.uk/ +CHECK http://x.y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://y.z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://z.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://www.foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://foobar33.com/ Cookie: $Version=1; set_by=x.y.z.foobar.com; $Domain=".foobar33.com" +CHECK http://x.y.z.foobar.com/ +CHECK http://y.z.foobar.com/ +CHECK http://z.foobar.com/ +CHECK http://www.foobar.com/ +CHECK http://foobar.com/ diff --git a/kioslave/http/kcookiejar/tests/cookie_settings.test b/kioslave/http/kcookiejar/tests/cookie_settings.test new file mode 100644 index 000000000..7fc1a03a7 --- /dev/null +++ b/kioslave/http/kcookiejar/tests/cookie_settings.test @@ -0,0 +1,116 @@ +## Check CookieGlobalAdvice setting +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value1; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value2; Path="/" +CONFIG CookieGlobalAdvice Reject +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value3; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value4; Path="/" +CONFIG CookieGlobalAdvice Accept +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value5; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value6; Path="/" +CONFIG CookieGlobalAdvice Ask +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value7; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value8; Path="/" +CONFIG AcceptSessionCookies true +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +# FIXME: Shouldn't this be considered a session cookie? +# COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="0" +# COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%LASTYEAR% +# FIXME: The 'Discard' attribute makes the cookie a session cookie +# COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +## Treat all cookies as session cookies +CONFIG IgnoreExpirationDate true +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check host-based domain policies +CONFIG IgnoreExpirationDate false +CONFIG AcceptSessionCookies false +CONFIG CookieDomainAdvice a.b.c:Reject +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check resetting of domain policies +CONFIG CookieDomainAdvice +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check domain policies +CONFIG CookieDomainAdvice .b.c:Reject +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE REJECT http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check overriding of domain policies #1 +CONFIG CookieDomainAdvice .b.c:Reject,a.b.c:Accept +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE REJECT http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check overriding of domain policies #2 +CONFIG CookieDomainAdvice a.b.c:Reject,.b.c:Accept +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE ACCEPT http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check resetting of domain policies +CONFIG CookieDomainAdvice +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ASK http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ASK http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ASK http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ASK http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check overriding of domain policies #3 +CONFIG CookieDomainAdvice b.c:Reject,.b.c:Accept +COOKIE REJECT http://b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE REJECT http://b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE REJECT http://b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +## Check overriding of domain policies #4 +CONFIG CookieDomainAdvice .a.b.c.d:Reject,.b.c.d:Accept,.c.d:Ask +COOKIE REJECT http://www.a.b.c.d/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ACCEPT http://www.b.c.d/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE ASK http://www.c.d/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +## Check interaction with session policy +CONFIG AcceptSessionCookies true +CONFIG CookieDomainAdvice .b.c:Reject +COOKIE REJECT http://a.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://a.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://a.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://a.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" +COOKIE REJECT http://d.b.c/ Set-Cookie: some_value=value9; Path="/"; expires=%NEXTYEAR% +COOKIE REJECT http://d.b.c/ Set-Cookie2: some_value=value10; Version=1; Path="/"; max-age="600" +COOKIE ACCEPT http://d.b.c/ Set-Cookie: some_value=value11; Path="/" +COOKIE ACCEPT http://d.b.c/ Set-Cookie2: some_value=value12; Version=1; Path="/" diff --git a/kioslave/http/kcookiejar/tests/kcookiejartest.cpp b/kioslave/http/kcookiejar/tests/kcookiejartest.cpp new file mode 100644 index 000000000..f196f1820 --- /dev/null +++ b/kioslave/http/kcookiejar/tests/kcookiejartest.cpp @@ -0,0 +1,270 @@ +/* + This file is part of KDE + + Copyright (C) 2004 Waldo Bastian (bastian@kde.org) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + + This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <stdio.h> +#include <stdlib.h> + +#include <qdatetime.h> +#include <qstring.h> + +#include <kapplication.h> +#include <kaboutdata.h> +#include <kcmdlineargs.h> +#include <kstandarddirs.h> + +#include "../kcookiejar.cpp" + +static const char *description = "KCookiejar regression test"; + +static KCookieJar *jar; +static QCString *lastYear; +static QCString *nextYear; +static KConfig *config = 0; + + +static KCmdLineOptions options[] = +{ + { "+testfile", "Regression test to run", 0}, + KCmdLineLastOption +}; + +static void FAIL(const QString &msg) +{ + qWarning("%s", msg.local8Bit().data()); + exit(1); +} + +static void popArg(QCString &command, QCString & line) +{ + int i = line.find(' '); + if (i != -1) + { + command = line.left(i); + line = line.mid(i+1); + } + else + { + command = line; + line = 0; + } +} + + +static void popArg(QString &command, QCString & line) +{ + int i = line.find(' '); + if (i != -1) + { + command = QString::fromLatin1(line.left(i)); + line = line.mid(i+1); + } + else + { + command = QString::fromLatin1(line); + line = 0; + } +} + +static void clearConfig() +{ + delete config; + QString file = locateLocal("config", "kcookiejar-testconfig"); + QFile::remove(file); + config = new KConfig(file); + config->setGroup("Cookie Policy"); + config->writeEntry("RejectCrossDomainCookies", false); + config->writeEntry("AcceptSessionCookies", false); + config->writeEntry("IgnoreExpirationDate", false); + config->writeEntry("CookieGlobalAdvice", "Ask"); + jar->loadConfig(config, false); +} + +static void clearCookies() +{ + jar->eatAllCookies(); +} + +static void saveCookies() +{ + QString file = locateLocal("config", "kcookiejar-testcookies"); + QFile::remove(file); + jar->saveCookies(file); + delete jar; + jar = new KCookieJar(); + clearConfig(); + jar->loadCookies(file); +} + +static void processCookie(QCString &line) +{ + QString policy; + popArg(policy, line); + KCookieAdvice expectedAdvice = KCookieJar::strToAdvice(policy); + if (expectedAdvice == KCookieDunno) + FAIL(QString("Unknown accept policy '%1'").arg(policy)); + + QString urlStr; + popArg(urlStr, line); + KURL url(urlStr); + if (!url.isValid()) + FAIL(QString("Invalid URL '%1'").arg(urlStr)); + if (url.isEmpty()) + FAIL(QString("Missing URL")); + + line.replace("%LASTYEAR%", *lastYear); + line.replace("%NEXTYEAR%", *nextYear); + + KHttpCookieList list = jar->makeCookies(urlStr, line, 0); + + if (list.isEmpty()) + FAIL(QString("Failed to make cookies from: '%1'").arg(line)); + + for(KHttpCookie *cookie = list.first(); + cookie; cookie = list.next()) + { + KCookieAdvice cookieAdvice = jar->cookieAdvice(cookie); + if (cookieAdvice != expectedAdvice) + FAIL(urlStr+QString("\n'%2'\nGot advice '%3' expected '%4'").arg(line) + .arg(KCookieJar::adviceToStr(cookieAdvice)) + .arg(KCookieJar::adviceToStr(expectedAdvice))); + jar->addCookie(cookie); + } +} + +static void processCheck(QCString &line) +{ + QString urlStr; + popArg(urlStr, line); + KURL url(urlStr); + if (!url.isValid()) + FAIL(QString("Invalid URL '%1'").arg(urlStr)); + if (url.isEmpty()) + FAIL(QString("Missing URL")); + + QString expectedCookies = QString::fromLatin1(line); + + QString cookies = jar->findCookies(urlStr, false, 0, 0).stripWhiteSpace(); + if (cookies != expectedCookies) + FAIL(urlStr+QString("\nGot '%1' expected '%2'") + .arg(cookies, expectedCookies)); +} + +static void processClear(QCString &line) +{ + if (line == "CONFIG") + clearConfig(); + else if (line == "COOKIES") + clearCookies(); + else + FAIL(QString("Unknown command 'CLEAR %1'").arg(line)); +} + +static void processConfig(QCString &line) +{ + QCString key; + popArg(key, line); + + if (key.isEmpty()) + FAIL(QString("Missing Key")); + + config->setGroup("Cookie Policy"); + config->writeEntry(key.data(), line.data()); + jar->loadConfig(config, false); +} + +static void processLine(QCString line) +{ + if (line.isEmpty()) + return; + + if (line[0] == '#') + { + if (line[1] == '#') + qWarning("%s", line.data()); + return; + } + + QCString command; + popArg(command, line); + if (command.isEmpty()) + return; + + if (command == "COOKIE") + processCookie(line); + else if (command == "CHECK") + processCheck(line); + else if (command == "CLEAR") + processClear(line); + else if (command == "CONFIG") + processConfig(line); + else if (command == "SAVE") + saveCookies(); + else + FAIL(QString("Unknown command '%1'").arg(command)); +} + +static void runRegression(const QString &filename) +{ + FILE *file = fopen(filename.local8Bit(), "r"); + if (!file) + FAIL(QString("Can't open '%1'").arg(filename)); + + char buf[4096]; + while (fgets(buf, sizeof(buf), file)) + { + int l = strlen(buf); + if (l) + { + l--; + buf[l] = 0; + } + processLine(buf); + } + qWarning("%s OK", filename.local8Bit().data()); +} + +int main(int argc, char *argv[]) +{ + QString arg1; + QCString arg2; + QString result; + + lastYear = new QCString(QString("Fri, 04-May-%1 01:00:00 GMT").arg(QDate::currentDate().year()-1).utf8()); + nextYear = new QCString(QString(" expires=Fri, 04-May-%1 01:00:00 GMT").arg(QDate::currentDate().year()+1).utf8()); + + KAboutData about("kcookietest", "kcookietest", "1.0", description, KAboutData::License_GPL, "(C) 2004 Waldo Bastian"); + KCmdLineArgs::init( argc, argv, &about); + + KCmdLineArgs::addCmdLineOptions( options ); + + KInstance a("kcookietest"); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + if (args->count() != 1) + KCmdLineArgs::usage(); + + jar = new KCookieJar; + + clearConfig(); + + QString file = args->url(0).path(); + runRegression(file); + return 0; +} diff --git a/kioslave/http/rfc2518.txt b/kioslave/http/rfc2518.txt new file mode 100644 index 000000000..34d2e942a --- /dev/null +++ b/kioslave/http/rfc2518.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc2518.txt diff --git a/kioslave/http/rfc2616.txt b/kioslave/http/rfc2616.txt new file mode 100644 index 000000000..7be662a97 --- /dev/null +++ b/kioslave/http/rfc2616.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc2616.txt diff --git a/kioslave/http/rfc2617.txt b/kioslave/http/rfc2617.txt new file mode 100644 index 000000000..da74cc63a --- /dev/null +++ b/kioslave/http/rfc2617.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc2617.txt diff --git a/kioslave/http/rfc2817.txt b/kioslave/http/rfc2817.txt new file mode 100644 index 000000000..a29dfc44b --- /dev/null +++ b/kioslave/http/rfc2817.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc2817.txt diff --git a/kioslave/http/rfc2818.txt b/kioslave/http/rfc2818.txt new file mode 100644 index 000000000..fff91b1a9 --- /dev/null +++ b/kioslave/http/rfc2818.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc2818.txt diff --git a/kioslave/http/rfc3229.txt b/kioslave/http/rfc3229.txt new file mode 100644 index 000000000..54a19b685 --- /dev/null +++ b/kioslave/http/rfc3229.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc3229.txt diff --git a/kioslave/http/rfc3253.txt b/kioslave/http/rfc3253.txt new file mode 100644 index 000000000..9968eea02 --- /dev/null +++ b/kioslave/http/rfc3253.txt @@ -0,0 +1 @@ +http://www.ietf.org/rfc/rfc3253.txt diff --git a/kioslave/http/shoutcast-icecast.txt b/kioslave/http/shoutcast-icecast.txt new file mode 100644 index 000000000..f7bdcf1e7 --- /dev/null +++ b/kioslave/http/shoutcast-icecast.txt @@ -0,0 +1,605 @@ + +Audio and Apache HTTPD +ApacheCon 2001 +Santa Clara, US + +April 6th, 2001 + +Sander van Zoest <sander@vanZoest.com> +Covalent Technologies, Inc. +<http://www.covalent.net/> + +Latest version can be found at: + <http://www.vanZoest.com/sander/apachecon/2001/> + +Introduction: + +About this paper: + +Contents: + + 1. Why serve Audio on the Net? + + This is almost like asking, why are you reading this? it might be + because of the excitement caused by the new media that has recently + crazed upon the internet. People are looking to bring their lifes onto + the net, one of the things that brings that closer to a reality is the + ability to hear live broadcasts of the worlds news, favorite sport; + hear music and to teleconference with others. Sometimes it is simply + to enhance the mood to a web site or to provide audio feedback of + actions performed by the visitor of the web site. + + 2. What makes delivering audio so different? + + The biggest reason to what makes audio different then traditional + web media such as graphics, text and HTML is the fact that timing + is very important. This caused by the significant increase in size + of the media and the different quality levels that exist. + + There really are two kinds of goals behind audio streams. + In one case there is a need for immediate response the moment + playback is requested and this can sacrifice quality. While + in the other case quality and a non-interrupted stream are much + more important. + + This sort of timing is not really required of any other media, + with the exception of video. In the case of HTML and images the + files sizes are usually a lot smaller which causes the objects + to load much quicker and usually are not very useful without + having the entire file. In audio the middle of a stream can have + useful information and still set a particular mood. + + 3. Different ways of delivery Audio on the Net. + Embedding audio in your Web Page + + This used to be a lot more common in the past. Just like embedding + an image in a web page, it is possible to add a sound clip or score + to the web page. + + The linked in audio files are usually short and of low quality to + avoid a long delay for downloading the rest of the web page and the + audio format needs to be supported by the browser natively or with + a browser plug-in to avoid annoying the visitor. + + This can be accomplished using the HTML 4.0 [HTML4] object element which + works similar to how to specify an applet with the object element. + In the past this could also be accomplished using the embed and bgsound + browser specific additions to HTML. + + example: + <object type="audio/x-midi" data="../media/sound.mid" width="200" height="26"> + <param name="src" value="../media/sound.mid"> + <param name="autostart" value="true"> + <param name="controls" value="ControlPanel"> + </object> + + Each param element is specific to each browser. Please check with each + browser for specific information in regards to what param elements are + available. + + In this method of delivering audio the audio file is served up via the + web server. When using an Apache HTTPD server make sure that the appropriate + mime type is configured for the audio file and that the audio file is + named and referenced by the appropriate extension. + + Although the current HTML 4.01 [HTML4] says to use the object element + many browsers out on the market today still look for the embed element. + Below find a little snipbit that will work work in many browsers. + + <object type="audio/x-midi" data="../media/sound.mid" width="200" height="26"> + <param name="src" value="../media/sound.mid"> + <param name="autostart" value="true"> + <param name="controls" value="ControlPanel"> + + <embed type="audio/x-midi" src="../media/sound.mid" + width="200" height="26" autoplay="true" controls="ControlPanel"> + <noembed>Your browser does not support embedded WAV files.</noembed> + </object> + + With the increasing installation base of the Flash browser plug-in by + Macromedia most developers that are looking to provide this kind of + functionality to a web page are creating flash elements that have their + own way of adding audio that is discussed in Flash specific documents. + + Downloading via HTTP + + Using this method the visitor to the website will have to download the + entire audio file and save it to the hard drive before it can be + listened to. (1) This is very popular with people that want to listen + to high quality streams of audio and have a below ISDN connection to + the internet. In some cases where the demand for a stream is high or + the internet is congested downloading the content even for high bandwidth + users can be affective and useful. + + One of the advantages of downloading audio to the local computer hard + drive is that it can be played back (once downloaded) any time as long + as the audio file is accessable from the computer. + + There are a lot of sites on the internet that provide this functionality + for music and other audio files. It is also one of the easiest ways to + delivery high quality audio to visitors. + + (1) Microsoft Windows Media Player in conjunction with the Microsoft + Internet Explorer Browser will automaticly start playing the + audio stream after a sufficient amount of the file has been + downloaded. This can be accomplished because of the tight + integration of the Browser and Media Player. With most audio players + you can listen to a file being downloaded, but you will have to + envoke the action manually. + + . On-Demand streaming via HTTP + + The real difference between downloading and on-demand streaming is + that in on-demand streaming the audio starts playing before the entire + audio file has been downloaded. This is accomplished by a hand of off + the browser to the audio player via an intermediate file format that + has been configured by the browser to be handled by the audio player. + + Look in a further section entitled "Linking to Audio via Apache HTTPD" + below for more information about the different intermediate file formats. + + This type of streaming is very popular among the open source crowd and + is the most widely implemented using the MP3 file format. Apache, + Shoutcast [SHOUTCAST] and Icecast [ICECAST] are the most common + software components used to provide on-demand streaming via HTTP. Both + Icecast and Shoutcast are not fully HTTP compliant, but Icecast is + becoming closer. For more information about the Shoutcast and Icecast + differences see the section below. + + Sites like Live365.com and MP3.com are huge sites that rely on this + method of delivery of audio. + + . On-Demand Streaming via RTSP/RTP + + RTSP/RTP is a new set of streaming protocols that is getting more + backing and becoming more popular by the second. The specification + was developed by the Internet Engineering Task Force Working Groups + AVT [IETFAVT] and MMUSIC [IETFMMUSIC]. RTP the Realtime Transfer + Protocol has been around longer then RTSP and originally came out + of the work towards a better teleconferencing, mbone, type system. + RTSP is the Real-Time Streaming Protocol that is used as a control + protocol and acts similarily to HTTP except that it maintains state + and is bi-directional. + + Currently the latest Real Networks Streaming Servers support RTSP + and RTP and Real Networks own proprietary transfer protocol RDT. + Apple's Darwin Streaming server is also RTSP/RTP compliant. + + The RTSP/RTP protocol suite is very powerful and flexable in regards + to your streaming needs. It has the ability to suport "server-push" + style stream redirects and has the ability to throttle streams to + ensure the stream can sustain the limited bandwidth over the network. + + For On-Demand streams the RTP protocol would usually stream over + TCP and have a second TCP connection open for RTSP. Because of the + rich features provided by the protocol suite, it is not very well + suited to allow people to download the stream and therefore the + download via HTTP method might still be preferred by some. + + . Live Broadcast Streaming via RTSP/RTP + + In the case of a live broadcast streaming RTSP/RTP shines. RTP allowing + for UDP datagrams to be transmitted to clients allows for fast immediate + delivery of content with the sacrifice of reliability. The RTP stream + can be send over IP Multicast to minimize bandwidth on the network. + + Many Content Delivery Networks (CDNs) are starting to provide support for + RTSP/RTP proxies that should provide a better quality streaming environment + on the internet. + + Much work is also being done in the RTP space to provide transfers over + telecommunication networks such as cellular phones. Although not directly + related, per se, it does provide a positive feeling knowing that all the + audio related transfer groups seem to be working towards a common standard + such as RTP. + + . On-Demand or Live Broadcast streaming via MMS. + + This is the Microsoft Windows Media Technologies Streaming protocol. It + is only supported by Microsoft Windows Media Player and currently only + works on Microsoft Windows. + + 5. Configuring Mime Types + + One of the most hardest things in serving audio has been the wide variety + of audio codecs and mime types available. The battle of mime types on the + audio player side of things isn't over, but it seems to be a little more + controlled. + + On the server side of things provide the appropriate mime type for the + particular audio streams and/or files that are being served to the audio + players. Although some clients and operating systems handle files fully + based on the file extension. The mime type [RFC2045] is more specific + and more defined. + + The registered mime types are maintained by IANA [IANA]. On their site + they have a list of all the registered mime types and their name space. + + If you are planning on using a mime type that isn't registered by IANA + then signal this in the name space by adding a "x-" before the subtype. + Because this was not done very often in the audio space, there was a + lot of confusion to what the real mime type should be. + + For example the MPEG 1.0 Layer 3 Audio (MP3) [ORAMP3BOOK] mime type + was not specified for the longest time. Because of this the mime type + was audio/x-mpeg. Although none of the audio players understood + audio/x-mpeg, but understood audio/mpeg it was not a technically + correct mime type. Later audio players recognized this and started + using the audio/x-mpeg mime type. Which in the end caused a lot + of hassles with clients needing to be configured differently depending + on the website and client that was used. Last november we thanked + Martin Nilsson of the ID3 tagging project for registering audo/mpeg + with IANA. [RFC3003] + + Correct configuration of Mime Types is very important. Apache HTTPD + ships with a fairly up to date copy of the mime.types file, so most + of the default ones (including audio/mpeg) are there. + + But in case you run into some that are not defined use the mod_mime + directives such as AddType to fix this. + + Examples: + AddType audio/x-mpegurl .m3u + AddType audio/x-scpls .pls + AddType application/x-ogg .ogg + + + 6. Common Audio File Formats + + There are many audio formats and metadata formats that exist. Many of + them do not have registered mime types and are hardly documented. + This section is an attempt at providing the most accurate mime type + information for each format with a rough description of what the files + are used for. + + . Real Audio + + Real Networks Proprietary audio format and meta formats. This is one + of the more common streaming audio formats today. It comes in several + sub flavors such as Real 5.0, Real G2 and Real 8.0 etc. The file size + varies depending on the bitrates and what combination of bitrates are + contained within the single file. + The following mime types are used + audio/x-pn-realaudio .ra, .ram, .rm + audio/x-pn-realaudio-plugin .rpm + application/x-pn-realmedia + + . MPEG 1.0 Layer 3 Audio (MP3) + + This is currently one of the most popular downloaded audio formats + that was originally developed by the Motion Pictures Experts Group + and has patents by the Fraunhofer IIS Institute and Thompson + Multimedia. [ORAMP3BOOK] The file is a lossy compression that at + a bitrate of 128kbps reduces the file size to roughly a MB/minute. + The mime type is audio/mpeg with the extension of .mp3 [RFC3003] + + . Windows Media Audio + + Originally known as MS Audio was developed by Microsoft as the MP3 + killer. Still relatively a new format but heavily marketed by + Microsoft and becoming more popular by the minute. It is a successor + to the Microsoft Audio Streaming Format (ASF). + + . WAV + + Windows Audio Format is a pretty semi-complicated encapsulating + format that in the most common case is PCM with a WAV header up front. + It has the mime type audio/x-wav with the extension .wav. + + . Vorbis + + Ogg Vorbis [VORBIS] is still a relatively new format brought to + life by CD Paranoia author Christopher Montgomery; known to the + world as Monty. It is an open source audio format free of patents + and gotchas. It is a codec/file format that is roughly as good as + the MP3 format, if not much better. The mime type for Ogg Vorbis is + application/x-ogg with the extension of .ogg. + + . MIDI + + The MIDI standard and file format [MIDISPEC] have been used by + Musicians for a long time. It is a great format to add music to + a website without the long download times and needing special players + or plug-ins. The mime type is audio/x-midi and the extension is .mid + + . Shockwave Flash (ADPCM/MP3) [FLASH4AUDIO] + + Macromedia Flash [FLASH4AUDIO] uses its own internal audio format + that is often used on Flash websites. It is based on Adaptive + Differential Pulse Code Modulation (ADPCM) and the MP3 file format. + Because it is usually used from within Flash it usually isn't served + up seperatedly but it's extension is .swf + + There are many many many more audio codecs and file formats that exist. + I have listed a few that won't be discussed but should be kept in mind. + Formats such as PCM/Raw Audio (audio/basic), MOD, MIDI (audio/x-midi), + QDesign (used by Quicktime), Beatnik, Sun's AU, Apple/SGI's AIFF, AAC + by the MPEG Group, Liquid Audio and AT&T's a2b (AAC derivatives), + Dolby AC-3, Yamaha's TwinVQ (originally by Nippon Telephone and Telegraph) + and MPEG-4 audio. + + 7. Linking to Audio via Apache HTTPD + + There are many different ways to link to audio from the Apache HTTPD + web server. It seems as if every codec has their own metafile format. + The metafile format is provided to allow the browser to hand off the + job of requesting the audio file to the audio player, because it is + more familiar with the file format and how to handle streaming or how + to actually connect to the audio server then the web browser is. + + This section will discuss the more common methods to provide streaming + links to provide that gateway from the web to the audio world. + + Probably the one that is the most recognized file is the RAM file. + + . RAM + + Real Audio Metafile. It is a pretty straight forward way that Real + Networks allowed their Real Player to take more control over their + proprietary audio streams. The file format is simply a URL on each + line that will be streamed in order by the client. The mime type + is the same as other RealAudio files audio/x-pn-realaudio where + the pn stands for Progressive Networks the old name of the company. + + . M3U + + This next one is the MPEG Layer 3 URL Metafile that has been around + for a very long time as a playlist format for MP3 players. It supported + URLs pretty early on by some players and got the mime type + audio/x-mpegurl and is now used by Icecast and many destination sites + such as MP3.com. The format is exactly the same as that of the RAM + file, just a list of urls that are separated by line feeds. + + . PLS + + This is the playlist files used by Nullsoft's Winamp MP3 Player. Later + on it got more widely used by Nullsoft's Shoutcast and has the mime + type of audio/x-scpls with the extension .pls. Before shoutcast the + mimetype was simply audio/x-pls. As you can see in the example below + it looks very much like a standard windows INI file format. + + Example: + [playlist] + numberofentries=2 + File1=<uri> + Title1=<title> + Length1=<length or -1> + File2=<uri> + Title2=<title> + Length2=<length or -1> + + . SDP + + This is the Session Description Protocol [RFC2327] which is heavily + used within RTSP and is a standard way of describing how to subscribe + to a particular RTP stream. The mime type is application/sdp with the + extension .sdp . + + Sometimes you might see RTSL (Real-Time Streaming Language) floating + around. This was an old Real Networks format that has been succeeded + by SDP. It's mimetype was application/x-rtsl with the extension of .rtsl + + . ASX + + Is a Windows Media Metafile format [MSASX] that is based on early XML + standards. It can be found with many extensions such as .wvx, .wax + and .asx. I am not aware of a mime type for this format. + + . SMIL + + Is the Synchronized Multimedia Integration Language [SMIL20] that + is now a W3C Recommendation [W3SYMM]. It was originally developed + by Real Networks to provide an HTML-like language to their Real Player + that was more focused on multimedia. The mime type is application/smil + with the extensions of either .smil or .smi + + . MHEG + + Is a hypertext language developed by the ISO group. [MHEG1] [MHEG5] + and [MHEG5COR]. It has been adopted by the Digital Audio Visual + Council [DAVIC]. It is more used for teleconferencing, broadcasting + and television, but close enough related that it receives a mention + here. The mime type is application/x-mheg with the extension of + .mheg + + 8. Configuring Apache HTTPD specificly to serve large Audio Files + + Some of the most common things that you will need to adjust to be + able to serve many large audio files via the Apache HTTPD Server. + Because of the difference in size between HTML files and Audio files, + the MaxClients will need to be adjusted appropriatedly depending on + the amount of time listeners end up tieing up a process. If you are + serving high quality MP3 files at 128kbps for example you should + expect more then 5 minute download times for most people. + + This will significantly impact your webserver since this means that + that process is occupied for the entire time. Because of this you + will also want to in crease the TimeOut Directive to a higher + number. This is to ensure that connections do not get disconnected + half way through a transfer and having that person hit "reload" + and connect again. + + Because of the amount of time the downloads tie up the processes + of the server, the smallest footprint of the server in memory would + be recommended because that would mean you could run more processes + on the machine. + + After that normal performance tweaks such as max file descriptor + changes and longer tcp listen queues apply. + + 9. Icecast/Shoutcast Protocol. + + Both protocols are very tightly based on HTTP/1.0. The main difference + is a group of new headers such as the icy headers by Shoutcast and the + new x-audiocast headers provided by Icecast. + + A typical shoutcast request from the client. + + GET / HTTP/1.0 + + ICY 200 OK + icy-notice1:<BR>This stream requires <a href="http://www.winamp.com/"> + Winamp</a><BR> + icy-notice2:SHOUTcast Distributed Network Audio Server/posix v1.0b<BR> + icy-name: Great Songs + icy-genre: Jazz + icy-url: http://shout.serv.dom/ + icy-pub: 1 + icy-br: 24 + + <data><songtitle><data> + + The icy headers display the song title and other formation including if + this stream is public and what the bitrate is. + + A typical icecast request from the client. + + GET / HTTP/1.0 + Host: icecast.serv.dom + x-audiocast-udpport: 6000 + Icy-MetaData: 0 + Accept: */* + + HTTP/1.0 200 OK + Server: Icecast/VERSION + Content-Type: audio/mpeg + x-audiocast-name: Great Songs + x-audiocast-genre: Jazz + x-audiocast-url: http://icecast.serv.dom/ + x-audiocast-streamid: + x-audiocast-public: 0 + x-audiocast-bitrate: 24 + x-audiocast-description: served by Icecast + + <data> + + NOTE: I am mixing the headers of the controlling client with those form + a listening client. This might be better explained at a latter + date. + + The CPAN Perl Package Apache::MP3 by Lincoln Stein implements a little of + each which works because MP3 players tend to support both. + + One of the big differences in implementations between the listening clients + is that Icecast uses an out of band UDP channel to update metadata + while the Shoutcast server gets it meta data from the client embedded within + the MP3 stream. The general meta data for the stream is set up via the + icy and x-audiocast HTTP headers. + + Although the MP3 standard documents were written for interrupted communication + it is not very specific on that. So although it doesn't state that there is + anything wrong with embedding garbage between MPEG frames the players that + do not understand it might make a noisy bleep and chirps because of it. + +References and Further Reading: + +[DAVIC] + Digital Audio Visual Council + <http://www.davic.org/> + +[FLASH4AUDIO] + L. J. Lotus, "Flash 4: Audio Options", ZD, Inc. 2000. + <http://www.zdnet.com/devhead/stories/articles/0,4413,2580376,00.html> + +[HTML4] + D. Ragget, A. Le Hors, I. Jacobs, "HTML 4.01 Specification", W3C + Recommendation, December, 1999. + <http://www.w3.org/TR/html401/> + +[IANA] + Internet Assigned Numbers Authority. + <http:/www.iana.org/> + +[ICECAST] + Icecast Open Source Streaming Audio System. + <http://www.icecast.org/> + +[IETFAVT] + Audio/Video Transport WG, Internet Engineering Task Force. + <http://www.ietf.org/html.charters/avt-charter.html> + +[IETFMMUSIC] + Multiparty Multimedia Session Control WG, Internet Engineering Task + Force. <http://www.ietf.org/html.charters/mmusic-charter.html> + +[IETFSIP] + Session Initiation Protocol WG, Internet Engineering Task Force. + <http://www.ietf.org/html.charters/sip-charter.html> + +[IPMULTICAST] + Transmit information to a group of recipients via a single transmission + by the source, in contrast to unicast. + IP Multicast Initiative + <http://www.ipmulticast.com/> + +[MIDISPEC] + The International MIDI Association,"MIDI File Format Spec 1.1", + <http://www.vanZoest.com/sander/apachecon/2001/midispec.html> + +[MHEG1] + ISO/IEC, "Information Technology - Coding of Multimedia and Hypermedia + Information - Part 1: MHEG Object Representation, Base Notation (ASN.1)"; + Draft International Standard ISO 13522-1;1997; + <http://www.ansi.org/> + <http://www.iso.ch/cate/d22153.html> + +[MHEG5] + ISO/IEC, "Information Technology - Coding of Multimedia and Hypermedia + Information - Part 5: Support for Base-Level Interactive Applications"; + Draft International Standard ISO 13522-5:1997; + <http://www.ansi.org/> + <http://www.iso.ch/cate/d26876.html> + +[MHEG5COR] + Information Technology - Coding of Multimedia and Hypermedia Information + - Part 5: Support for base-level interactive applications - + - Technical Corrigendum 1; ISO/IEC 13552-5:1997/Cor.1:1999(E) + <http://www.ansi.org/> + <http://www.iso.ch/cate/d31582.html> + +[MSASX] + Microsoft Corp. "All About Windows Media Metafiles". October 2000. + <http://msdn.microsoft.com/workshop/imedia/windowsmedia/ + crcontent/asx.asp> + +[ORAMP3] + S. Hacker; MP3: The Definitive Guide; O'Reilly and Associates, Inc. + March, 2000. + <http://www.oreilly.com/catalog/mp3/> +[RFC2045] + N. Freed and N. Borenstein, "Multipurpose Internet Mail + Extensions (MIME) Part One: Format of Internet Message Bodies", + RFC 2045, November 1996. <http://www.ietf.org/rfc/2045.txt> + +[RFC2327] + M. Handley and V. Jacobson, "SDP: Session Description Protocol", + RFC 2327, April 1998. <http://www.ietf.org/rfc/rfc2327.txt> + +[RFC3003] + M. Nilsson, "The audio/mpeg Media Type", RFC 3003, November 2000. + <http://www.ietf.org/rfc/rfc3003.txt> + +[SHOUTCAST] + Nullsoft Shoutcast MP3 Streaming Technology. + <http://www.shoutcast.com/> + +[SMIL20] + L. Rutledge, J. van Ossenbruggen, L. Hardman, D. Bulterman, + "Anticipating SMIL 2.0: The Developing Cooperative Infrastructure + for Multimedia on the Web"; 8th International WWW Conference, + Proc. May, 1999. + <http://www8.org/w8-papers/3c-hypermedia-video/anticipating/ + anticipating.html> + +[W39CIR] + V. Krishnan and S. G. Chang, "Customized Internet Radio"; 9th + International WWW Conference Proc. May 2000. + <http://www9.org/w9cdrom/353/353.html> + +[VORBIS] + Ogg Vorbis - Open Source Audio Codec + <http://www.xiph.org/ogg/vorbis/> + +[W3SYMM] + W3C Synchronized Multimedia Activity (SYMM Working Group); + <http://www.w3.org/AudioVideo/> diff --git a/kioslave/http/webdav.protocol b/kioslave/http/webdav.protocol new file mode 100644 index 000000000..f4f4df462 --- /dev/null +++ b/kioslave/http/webdav.protocol @@ -0,0 +1,18 @@ +[Protocol] +exec=kio_http +protocol=webdav +input=none +output=filesystem +listing=Name,Type,Size,Date,AccessDate,Access +reading=true +writing=true +makedir=true +deleting=true +moving=true +deleteRecursive=true +defaultMimetype=application/octet-stream +determineMimetypeFromExtension=false +Icon=www +maxInstances=3 +DocPath=kioslave/webdav.html +Class=:internet diff --git a/kioslave/http/webdavs.protocol b/kioslave/http/webdavs.protocol new file mode 100644 index 000000000..c8b7cba3f --- /dev/null +++ b/kioslave/http/webdavs.protocol @@ -0,0 +1,18 @@ +[Protocol] +exec=kio_http +protocol=webdavs +input=none +output=filesystem +listing=Name,Type,Size,Date,AccessDate,Access +reading=true +writing=true +makedir=true +deleting=true +moving=true +deleteRecursive=true +defaultMimetype=application/octet-stream +determineMimetypeFromExtension=false +Icon=www +config=webdav +DocPath=kioslave/webdavs.html +Class=:internet |