diff options
Diffstat (limited to 'kioslave')
67 files changed, 21993 insertions, 0 deletions
diff --git a/kioslave/Mainpage.dox b/kioslave/Mainpage.dox new file mode 100644 index 000000000..44979b667 --- /dev/null +++ b/kioslave/Mainpage.dox @@ -0,0 +1,9 @@ +/** @mainpage KIO Slaves +* +* KIO Slaves are out-of-process worker applications that +* perform protocol-specific communications. +* +* - <a href="../http/html/">http</a> ioslave +* - ftp ioslave +* +*/ diff --git a/kioslave/Makefile.am b/kioslave/Makefile.am new file mode 100644 index 000000000..42f294e76 --- /dev/null +++ b/kioslave/Makefile.am @@ -0,0 +1,27 @@ +# This file is part of the KDE libraries +# Copyright (C) 1997 Torben Weis (weis@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. + +if include_bzip2 +BZIP2DIR=bzip2 +endif + +SUBDIRS = file http ftp gzip $(BZIP2DIR) metainfo + +messages: # they get into kio.pot + +include $(top_srcdir)/admin/Doxyfile.am diff --git a/kioslave/bzip2/Makefile.am b/kioslave/bzip2/Makefile.am new file mode 100644 index 000000000..bb5ca3dc5 --- /dev/null +++ b/kioslave/bzip2/Makefile.am @@ -0,0 +1,11 @@ +INCLUDES = -I$(top_srcdir)/kio $(all_includes) +METASOURCES = AUTO + +kde_module_LTLIBRARIES = kbzip2filter.la + +kbzip2filter_la_SOURCES = kbzip2filter.cpp +kbzip2filter_la_LIBADD = $(LIB_KIO) $(LIBBZ2) +kbzip2filter_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) + +kde_services_DATA = kbzip2filter.desktop + diff --git a/kioslave/bzip2/configure.in.in b/kioslave/bzip2/configure.in.in new file mode 100644 index 000000000..99392042d --- /dev/null +++ b/kioslave/bzip2/configure.in.in @@ -0,0 +1,11 @@ +AC_DEFUN([KIOBZIP2_CHECK_BZIP2], +[ +AC_REQUIRE([AC_FIND_BZIP2]) + +AM_CONDITIONAL(include_bzip2, test -n "$BZIP2DIR") +if test -n "$BZIP2DIR"; then + AC_DEFINE(HAVE_BZIP2_SUPPORT, 1, [Defines if bzip2 is compiled]) +fi +]) + +KIOBZIP2_CHECK_BZIP2 diff --git a/kioslave/bzip2/kbzip2filter.cpp b/kioslave/bzip2/kbzip2filter.cpp new file mode 100644 index 000000000..5ef6aaf5b --- /dev/null +++ b/kioslave/bzip2/kbzip2filter.cpp @@ -0,0 +1,187 @@ +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@kde.org> + + $Id$ + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + 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> + +#if defined( HAVE_BZIP2_SUPPORT ) + +// we don't need that +#define BZ_NO_STDIO +extern "C" { + #include <bzlib.h> +} + +#ifdef NEED_BZ2_PREFIX + #define bzDecompressInit(x,y,z) BZ2_bzDecompressInit(x,y,z) + #define bzDecompressEnd(x) BZ2_bzDecompressEnd(x) + #define bzCompressEnd(x) BZ2_bzCompressEnd(x) + #define bzDecompress(x) BZ2_bzDecompress(x) + #define bzCompress(x,y) BZ2_bzCompress(x, y) + #define bzCompressInit(x,y,z,a) BZ2_bzCompressInit(x, y, z, a); +#endif + +#include <kdebug.h> +#include <klibloader.h> + +#include "kbzip2filter.h" + +// For docu on this, see /usr/doc/bzip2-0.9.5d/bzip2-0.9.5d/manual_3.html + +class KBzip2FilterFactory : public KLibFactory +{ +public: + KBzip2FilterFactory() : KLibFactory() {} + virtual ~KBzip2FilterFactory(){} + QObject *createObject( QObject *, const char *, const char*, const QStringList & ) + { + return new KBzip2Filter; + } +}; + +K_EXPORT_COMPONENT_FACTORY( kbzip2filter, KBzip2FilterFactory ) + +// Not really useful anymore +class KBzip2Filter::KBzip2FilterPrivate +{ +public: + bz_stream zStream; +}; + +KBzip2Filter::KBzip2Filter() +{ + d = new KBzip2FilterPrivate; + d->zStream.bzalloc = 0; + d->zStream.bzfree = 0; + d->zStream.opaque = 0; + m_mode = 0; +} + + +KBzip2Filter::~KBzip2Filter() +{ + delete d; +} + +void KBzip2Filter::init( int mode ) +{ + d->zStream.next_in = 0; + d->zStream.avail_in = 0; + if ( mode == IO_ReadOnly ) + { + (void)bzDecompressInit(&d->zStream, 0, 0); + //kdDebug(7118) << "bzDecompressInit returned " << result << endl; + // No idea what to do with result :) + } else if ( mode == IO_WriteOnly ) { + (void)bzCompressInit(&d->zStream, 5, 0, 0); + //kdDebug(7118) << "bzDecompressInit returned " << result << endl; + } else + kdWarning(7118) << "Unsupported mode " << mode << ". Only IO_ReadOnly and IO_WriteOnly supported" << endl; + m_mode = mode; +} + +void KBzip2Filter::terminate() +{ + if ( m_mode == IO_ReadOnly ) + { + int result = bzDecompressEnd(&d->zStream); + kdDebug(7118) << "bzDecompressEnd returned " << result << endl; + } else if ( m_mode == IO_WriteOnly ) + { + int result = bzCompressEnd(&d->zStream); + kdDebug(7118) << "bzCompressEnd returned " << result << endl; + } else + kdWarning(7118) << "Unsupported mode " << m_mode << ". Only IO_ReadOnly and IO_WriteOnly supported" << endl; +} + + +void KBzip2Filter::reset() +{ + kdDebug(7118) << "KBzip2Filter::reset" << endl; + // bzip2 doesn't seem to have a reset call... + terminate(); + init( m_mode ); +} + +void KBzip2Filter::setOutBuffer( char * data, uint maxlen ) +{ + d->zStream.avail_out = maxlen; + d->zStream.next_out = data; +} + +void KBzip2Filter::setInBuffer( const char *data, unsigned int size ) +{ + d->zStream.avail_in = size; + d->zStream.next_in = const_cast<char *>(data); +} + +int KBzip2Filter::inBufferAvailable() const +{ + return d->zStream.avail_in; +} + +int KBzip2Filter::outBufferAvailable() const +{ + return d->zStream.avail_out; +} + +KBzip2Filter::Result KBzip2Filter::uncompress() +{ + //kdDebug(7118) << "Calling bzDecompress with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable() << endl; + int result = bzDecompress(&d->zStream); + if ( result != BZ_OK ) + { + kdDebug(7118) << "bzDecompress returned " << result << endl; + kdDebug(7118) << "KBzip2Filter::uncompress " << ( result == BZ_OK ? OK : ( result == BZ_STREAM_END ? END : ERROR ) ) << endl; + } + + switch (result) { + case BZ_OK: + return OK; + case BZ_STREAM_END: + return END; + default: + return ERROR; + } +} + +KBzip2Filter::Result KBzip2Filter::compress( bool finish ) +{ + //kdDebug(7118) << "Calling bzCompress with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable() << endl; + int result = bzCompress(&d->zStream, finish ? BZ_FINISH : BZ_RUN ); + + switch (result) { + case BZ_OK: + case BZ_FLUSH_OK: + case BZ_RUN_OK: + case BZ_FINISH_OK: + return OK; + break; + case BZ_STREAM_END: + kdDebug(7118) << " bzCompress returned " << result << endl; + return END; + break; + default: + kdDebug(7118) << " bzCompress returned " << result << endl; + return ERROR; + break; + } +} + +#endif diff --git a/kioslave/bzip2/kbzip2filter.desktop b/kioslave/bzip2/kbzip2filter.desktop new file mode 100644 index 000000000..d9d90ec88 --- /dev/null +++ b/kioslave/bzip2/kbzip2filter.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Type=Service +Name=BZip2 Filter +Name[af]=Bzip2 Filter +Name[ar]=فلتر BZip2 +Name[az]=BZip2 Filtri +Name[be]=Фільтр BZip2 +Name[bg]=Филтър BZip2 +Name[bn]=বি-জিপ২ (BZip2) ফিল্টার +Name[br]=Sil BZip2 +Name[ca]=Filtre BZip2 +Name[cs]=Filtr BZip2 +Name[csb]=Filter BZip2 +Name[cy]=Hidl BZip2 +Name[da]=BZip2-filter +Name[de]=BZip2-Filter +Name[el]=Φίλτρο BZip2 +Name[eo]=Bzip2-filtrilo +Name[es]=Filtro BZip2 +Name[et]=BZip2 filter +Name[eu]=BZip2 iragazkia +Name[fa]=پالایۀ BZip2 +Name[fi]=BZip2-suodin +Name[fr]=Filtre Bzip2 +Name[fy]=BZip2-filter +Name[ga]=Scagaire bzip2 +Name[gl]=Filtro BZip2 +Name[he]=מסנן BZip2 +Name[hi]=BZip2 फ़िल्टर +Name[hr]=BZip2 filtar +Name[hu]=BZip2 szűrő +Name[id]=Filter BZip2 +Name[is]=BZip2 sía +Name[it]=Filtro Bzip2 +Name[ja]=BZip2 フィルタ +Name[ka]=Bzip2 ფილტრი +Name[kk]=BZip2 сүзгісі +Name[km]=តម្រង BZip2 +Name[ko]=BZip2 거르개 +Name[lb]=BZip2-Filter +Name[lt]=BZip2 filtras +Name[lv]=BZip2 Filtrs +Name[mk]=BZip2 филтер +Name[mn]=BZip2-Filter +Name[ms]=Penapis BZip2 +Name[mt]=Filtru BZip2 +Name[nb]=BZip2-filter +Name[nds]=BZip2-Filter +Name[ne]=BZip2 फिल्टर +Name[nl]=BZip2-filter +Name[nn]=BZip2-filter +Name[nso]=Sesekodi sa BZip2 +Name[pa]=BZip2 ਫਿਲਟਰ +Name[pl]=Filtr BZip2 +Name[pt]=Filtro do Bzip2 +Name[pt_BR]=Filtro BZip2 +Name[ro]=Filtru BZip2 +Name[ru]=Фильтр bzip2 +Name[rw]=Muyunguruzi BZipu2 +Name[se]=BZip2-filter +Name[sk]=BZip2 filter +Name[sl]=Filter za bzip2 +Name[sq]=Filteri BZip2 +Name[sr]=BZip2 филтер +Name[sr@Latn]=BZip2 filter +Name[ss]=Sisefo se BZip2 +Name[sv]=Bzip2-filter +Name[ta]=BZip2 வடிகட்டி +Name[te]=బిజిప్2 గలని +Name[tg]=Таровиши BZip2 +Name[th]=ตัวกรอง BZip2 +Name[tr]=BZip2 Filtresi +Name[tt]=BZip2 Sözgeçe +Name[uk]=Фільтр BZip2 +Name[uz]=BZip2-filter +Name[uz@cyrillic]=BZip2-филтер +Name[ven]=Filithara ya BZip2 +Name[vi]=Bộ lọc BZip2 +Name[wa]=Passete BZip2 +Name[xh]=Isihluzi se BZip2 +Name[zh_CN]=BZip2 过滤程序 +Name[zh_HK]=BZip2 過濾器 +Name[zh_TW]=BZip2 過濾器 +Name[zu]=Ihluzo le-BZip2 +X-KDE-Library=kbzip2filter +ServiceTypes=KDECompressionFilter,application/x-bzip2,application/x-tbz diff --git a/kioslave/bzip2/kbzip2filter.h b/kioslave/bzip2/kbzip2filter.h new file mode 100644 index 000000000..47ccef915 --- /dev/null +++ b/kioslave/bzip2/kbzip2filter.h @@ -0,0 +1,54 @@ +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@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 version 2 as published by the Free Software Foundation. + + 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 __kbzip2filter__h +#define __kbzip2filter__h + +#include <config.h> + +#if defined( HAVE_BZIP2_SUPPORT ) + +#include "kfilterbase.h" + +class KBzip2Filter : public KFilterBase +{ +public: + KBzip2Filter(); + virtual ~KBzip2Filter(); + + virtual void init( int ); + virtual int mode() const { return m_mode; } + virtual void terminate(); + virtual void reset(); + virtual bool readHeader() { return true; } // bzip2 handles it by itself ! Cool ! + virtual bool writeHeader( const QCString & ) { return true; } + virtual void setOutBuffer( char * data, uint maxlen ); + virtual void setInBuffer( const char * data, uint size ); + virtual int inBufferAvailable() const; + virtual int outBufferAvailable() const; + virtual Result uncompress(); + virtual Result compress( bool finish ); +private: + class KBzip2FilterPrivate; + KBzip2FilterPrivate *d; + int m_mode; +}; + +#endif + +#endif diff --git a/kioslave/file/Makefile.am b/kioslave/file/Makefile.am new file mode 100644 index 000000000..0dd7a760f --- /dev/null +++ b/kioslave/file/Makefile.am @@ -0,0 +1,22 @@ +## Makefile.am of kdebase/kioslave/file + +AM_CPPFLAGS = -D_LARGEFILE64_SOURCE + +INCLUDES = $(all_includes) + +####### Files + +kde_module_LTLIBRARIES = kio_file.la + +kio_file_la_SOURCES = file.cc +kio_file_la_LIBADD = $(LIB_KIO) +kio_file_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) +noinst_HEADERS = file.h + +fileinclude_HEADERS = file.h +fileincludedir = $(includedir)/kio + +METASOURCES = AUTO + +kdelnkdir = $(kde_servicesdir) +kdelnk_DATA = file.protocol diff --git a/kioslave/file/file.cc b/kioslave/file/file.cc new file mode 100644 index 000000000..718d42125 --- /dev/null +++ b/kioslave/file/file.cc @@ -0,0 +1,1797 @@ +/* + Copyright (C) 2000-2002 Stephan Kulow <coolo@kde.org> + Copyright (C) 2000-2002 David Faure <faure@kde.org> + Copyright (C) 2000-2002 Waldo Bastian <bastian@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. +*/ + +// $Id$ + +#include <config.h> + +#include <qglobal.h> //for Q_OS_XXX +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/stat.h> +#ifdef HAVE_SYS_TIME_H +#include <sys/time.h> +#endif + +//sendfile has different semantics in different platforms +#if defined HAVE_SENDFILE && defined Q_OS_LINUX +#define USE_SENDFILE 1 +#endif + +#ifdef USE_SENDFILE +#include <sys/sendfile.h> +#endif + +#ifdef USE_POSIX_ACL +#include <sys/acl.h> +#ifdef HAVE_NON_POSIX_ACL_EXTENSIONS +#include <acl/libacl.h> +#else +#include <posixacladdons.h> +#endif +#endif + +#include <assert.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <signal.h> +#include <time.h> +#include <utime.h> +#include <unistd.h> +#ifdef HAVE_STRING_H +#include <string.h> +#endif + +#include <qvaluelist.h> +#include <qregexp.h> + +#include <kshred.h> +#include <kdebug.h> +#include <kurl.h> +#include <kinstance.h> +#include <ksimpleconfig.h> +#include <ktempfile.h> +#include <klocale.h> +#include <qfile.h> +#include <qstrlist.h> +#include "file.h" +#include <limits.h> +#include <kprocess.h> +#include <kmountpoint.h> +#include <kstandarddirs.h> + +#ifdef HAVE_VOLMGT +#include <volmgt.h> +#include <sys/mnttab.h> +#endif + +#include <kstandarddirs.h> +#include <kio/ioslave_defaults.h> +#include <klargefile.h> +#include <kglobal.h> +#include <kmimetype.h> + +using namespace KIO; + +#define MAX_IPC_SIZE (1024*32) + +static QString testLogFile( const char *_filename ); +#ifdef USE_POSIX_ACL +static QString aclAsString( acl_t p_acl ); +static bool isExtendedACL( acl_t p_acl ); +static void appendACLAtoms( const QCString & path, UDSEntry& entry, + mode_t type, bool withACL ); +#endif + +extern "C" { KDE_EXPORT int kdemain(int argc, char **argv); } + +int kdemain( int argc, char **argv ) +{ + KLocale::setMainCatalogue("kdelibs"); + KInstance instance( "kio_file" ); + ( void ) KGlobal::locale(); + + kdDebug(7101) << "Starting " << getpid() << endl; + + if (argc != 4) + { + fprintf(stderr, "Usage: kio_file protocol domain-socket1 domain-socket2\n"); + exit(-1); + } + + FileProtocol slave(argv[2], argv[3]); + slave.dispatchLoop(); + + kdDebug(7101) << "Done" << endl; + return 0; +} + + +FileProtocol::FileProtocol( const QCString &pool, const QCString &app ) : SlaveBase( "file", pool, app ) +{ + usercache.setAutoDelete( true ); + groupcache.setAutoDelete( true ); +} + + +int FileProtocol::setACL( const char *path, mode_t perm, bool directoryDefault ) +{ + int ret = 0; +#ifdef USE_POSIX_ACL + + const QString ACLString = metaData( "ACL_STRING" ); + const QString defaultACLString = metaData( "DEFAULT_ACL_STRING" ); + // Empty strings mean leave as is + if ( !ACLString.isEmpty() ) { + acl_t acl = 0; + if ( ACLString == "ACL_DELETE" ) { + // user told us to delete the extended ACL, so let's write only + // the minimal (UNIX permission bits) part + acl = acl_from_mode( perm ); + } + acl = acl_from_text( ACLString.latin1() ); + if ( acl_valid( acl ) == 0 ) { // let's be safe + ret = acl_set_file( path, ACL_TYPE_ACCESS, acl ); + kdDebug(7101) << "Set ACL on: " << path << " to: " << aclAsString( acl ) << endl; + } + acl_free( acl ); + if ( ret != 0 ) return ret; // better stop trying right away + } + + if ( directoryDefault && !defaultACLString.isEmpty() ) { + if ( defaultACLString == "ACL_DELETE" ) { + // user told us to delete the default ACL, do so + ret += acl_delete_def_file( path ); + } else { + acl_t acl = acl_from_text( defaultACLString.latin1() ); + if ( acl_valid( acl ) == 0 ) { // let's be safe + ret += acl_set_file( path, ACL_TYPE_DEFAULT, acl ); + kdDebug(7101) << "Set Default ACL on: " << path << " to: " << aclAsString( acl ) << endl; + } + acl_free( acl ); + } + } +#endif + return ret; +} + +void FileProtocol::chmod( const KURL& url, int permissions ) +{ + QCString _path( QFile::encodeName(url.path()) ); + /* FIXME: Should be atomic */ + if ( ::chmod( _path.data(), permissions ) == -1 || + ( setACL( _path.data(), permissions, false ) == -1 ) || + /* if not a directory, cannot set default ACLs */ + ( setACL( _path.data(), permissions, true ) == -1 && errno != ENOTDIR ) ) { + + switch (errno) { + case EPERM: + case EACCES: + error( KIO::ERR_ACCESS_DENIED, url.path() ); + break; + case ENOTSUP: + error( KIO::ERR_UNSUPPORTED_ACTION, url.path() ); + break; + case ENOSPC: + error( KIO::ERR_DISK_FULL, url.path() ); + break; + default: + error( KIO::ERR_CANNOT_CHMOD, url.path() ); + } + } else + finished(); +} + +void FileProtocol::mkdir( const KURL& url, int permissions ) +{ + QCString _path( QFile::encodeName(url.path())); + + kdDebug(7101) << "mkdir(): " << _path << ", permission = " << permissions << endl; + + KDE_struct_stat buff; + if ( KDE_stat( _path.data(), &buff ) == -1 ) { + if ( ::mkdir( _path.data(), 0777 /*umask will be applied*/ ) != 0 ) { + if ( errno == EACCES ) { + error( KIO::ERR_ACCESS_DENIED, url.path() ); + return; + } else if ( errno == ENOSPC ) { + error( KIO::ERR_DISK_FULL, url.path() ); + return; + } else { + error( KIO::ERR_COULD_NOT_MKDIR, url.path() ); + return; + } + } else { + if ( permissions != -1 ) + chmod( url, permissions ); + else + finished(); + return; + } + } + + if ( S_ISDIR( buff.st_mode ) ) { + kdDebug(7101) << "ERR_DIR_ALREADY_EXIST" << endl; + error( KIO::ERR_DIR_ALREADY_EXIST, url.path() ); + return; + } + error( KIO::ERR_FILE_ALREADY_EXIST, url.path() ); + return; +} + +void FileProtocol::get( const KURL& url ) +{ + if (!url.isLocalFile()) { + KURL redir(url); + redir.setProtocol(config()->readEntry("DefaultRemoteProtocol", "smb")); + redirection(redir); + finished(); + return; + } + + QCString _path( QFile::encodeName(url.path())); + KDE_struct_stat buff; + if ( KDE_stat( _path.data(), &buff ) == -1 ) { + if ( errno == EACCES ) + error( KIO::ERR_ACCESS_DENIED, url.path() ); + else + error( KIO::ERR_DOES_NOT_EXIST, url.path() ); + return; + } + + if ( S_ISDIR( buff.st_mode ) ) { + error( KIO::ERR_IS_DIRECTORY, url.path() ); + return; + } + if ( !S_ISREG( buff.st_mode ) ) { + error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.path() ); + return; + } + + int fd = KDE_open( _path.data(), O_RDONLY); + if ( fd < 0 ) { + error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.path() ); + return; + } + +#ifdef HAVE_FADVISE + posix_fadvise( fd, 0, 0, POSIX_FADV_SEQUENTIAL); +#endif + + // Determine the mimetype of the file to be retrieved, and emit it. + // This is mandatory in all slaves (for KRun/BrowserRun to work). + KMimeType::Ptr mt = KMimeType::findByURL( url, buff.st_mode, true /* local URL */ ); + emit mimeType( mt->name() ); + + KIO::filesize_t processed_size = 0; + + QString resumeOffset = metaData("resume"); + if ( !resumeOffset.isEmpty() ) + { + bool ok; + KIO::fileoffset_t offset = resumeOffset.toLongLong(&ok); + if (ok && (offset > 0) && (offset < buff.st_size)) + { + if (KDE_lseek(fd, offset, SEEK_SET) == offset) + { + canResume (); + processed_size = offset; + kdDebug( 7101 ) << "Resume offset: " << KIO::number(offset) << endl; + } + } + } + + totalSize( buff.st_size ); + + char buffer[ MAX_IPC_SIZE ]; + QByteArray array; + + while( 1 ) + { + int n = ::read( fd, buffer, MAX_IPC_SIZE ); + if (n == -1) + { + if (errno == EINTR) + continue; + error( KIO::ERR_COULD_NOT_READ, url.path()); + close(fd); + return; + } + if (n == 0) + break; // Finished + + array.setRawData(buffer, n); + data( array ); + array.resetRawData(buffer, n); + + processed_size += n; + processedSize( processed_size ); + + //kdDebug( 7101 ) << "Processed: " << KIO::number (processed_size) << endl; + } + + data( QByteArray() ); + + close( fd ); + + processedSize( buff.st_size ); + finished(); +} + +static int +write_all(int fd, const char *buf, size_t len) +{ + while (len > 0) + { + ssize_t written = write(fd, buf, len); + if (written < 0) + { + if (errno == EINTR) + continue; + return -1; + } + buf += written; + len -= written; + } + return 0; +} + +static bool +same_inode(const KDE_struct_stat &src, const KDE_struct_stat &dest) +{ + if (src.st_ino == dest.st_ino && + src.st_dev == dest.st_dev) + return true; + + return false; +} + +void FileProtocol::put( const KURL& url, int _mode, bool _overwrite, bool _resume ) +{ + QString dest_orig = url.path(); + QCString _dest_orig( QFile::encodeName(dest_orig)); + + kdDebug(7101) << "put(): " << dest_orig << ", mode=" << _mode << endl; + + QString dest_part( dest_orig ); + dest_part += QString::fromLatin1(".part"); + QCString _dest_part( QFile::encodeName(dest_part)); + + KDE_struct_stat buff_orig; + bool bOrigExists = (KDE_lstat( _dest_orig.data(), &buff_orig ) != -1); + bool bPartExists = false; + bool bMarkPartial = config()->readBoolEntry("MarkPartial", true); + + if (bMarkPartial) + { + KDE_struct_stat buff_part; + bPartExists = (KDE_stat( _dest_part.data(), &buff_part ) != -1); + + if (bPartExists && !_resume && !_overwrite && buff_part.st_size > 0 && S_ISREG(buff_part.st_mode)) + { + kdDebug(7101) << "FileProtocol::put : calling canResume with " + << KIO::number(buff_part.st_size) << endl; + + // Maybe we can use this partial file for resuming + // Tell about the size we have, and the app will tell us + // if it's ok to resume or not. + _resume = canResume( buff_part.st_size ); + + kdDebug(7101) << "FileProtocol::put got answer " << _resume << endl; + } + } + + if ( bOrigExists && !_overwrite && !_resume) + { + if (S_ISDIR(buff_orig.st_mode)) + error( KIO::ERR_DIR_ALREADY_EXIST, dest_orig ); + else + error( KIO::ERR_FILE_ALREADY_EXIST, dest_orig ); + return; + } + + int result; + QString dest; + QCString _dest; + + int fd = -1; + + // Loop until we got 0 (end of data) + do + { + QByteArray buffer; + dataReq(); // Request for data + result = readData( buffer ); + + if (result >= 0) + { + if (dest.isEmpty()) + { + if (bMarkPartial) + { + kdDebug(7101) << "Appending .part extension to " << dest_orig << endl; + dest = dest_part; + if ( bPartExists && !_resume ) + { + kdDebug(7101) << "Deleting partial file " << dest_part << endl; + remove( _dest_part.data() ); + // Catch errors when we try to open the file. + } + } + else + { + dest = dest_orig; + if ( bOrigExists && !_resume ) + { + kdDebug(7101) << "Deleting destination file " << dest_orig << endl; + remove( _dest_orig.data() ); + // Catch errors when we try to open the file. + } + } + + _dest = QFile::encodeName(dest); + + if ( _resume ) + { + fd = KDE_open( _dest.data(), O_RDWR ); // append if resuming + KDE_lseek(fd, 0, SEEK_END); // Seek to end + } + else + { + // WABA: Make sure that we keep writing permissions ourselves, + // otherwise we can be in for a surprise on NFS. + mode_t initialMode; + if (_mode != -1) + initialMode = _mode | S_IWUSR | S_IRUSR; + else + initialMode = 0666; + + fd = KDE_open(_dest.data(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); + } + + if ( fd < 0 ) + { + kdDebug(7101) << "####################### COULD NOT WRITE " << dest << " _mode=" << _mode << endl; + kdDebug(7101) << "errno==" << errno << "(" << strerror(errno) << ")" << endl; + if ( errno == EACCES ) + error( KIO::ERR_WRITE_ACCESS_DENIED, dest ); + else + error( KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest ); + return; + } + } + + if (write_all( fd, buffer.data(), buffer.size())) + { + if ( errno == ENOSPC ) // disk full + { + error( KIO::ERR_DISK_FULL, dest_orig); + result = -2; // means: remove dest file + } + else + { + kdWarning(7101) << "Couldn't write. Error:" << strerror(errno) << endl; + error( KIO::ERR_COULD_NOT_WRITE, dest_orig); + result = -1; + } + } + } + } + while ( result > 0 ); + + // An error occurred deal with it. + if (result < 0) + { + kdDebug(7101) << "Error during 'put'. Aborting." << endl; + + if (fd != -1) + { + close(fd); + + KDE_struct_stat buff; + if (bMarkPartial && KDE_stat( _dest.data(), &buff ) == 0) + { + int size = config()->readNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); + if (buff.st_size < size) + remove(_dest.data()); + } + } + + ::exit(255); + } + + if ( fd == -1 ) // we got nothing to write out, so we never opened the file + { + finished(); + return; + } + + if ( close(fd) ) + { + kdWarning(7101) << "Error when closing file descriptor:" << strerror(errno) << endl; + error( KIO::ERR_COULD_NOT_WRITE, dest_orig); + return; + } + + // after full download rename the file back to original name + if ( bMarkPartial ) + { + // If the original URL is a symlink and we were asked to overwrite it, + // remove the symlink first. This ensures that we do not overwrite the + // current source if the symlink points to it. + if( _overwrite && S_ISLNK( buff_orig.st_mode ) ) + remove( _dest_orig.data() ); + + if ( ::rename( _dest.data(), _dest_orig.data() ) ) + { + kdWarning(7101) << " Couldn't rename " << _dest << " to " << _dest_orig << endl; + error( KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig ); + return; + } + } + + // set final permissions + if ( _mode != -1 && !_resume ) + { + if (::chmod(_dest_orig.data(), _mode) != 0) + { + // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. + if ( KIO::testFileSystemFlag( _dest_orig, KIO::SupportsChmod ) ) + warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) ); + } + } + + // set modification time + const QString mtimeStr = metaData( "modified" ); + if ( !mtimeStr.isEmpty() ) { + QDateTime dt = QDateTime::fromString( mtimeStr, Qt::ISODate ); + if ( dt.isValid() ) { + KDE_struct_stat dest_statbuf; + if (KDE_stat( _dest_orig.data(), &dest_statbuf ) == 0) { + struct utimbuf utbuf; + utbuf.actime = dest_statbuf.st_atime; // access time, unchanged + utbuf.modtime = dt.toTime_t(); // modification time + kdDebug() << k_funcinfo << "setting modtime to " << utbuf.modtime << endl; + utime( _dest_orig.data(), &utbuf ); + } + } + + } + + // We have done our job => finish + finished(); +} + + +void FileProtocol::copy( const KURL &src, const KURL &dest, + int _mode, bool _overwrite ) +{ + kdDebug(7101) << "copy(): " << src << " -> " << dest << ", mode=" << _mode << endl; + + QCString _src( QFile::encodeName(src.path())); + QCString _dest( QFile::encodeName(dest.path())); + KDE_struct_stat buff_src; +#ifdef USE_POSIX_ACL + acl_t acl; +#endif + + if ( KDE_stat( _src.data(), &buff_src ) == -1 ) { + if ( errno == EACCES ) + error( KIO::ERR_ACCESS_DENIED, src.path() ); + else + error( KIO::ERR_DOES_NOT_EXIST, src.path() ); + return; + } + + if ( S_ISDIR( buff_src.st_mode ) ) { + error( KIO::ERR_IS_DIRECTORY, src.path() ); + return; + } + if ( S_ISFIFO( buff_src.st_mode ) || S_ISSOCK ( buff_src.st_mode ) ) { + error( KIO::ERR_CANNOT_OPEN_FOR_READING, src.path() ); + return; + } + + KDE_struct_stat buff_dest; + bool dest_exists = ( KDE_lstat( _dest.data(), &buff_dest ) != -1 ); + if ( dest_exists ) + { + if (S_ISDIR(buff_dest.st_mode)) + { + error( KIO::ERR_DIR_ALREADY_EXIST, dest.path() ); + return; + } + + if ( same_inode( buff_dest, buff_src) ) + { + error( KIO::ERR_IDENTICAL_FILES, dest.path() ); + return; + } + + if (!_overwrite) + { + error( KIO::ERR_FILE_ALREADY_EXIST, dest.path() ); + return; + } + + // If the destination is a symlink and overwrite is TRUE, + // remove the symlink first to prevent the scenario where + // the symlink actually points to current source! + if (_overwrite && S_ISLNK(buff_dest.st_mode)) + { + kdDebug(7101) << "copy(): LINK DESTINATION" << endl; + remove( _dest.data() ); + } + } + + int src_fd = KDE_open( _src.data(), O_RDONLY); + if ( src_fd < 0 ) { + error( KIO::ERR_CANNOT_OPEN_FOR_READING, src.path() ); + return; + } + +#ifdef HAVE_FADVISE + posix_fadvise(src_fd,0,0,POSIX_FADV_SEQUENTIAL); +#endif + // WABA: Make sure that we keep writing permissions ourselves, + // otherwise we can be in for a surprise on NFS. + mode_t initialMode; + if (_mode != -1) + initialMode = _mode | S_IWUSR; + else + initialMode = 0666; + + int dest_fd = KDE_open(_dest.data(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); + if ( dest_fd < 0 ) { + kdDebug(7101) << "###### COULD NOT WRITE " << dest.url() << endl; + if ( errno == EACCES ) { + error( KIO::ERR_WRITE_ACCESS_DENIED, dest.path() ); + } else { + error( KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest.path() ); + } + close(src_fd); + return; + } + +#ifdef HAVE_FADVISE + posix_fadvise(dest_fd,0,0,POSIX_FADV_SEQUENTIAL); +#endif + +#ifdef USE_POSIX_ACL + acl = acl_get_fd(src_fd); + if ( acl && !isExtendedACL( acl ) ) { + kdDebug(7101) << _dest.data() << " doesn't have extended ACL" << endl; + acl_free( acl ); + acl = NULL; + } +#endif + totalSize( buff_src.st_size ); + + KIO::filesize_t processed_size = 0; + char buffer[ MAX_IPC_SIZE ]; + int n; +#ifdef USE_SENDFILE + bool use_sendfile=buff_src.st_size < 0x7FFFFFFF; +#endif + while( 1 ) + { +#ifdef USE_SENDFILE + if (use_sendfile) { + off_t sf = processed_size; + n = ::sendfile( dest_fd, src_fd, &sf, MAX_IPC_SIZE ); + processed_size = sf; + if ( n == -1 && errno == EINVAL ) { //not all filesystems support sendfile() + kdDebug(7101) << "sendfile() not supported, falling back " << endl; + use_sendfile = false; + } + } + if (!use_sendfile) +#endif + n = ::read( src_fd, buffer, MAX_IPC_SIZE ); + + if (n == -1) + { + if (errno == EINTR) + continue; +#ifdef USE_SENDFILE + if ( use_sendfile ) { + kdDebug(7101) << "sendfile() error:" << strerror(errno) << endl; + if ( errno == ENOSPC ) // disk full + { + error( KIO::ERR_DISK_FULL, dest.path()); + remove( _dest.data() ); + } + else { + error( KIO::ERR_SLAVE_DEFINED, + i18n("Cannot copy file from %1 to %2. (Errno: %3)") + .arg( src.path() ).arg( dest.path() ).arg( errno ) ); + } + } else +#endif + error( KIO::ERR_COULD_NOT_READ, src.path()); + close(src_fd); + close(dest_fd); +#ifdef USE_POSIX_ACL + if (acl) acl_free(acl); +#endif + return; + } + if (n == 0) + break; // Finished +#ifdef USE_SENDFILE + if ( !use_sendfile ) { +#endif + if (write_all( dest_fd, buffer, n)) + { + close(src_fd); + close(dest_fd); + + if ( errno == ENOSPC ) // disk full + { + error( KIO::ERR_DISK_FULL, dest.path()); + remove( _dest.data() ); + } + else + { + kdWarning(7101) << "Couldn't write[2]. Error:" << strerror(errno) << endl; + error( KIO::ERR_COULD_NOT_WRITE, dest.path()); + } +#ifdef USE_POSIX_ACL + if (acl) acl_free(acl); +#endif + return; + } + processed_size += n; +#ifdef USE_SENDFILE + } +#endif + processedSize( processed_size ); + } + + close( src_fd ); + + if (close( dest_fd)) + { + kdWarning(7101) << "Error when closing file descriptor[2]:" << strerror(errno) << endl; + error( KIO::ERR_COULD_NOT_WRITE, dest.path()); +#ifdef USE_POSIX_ACL + if (acl) acl_free(acl); +#endif + return; + } + + // set final permissions + if ( _mode != -1 ) + { + if ( (::chmod(_dest.data(), _mode) != 0) +#ifdef USE_POSIX_ACL + || (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) +#endif + ) + { + // Eat the error if the filesystem apparently doesn't support chmod. + if ( KIO::testFileSystemFlag( _dest, KIO::SupportsChmod ) ) + warning( i18n( "Could not change permissions for\n%1" ).arg( dest.path() ) ); + } + } +#ifdef USE_POSIX_ACL + if (acl) acl_free(acl); +#endif + + // copy access and modification time + struct utimbuf ut; + ut.actime = buff_src.st_atime; + ut.modtime = buff_src.st_mtime; + if ( ::utime( _dest.data(), &ut ) != 0 ) + { + kdWarning() << QString::fromLatin1("Couldn't preserve access and modification time for\n%1").arg( dest.path() ) << endl; + } + + processedSize( buff_src.st_size ); + finished(); +} + +void FileProtocol::rename( const KURL &src, const KURL &dest, + bool _overwrite ) +{ + QCString _src( QFile::encodeName(src.path())); + QCString _dest( QFile::encodeName(dest.path())); + KDE_struct_stat buff_src; + if ( KDE_lstat( _src.data(), &buff_src ) == -1 ) { + if ( errno == EACCES ) + error( KIO::ERR_ACCESS_DENIED, src.path() ); + else + error( KIO::ERR_DOES_NOT_EXIST, src.path() ); + return; + } + + KDE_struct_stat buff_dest; + bool dest_exists = ( KDE_stat( _dest.data(), &buff_dest ) != -1 ); + if ( dest_exists ) + { + if (S_ISDIR(buff_dest.st_mode)) + { + error( KIO::ERR_DIR_ALREADY_EXIST, dest.path() ); + return; + } + + if ( same_inode( buff_dest, buff_src) ) + { + error( KIO::ERR_IDENTICAL_FILES, dest.path() ); + return; + } + + if (!_overwrite) + { + error( KIO::ERR_FILE_ALREADY_EXIST, dest.path() ); + return; + } + } + + if ( ::rename( _src.data(), _dest.data())) + { + if (( errno == EACCES ) || (errno == EPERM)) { + error( KIO::ERR_ACCESS_DENIED, dest.path() ); + } + else if (errno == EXDEV) { + error( KIO::ERR_UNSUPPORTED_ACTION, QString::fromLatin1("rename")); + } + else if (errno == EROFS) { // The file is on a read-only filesystem + error( KIO::ERR_CANNOT_DELETE, src.path() ); + } + else { + error( KIO::ERR_CANNOT_RENAME, src.path() ); + } + return; + } + + finished(); +} + +void FileProtocol::symlink( const QString &target, const KURL &dest, bool overwrite ) +{ + // Assume dest is local too (wouldn't be here otherwise) + if ( ::symlink( QFile::encodeName( target ), QFile::encodeName( dest.path() ) ) == -1 ) + { + // Does the destination already exist ? + if ( errno == EEXIST ) + { + if ( overwrite ) + { + // Try to delete the destination + if ( unlink( QFile::encodeName( dest.path() ) ) != 0 ) + { + error( KIO::ERR_CANNOT_DELETE, dest.path() ); + return; + } + // Try again - this won't loop forever since unlink succeeded + symlink( target, dest, overwrite ); + } + else + { + KDE_struct_stat buff_dest; + KDE_lstat( QFile::encodeName( dest.path() ), &buff_dest ); + if (S_ISDIR(buff_dest.st_mode)) + error( KIO::ERR_DIR_ALREADY_EXIST, dest.path() ); + else + error( KIO::ERR_FILE_ALREADY_EXIST, dest.path() ); + return; + } + } + else + { + // Some error occurred while we tried to symlink + error( KIO::ERR_CANNOT_SYMLINK, dest.path() ); + return; + } + } + finished(); +} + +void FileProtocol::del( const KURL& url, bool isfile) +{ + QCString _path( QFile::encodeName(url.path())); + /***** + * Delete files + *****/ + + if (isfile) { + kdDebug( 7101 ) << "Deleting file "<< url.url() << endl; + + // TODO deletingFile( source ); + + if ( unlink( _path.data() ) == -1 ) { + if ((errno == EACCES) || (errno == EPERM)) + error( KIO::ERR_ACCESS_DENIED, url.path()); + else if (errno == EISDIR) + error( KIO::ERR_IS_DIRECTORY, url.path()); + else + error( KIO::ERR_CANNOT_DELETE, url.path() ); + return; + } + } else { + + /***** + * Delete empty directory + *****/ + + kdDebug( 7101 ) << "Deleting directory " << url.url() << endl; + + if ( ::rmdir( _path.data() ) == -1 ) { + if ((errno == EACCES) || (errno == EPERM)) + error( KIO::ERR_ACCESS_DENIED, url.path()); + else { + kdDebug( 7101 ) << "could not rmdir " << perror << endl; + error( KIO::ERR_COULD_NOT_RMDIR, url.path() ); + return; + } + } + } + + finished(); +} + + +QString FileProtocol::getUserName( uid_t uid ) +{ + QString *temp; + temp = usercache.find( uid ); + if ( !temp ) { + struct passwd *user = getpwuid( uid ); + if ( user ) { + usercache.insert( uid, new QString(QString::fromLatin1(user->pw_name)) ); + return QString::fromLatin1( user->pw_name ); + } + else + return QString::number( uid ); + } + else + return *temp; +} + +QString FileProtocol::getGroupName( gid_t gid ) +{ + QString *temp; + temp = groupcache.find( gid ); + if ( !temp ) { + struct group *grp = getgrgid( gid ); + if ( grp ) { + groupcache.insert( gid, new QString(QString::fromLatin1(grp->gr_name)) ); + return QString::fromLatin1( grp->gr_name ); + } + else + return QString::number( gid ); + } + else + return *temp; +} + + + +bool FileProtocol::createUDSEntry( const QString & filename, const QCString & path, UDSEntry & entry, + short int details, bool withACL ) +{ + assert(entry.count() == 0); // by contract :-) + // Note: details = 0 (only "file or directory or symlink or doesn't exist") isn't implemented + // because there's no real performance penalty in kio_file for returning the complete + // details. Please consider doing it in your kioslave if you're using this one as a model :) + UDSAtom atom; + atom.m_uds = KIO::UDS_NAME; + atom.m_str = filename; + entry.append( atom ); + + mode_t type; + mode_t access; + KDE_struct_stat buff; + + if ( KDE_lstat( path.data(), &buff ) == 0 ) { + + if (S_ISLNK(buff.st_mode)) { + + char buffer2[ 1000 ]; + int n = readlink( path.data(), buffer2, 1000 ); + if ( n != -1 ) { + buffer2[ n ] = 0; + } + + atom.m_uds = KIO::UDS_LINK_DEST; + atom.m_str = QFile::decodeName( buffer2 ); + entry.append( atom ); + + // A symlink -> follow it only if details>1 + if ( details > 1 && KDE_stat( path.data(), &buff ) == -1 ) { + // It is a link pointing to nowhere + type = S_IFMT - 1; + access = S_IRWXU | S_IRWXG | S_IRWXO; + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = type; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = access; + entry.append( atom ); + + atom.m_uds = KIO::UDS_SIZE; + atom.m_long = 0L; + entry.append( atom ); + + goto notype; + + } + } + } else { + // kdWarning() << "lstat didn't work on " << path.data() << endl; + return false; + } + + type = buff.st_mode & S_IFMT; // extract file type + access = buff.st_mode & 07777; // extract permissions + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = type; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = access; + entry.append( atom ); + + atom.m_uds = KIO::UDS_SIZE; + atom.m_long = buff.st_size; + entry.append( atom ); + +#ifdef USE_POSIX_ACL + /* Append an atom indicating whether the file has extended acl information + * and if withACL is specified also one with the acl itself. If it's a directory + * and it has a default ACL, also append that. */ + appendACLAtoms( path, entry, type, withACL ); +#endif + + notype: + atom.m_uds = KIO::UDS_MODIFICATION_TIME; + atom.m_long = buff.st_mtime; + entry.append( atom ); + + atom.m_uds = KIO::UDS_USER; + atom.m_str = getUserName( buff.st_uid ); + entry.append( atom ); + + atom.m_uds = KIO::UDS_GROUP; + atom.m_str = getGroupName( buff.st_gid ); + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS_TIME; + atom.m_long = buff.st_atime; + entry.append( atom ); + + // Note: buff.st_ctime isn't the creation time ! + // We made that mistake for KDE 2.0, but it's in fact the + // "file status" change time, which we don't care about. + + return true; +} + +void FileProtocol::stat( const KURL & url ) +{ + if (!url.isLocalFile()) { + KURL redir(url); + redir.setProtocol(config()->readEntry("DefaultRemoteProtocol", "smb")); + redirection(redir); + kdDebug(7101) << "redirecting to " << redir.url() << endl; + finished(); + return; + } + + /* directories may not have a slash at the end if + * we want to stat() them; it requires that we + * change into it .. which may not be allowed + * stat("/is/unaccessible") -> rwx------ + * stat("/is/unaccessible/") -> EPERM H.Z. + * This is the reason for the -1 + */ + QCString _path( QFile::encodeName(url.path(-1))); + + QString sDetails = metaData(QString::fromLatin1("details")); + int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); + kdDebug(7101) << "FileProtocol::stat details=" << details << endl; + + UDSEntry entry; + if ( !createUDSEntry( url.fileName(), _path, entry, details, true /*with acls*/ ) ) + { + error( KIO::ERR_DOES_NOT_EXIST, url.path(-1) ); + return; + } +#if 0 +///////// debug code + KIO::UDSEntry::ConstIterator it = entry.begin(); + for( ; it != entry.end(); it++ ) { + switch ((*it).m_uds) { + case KIO::UDS_FILE_TYPE: + kdDebug(7101) << "File Type : " << (mode_t)((*it).m_long) << endl; + break; + case KIO::UDS_ACCESS: + kdDebug(7101) << "Access permissions : " << (mode_t)((*it).m_long) << endl; + break; + case KIO::UDS_USER: + kdDebug(7101) << "User : " << ((*it).m_str.ascii() ) << endl; + break; + case KIO::UDS_GROUP: + kdDebug(7101) << "Group : " << ((*it).m_str.ascii() ) << endl; + break; + case KIO::UDS_NAME: + kdDebug(7101) << "Name : " << ((*it).m_str.ascii() ) << endl; + //m_strText = decodeFileName( (*it).m_str ); + break; + case KIO::UDS_URL: + kdDebug(7101) << "URL : " << ((*it).m_str.ascii() ) << endl; + break; + case KIO::UDS_MIME_TYPE: + kdDebug(7101) << "MimeType : " << ((*it).m_str.ascii() ) << endl; + break; + case KIO::UDS_LINK_DEST: + kdDebug(7101) << "LinkDest : " << ((*it).m_str.ascii() ) << endl; + break; + case KIO::UDS_EXTENDED_ACL: + kdDebug(7101) << "Contains extended ACL " << endl; + break; + } + } + MetaData::iterator it1 = mOutgoingMetaData.begin(); + for ( ; it1 != mOutgoingMetaData.end(); it1++ ) { + kdDebug(7101) << it1.key() << " = " << it1.data() << endl; + } +///////// +#endif + statEntry( entry ); + + finished(); +} + +void FileProtocol::listDir( const KURL& url) +{ + kdDebug(7101) << "========= LIST " << url.url() << " =========" << endl; + if (!url.isLocalFile()) { + KURL redir(url); + redir.setProtocol(config()->readEntry("DefaultRemoteProtocol", "smb")); + redirection(redir); + kdDebug(7101) << "redirecting to " << redir.url() << endl; + finished(); + return; + } + + QCString _path( QFile::encodeName(url.path())); + + KDE_struct_stat buff; + if ( KDE_stat( _path.data(), &buff ) == -1 ) { + error( KIO::ERR_DOES_NOT_EXIST, url.path() ); + return; + } + + if ( !S_ISDIR( buff.st_mode ) ) { + error( KIO::ERR_IS_FILE, url.path() ); + return; + } + + DIR *dp = 0L; + KDE_struct_dirent *ep; + + dp = opendir( _path.data() ); + if ( dp == 0 ) { + switch (errno) + { +#ifdef ENOMEDIUM + case ENOMEDIUM: + error( ERR_SLAVE_DEFINED, + i18n( "No media in device for %1" ).arg( url.path() ) ); + break; +#endif + default: + error( KIO::ERR_CANNOT_ENTER_DIRECTORY, url.path() ); + break; + } + return; + } + + // Don't make this a QStringList. The locale file name we get here + // should be passed intact to createUDSEntry to avoid problems with + // files where QFile::encodeName(QFile::decodeName(a)) != a. + QStrList entryNames; + + while ( ( ep = KDE_readdir( dp ) ) != 0L ) + entryNames.append( ep->d_name ); + + closedir( dp ); + totalSize( entryNames.count() ); + + /* set the current dir to the path to speed up + in not having to pass an absolute path. + We restore the path later to get out of the + path - the kernel wouldn't unmount or delete + directories we keep as active directory. And + as the slave runs in the background, it's hard + to see for the user what the problem would be */ + char path_buffer[PATH_MAX]; + (void) getcwd(path_buffer, PATH_MAX - 1); + if ( chdir( _path.data() ) ) { + if (errno == EACCES) + error(ERR_ACCESS_DENIED, _path); + else + error(ERR_CANNOT_ENTER_DIRECTORY, _path); + finished(); + } + + UDSEntry entry; + QStrListIterator it(entryNames); + for (; it.current(); ++it) { + entry.clear(); + if ( createUDSEntry( QFile::decodeName(*it), + *it /* we can use the filename as relative path*/, + entry, 2, true ) ) + listEntry( entry, false); + //else + // ;//Well, this should never happen... but with wrong encoding names + } + + listEntry( entry, true ); // ready + + kdDebug(7101) << "============= COMPLETED LIST ============" << endl; + + chdir(path_buffer); + + finished(); +} + +/* +void FileProtocol::testDir( const QString& path ) +{ + QCString _path( QFile::encodeName(path)); + KDE_struct_stat buff; + if ( KDE_stat( _path.data(), &buff ) == -1 ) { + error( KIO::ERR_DOES_NOT_EXIST, path ); + return; + } + + if ( S_ISDIR( buff.st_mode ) ) + isDirectory(); + else + isFile(); + + finished(); +} +*/ + +void FileProtocol::special( const QByteArray &data) +{ + int tmp; + QDataStream stream(data, IO_ReadOnly); + + stream >> tmp; + switch (tmp) { + case 1: + { + QString fstype, dev, point; + Q_INT8 iRo; + + stream >> iRo >> fstype >> dev >> point; + + bool ro = ( iRo != 0 ); + + kdDebug(7101) << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro << endl; + bool ok = pmount( dev ); + if (ok) + finished(); + else + mount( ro, fstype.ascii(), dev, point ); + + } + break; + case 2: + { + QString point; + stream >> point; + bool ok = pumount( point ); + if (ok) + finished(); + else + unmount( point ); + } + break; + + case 3: + { + QString filename; + stream >> filename; + KShred shred( filename ); + connect( &shred, SIGNAL( processedSize( KIO::filesize_t ) ), + this, SLOT( slotProcessedSize( KIO::filesize_t ) ) ); + connect( &shred, SIGNAL( infoMessage( const QString & ) ), + this, SLOT( slotInfoMessage( const QString & ) ) ); + if (!shred.shred()) + error( KIO::ERR_CANNOT_DELETE, filename ); + else + finished(); + break; + } + default: + break; + } +} + +// Connected to KShred +void FileProtocol::slotProcessedSize( KIO::filesize_t bytes ) +{ + kdDebug(7101) << "FileProtocol::slotProcessedSize (" << (unsigned int) bytes << ")" << endl; + processedSize( bytes ); +} + +// Connected to KShred +void FileProtocol::slotInfoMessage( const QString & msg ) +{ + kdDebug(7101) << "FileProtocol::slotInfoMessage (" << msg << ")" << endl; + infoMessage( msg ); +} + +void FileProtocol::mount( bool _ro, const char *_fstype, const QString& _dev, const QString& _point ) +{ + kdDebug(7101) << "FileProtocol::mount _fstype=" << _fstype << endl; + QCString buffer; + +#ifdef HAVE_VOLMGT + /* + * support for Solaris volume management + */ + QString err; + QCString devname = QFile::encodeName( _dev ); + + if( volmgt_running() ) { +// kdDebug(7101) << "VOLMGT: vold ok." << endl; + if( volmgt_check( devname.data() ) == 0 ) { + kdDebug(7101) << "VOLMGT: no media in " + << devname.data() << endl; + err = i18n("No Media inserted or Media not recognized."); + error( KIO::ERR_COULD_NOT_MOUNT, err ); + return; + } else { + kdDebug(7101) << "VOLMGT: " << devname.data() + << ": media ok" << endl; + finished(); + return; + } + } else { + err = i18n("\"vold\" is not running."); + kdDebug(7101) << "VOLMGT: " << err << endl; + error( KIO::ERR_COULD_NOT_MOUNT, err ); + return; + } +#else + + + KTempFile tmpFile; + QCString tmpFileC = QFile::encodeName(tmpFile.name()); + const char *tmp = tmpFileC.data(); + QCString dev; + if ( _dev.startsWith( "LABEL=" ) ) { // turn LABEL=foo into -L foo (#71430) + QString labelName = _dev.mid( 6 ); + dev = "-L "; + dev += QFile::encodeName( KProcess::quote( labelName ) ); // is it correct to assume same encoding as filesystem? + } else if ( _dev.startsWith( "UUID=" ) ) { // and UUID=bar into -U bar + QString uuidName = _dev.mid( 5 ); + dev = "-U "; + dev += QFile::encodeName( KProcess::quote( uuidName ) ); + } + else + dev = QFile::encodeName( KProcess::quote(_dev) ); // get those ready to be given to a shell + + QCString point = QFile::encodeName( KProcess::quote(_point) ); + bool fstype_empty = !_fstype || !*_fstype; + QCString fstype = KProcess::quote(_fstype).latin1(); // good guess + QCString readonly = _ro ? "-r" : ""; + QString epath = QString::fromLatin1(getenv("PATH")); + QString path = QString::fromLatin1("/sbin:/bin"); + if(!epath.isEmpty()) + path += QString::fromLatin1(":") + epath; + QString mountProg = KGlobal::dirs()->findExe("mount", path); + if (mountProg.isEmpty()){ + error( KIO::ERR_COULD_NOT_MOUNT, i18n("Could not find program \"mount\"")); + return; + } + + // Two steps, in case mount doesn't like it when we pass all options + for ( int step = 0 ; step <= 1 ; step++ ) + { + // Mount using device only if no fstype nor mountpoint (KDE-1.x like) + if ( !_dev.isEmpty() && _point.isEmpty() && fstype_empty ) + buffer.sprintf( "%s %s 2>%s", mountProg.latin1(), dev.data(), tmp ); + else + // Mount using the mountpoint, if no fstype nor device (impossible in first step) + if ( !_point.isEmpty() && _dev.isEmpty() && fstype_empty ) + buffer.sprintf( "%s %s 2>%s", mountProg.latin1(), point.data(), tmp ); + else + // mount giving device + mountpoint but no fstype + if ( !_point.isEmpty() && !_dev.isEmpty() && fstype_empty ) + buffer.sprintf( "%s %s %s %s 2>%s", mountProg.latin1(), readonly.data(), dev.data(), point.data(), tmp ); + else + // mount giving device + mountpoint + fstype +#if defined(__svr4__) && defined(__sun__) // MARCO for Solaris 8 and I + // believe this is true for SVR4 in general + buffer.sprintf( "%s -F %s %s %s %s 2>%s" + mountProg.latin1() + fstype.data() + _ro ? "-oro" : "" + dev.data() + point.data() + tmp ); +#else + buffer.sprintf( "%s %s -t %s %s %s 2>%s", mountProg.latin1(), readonly.data(), + fstype.data(), dev.data(), point.data(), tmp ); +#endif + + kdDebug(7101) << buffer << endl; + + int mount_ret = system( buffer.data() ); + + QString err = testLogFile( tmp ); + if ( err.isEmpty() && mount_ret == 0) + { + finished(); + return; + } + else + { + // Didn't work - or maybe we just got a warning + QString mp = KIO::findDeviceMountPoint( _dev ); + // Is the device mounted ? + if ( !mp.isEmpty() && mount_ret == 0) + { + kdDebug(7101) << "mount got a warning: " << err << endl; + warning( err ); + finished(); + return; + } + else + { + if ( (step == 0) && !_point.isEmpty()) + { + kdDebug(7101) << err << endl; + kdDebug(7101) << "Mounting with those options didn't work, trying with only mountpoint" << endl; + fstype = ""; + fstype_empty = true; + dev = ""; + // The reason for trying with only mountpoint (instead of + // only device) is that some people (hi Malte!) have the + // same device associated with two mountpoints + // for different fstypes, like /dev/fd0 /mnt/e2floppy and + // /dev/fd0 /mnt/dosfloppy. + // If the user has the same mountpoint associated with two + // different devices, well they shouldn't specify the + // mountpoint but just the device. + } + else + { + error( KIO::ERR_COULD_NOT_MOUNT, err ); + return; + } + } + } + } +#endif /* ! HAVE_VOLMGT */ +} + + +void FileProtocol::unmount( const QString& _point ) +{ + QCString buffer; + + KTempFile tmpFile; + QCString tmpFileC = QFile::encodeName(tmpFile.name()); + QString err; + const char *tmp = tmpFileC.data(); + +#ifdef HAVE_VOLMGT + /* + * support for Solaris volume management + */ + char *devname; + char *ptr; + FILE *mnttab; + struct mnttab mnt; + + if( volmgt_running() ) { + kdDebug(7101) << "VOLMGT: looking for " + << _point.local8Bit() << endl; + + if( (mnttab = KDE_fopen( MNTTAB, "r" )) == NULL ) { + err = "couldn't open mnttab"; + kdDebug(7101) << "VOLMGT: " << err << endl; + error( KIO::ERR_COULD_NOT_UNMOUNT, err ); + return; + } + + /* + * since there's no way to derive the device name from + * the mount point through the volmgt library (and + * media_findname() won't work in this case), we have to + * look ourselves... + */ + devname = NULL; + rewind( mnttab ); + while( getmntent( mnttab, &mnt ) == 0 ) { + if( strcmp( _point.local8Bit(), mnt.mnt_mountp ) == 0 ){ + devname = mnt.mnt_special; + break; + } + } + fclose( mnttab ); + + if( devname == NULL ) { + err = "not in mnttab"; + kdDebug(7101) << "VOLMGT: " + << QFile::encodeName(_point).data() + << ": " << err << endl; + error( KIO::ERR_COULD_NOT_UNMOUNT, err ); + return; + } + + /* + * strip off the directory name (volume name) + * the eject(1) command will handle unmounting and + * physically eject the media (if possible) + */ + ptr = strrchr( devname, '/' ); + *ptr = '\0'; + QCString qdevname(QFile::encodeName(KProcess::quote(QFile::decodeName(QCString(devname)))).data()); + buffer.sprintf( "/usr/bin/eject %s 2>%s", qdevname.data(), tmp ); + kdDebug(7101) << "VOLMGT: eject " << qdevname << endl; + + /* + * from eject(1): exit status == 0 => need to manually eject + * exit status == 4 => media was ejected + */ +// if( WEXITSTATUS( system( buffer.local8Bit() )) == 4 ) { + if( WEXITSTATUS( system( buffer.data() )) == 4 ) { // Fix for QString -> QCString? + /* + * this is not an error, so skip "testLogFile()" + * to avoid wrong/confusing error popup + */ + unlink( tmp ); + finished(); + return; + } + } else { + /* + * eject(1) should do its job without vold(1M) running, + * so we probably could call eject anyway, but since the + * media is mounted now, vold must've died for some reason + * during the user's session, so it should be restarted... + */ + err = i18n("\"vold\" is not running."); + kdDebug(7101) << "VOLMGT: " << err << endl; + error( KIO::ERR_COULD_NOT_UNMOUNT, err ); + return; + } +#else + QString epath = getenv("PATH"); + QString path = QString::fromLatin1("/sbin:/bin"); + if (!epath.isEmpty()) + path += ":" + epath; + QString umountProg = KGlobal::dirs()->findExe("umount", path); + + if (umountProg.isEmpty()) { + error( KIO::ERR_COULD_NOT_UNMOUNT, i18n("Could not find program \"umount\"")); + return; + } + buffer.sprintf( "%s %s 2>%s", umountProg.latin1(), QFile::encodeName(KProcess::quote(_point)).data(), tmp ); + system( buffer.data() ); +#endif /* HAVE_VOLMGT */ + + err = testLogFile( tmp ); + if ( err.isEmpty() ) + finished(); + else + error( KIO::ERR_COULD_NOT_UNMOUNT, err ); +} + +/************************************* + * + * pmount handling + * + *************************************/ + +bool FileProtocol::pmount(const QString &dev) +{ + QString epath = getenv("PATH"); + QString path = QString::fromLatin1("/sbin:/bin"); + if (!epath.isEmpty()) + path += ":" + epath; + QString pmountProg = KGlobal::dirs()->findExe("pmount", path); + + if (pmountProg.isEmpty()) + return false; + + QCString buffer; + buffer.sprintf( "%s %s", QFile::encodeName(pmountProg).data(), + QFile::encodeName(KProcess::quote(dev)).data() ); + + int res = system( buffer.data() ); + + return res==0; +} + +bool FileProtocol::pumount(const QString &point) +{ + QString real_point = KStandardDirs::realPath(point); + + KMountPoint::List mtab = KMountPoint::currentMountPoints(); + + KMountPoint::List::const_iterator it = mtab.begin(); + KMountPoint::List::const_iterator end = mtab.end(); + + QString dev; + + for (; it!=end; ++it) + { + QString tmp = (*it)->mountedFrom(); + QString mp = (*it)->mountPoint(); + mp = KStandardDirs::realPath(mp); + + if (mp==real_point) + dev = KStandardDirs::realPath(tmp); + } + + if (dev.isEmpty()) return false; + if (dev.endsWith("/")) dev.truncate(dev.length()-1); + + QString epath = getenv("PATH"); + QString path = QString::fromLatin1("/sbin:/bin"); + if (!epath.isEmpty()) + path += ":" + epath; + QString pumountProg = KGlobal::dirs()->findExe("pumount", path); + + if (pumountProg.isEmpty()) + return false; + + QCString buffer; + buffer.sprintf( "%s %s", QFile::encodeName(pumountProg).data(), + QFile::encodeName(KProcess::quote(dev)).data() ); + + int res = system( buffer.data() ); + + return res==0; +} + +/************************************* + * + * Utilities + * + *************************************/ + +static QString testLogFile( const char *_filename ) +{ + char buffer[ 1024 ]; + KDE_struct_stat buff; + + QString result; + + KDE_stat( _filename, &buff ); + int size = buff.st_size; + if ( size == 0 ) { + unlink( _filename ); + return result; + } + + FILE * f = KDE_fopen( _filename, "rb" ); + if ( f == 0L ) { + unlink( _filename ); + result = i18n("Could not read %1").arg(QFile::decodeName(_filename)); + return result; + } + + result = ""; + const char *p = ""; + while ( p != 0L ) { + p = fgets( buffer, sizeof(buffer)-1, f ); + if ( p != 0L ) + result += QString::fromLocal8Bit(buffer); + } + + fclose( f ); + + unlink( _filename ); + + return result; +} + +/************************************* + * + * ACL handling helpers + * + *************************************/ +#ifdef USE_POSIX_ACL + +static bool isExtendedACL( acl_t acl ) +{ + return ( acl_equiv_mode( acl, 0 ) != 0 ); +} + +static QString aclAsString( acl_t acl ) +{ + char *aclString = acl_to_text( acl, 0 ); + QString ret = QString::fromLatin1( aclString ); + acl_free( (void*)aclString ); + return ret; +} + +static void appendACLAtoms( const QCString & path, UDSEntry& entry, mode_t type, bool withACL ) +{ + // first check for a noop +#ifdef HAVE_NON_POSIX_ACL_EXTENSIONS + if ( acl_extended_file( path.data() ) == 0 ) return; +#endif + + acl_t acl = 0; + acl_t defaultAcl = 0; + UDSAtom atom; + bool isDir = S_ISDIR( type ); + // do we have an acl for the file, and/or a default acl for the dir, if it is one? + if ( ( acl = acl_get_file( path.data(), ACL_TYPE_ACCESS ) ) ) { + if ( !isExtendedACL( acl ) ) { + acl_free( acl ); + acl = 0; + } + } + + /* Sadly libacl does not provided a means of checking for extended ACL and default + * ACL separately. Since a directory can have both, we need to check again. */ + if ( isDir ) + defaultAcl = acl_get_file( path.data(), ACL_TYPE_DEFAULT ); + + if ( acl || defaultAcl ) { + kdDebug(7101) << path.data() << " has extended ACL entries " << endl; + atom.m_uds = KIO::UDS_EXTENDED_ACL; + atom.m_long = 1; + entry.append( atom ); + } + if ( withACL ) { + if ( acl ) { + atom.m_uds = KIO::UDS_ACL_STRING; + atom.m_str = aclAsString( acl ); + entry.append( atom ); + kdDebug(7101) << path.data() << "ACL: " << atom.m_str << endl; + } + if ( defaultAcl ) { + atom.m_uds = KIO::UDS_DEFAULT_ACL_STRING; + atom.m_str = aclAsString( defaultAcl ); + entry.append( atom ); + kdDebug(7101) << path.data() << "DEFAULT ACL: " << atom.m_str << endl; + } + } + if ( acl ) acl_free( acl ); + if ( defaultAcl ) acl_free( defaultAcl ); +} +#endif + +#include "file.moc" diff --git a/kioslave/file/file.h b/kioslave/file/file.h new file mode 100644 index 000000000..eef71798b --- /dev/null +++ b/kioslave/file/file.h @@ -0,0 +1,98 @@ +/* + Copyright (C) 2000-2002 Stephan Kulow <coolo@kde.org> + Copyright (C) 2000-2002 David Faure <faure@kde.org> + Copyright (C) 2000-2002 Waldo Bastian <bastian@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. +*/ + +#ifndef __file_h__ +#define __file_h__ "$Id$" + +#include <sys/types.h> +#include <sys/stat.h> + +#include <stdio.h> +#include <unistd.h> + +#include <qobject.h> +#include <qintdict.h> +#include <qstring.h> +#include <qvaluelist.h> + +#include <kio/global.h> +#include <kio/slavebase.h> + +// Note that this header file is installed, so think twice +// before breaking binary compatibility (read: it is forbidden :) + +class FileProtocol : public QObject, public KIO::SlaveBase +{ + Q_OBJECT +public: + FileProtocol( const QCString &pool, const QCString &app); + virtual ~FileProtocol() { } + + virtual void get( const KURL& url ); + virtual void put( const KURL& url, int permissions, + bool overwrite, bool resume ); + virtual void copy( const KURL &src, const KURL &dest, + int permissions, bool overwrite ); + virtual void rename( const KURL &src, const KURL &dest, + bool overwrite ); + virtual void symlink( const QString &target, const KURL &dest, + bool overwrite ); + + virtual void stat( const KURL& url ); + virtual void listDir( const KURL& url ); + virtual void mkdir( const KURL& url, int permissions ); + virtual void chmod( const KURL& url, int permissions ); + virtual void del( const KURL& url, bool isfile); + + /** + * Special commands supported by this slave: + * 1 - mount + * 2 - unmount + * 3 - shred + */ + virtual void special( const QByteArray &data); + void unmount( const QString& point ); + void mount( bool _ro, const char *_fstype, const QString& dev, const QString& point ); + bool pumount( const QString &point ); + bool pmount( const QString &dev ); + +protected slots: + void slotProcessedSize( KIO::filesize_t _bytes ); + void slotInfoMessage( const QString & msg ); + +protected: + + bool createUDSEntry( const QString & filename, const QCString & path, KIO::UDSEntry & entry, + short int details, bool withACL ); + int setACL( const char *path, mode_t perm, bool _directoryDefault ); + + QString getUserName( uid_t uid ); + QString getGroupName( gid_t gid ); + + QIntDict<QString> usercache; // maps long ==> QString * + QIntDict<QString> groupcache; + + class FileProtocolPrivate; + FileProtocolPrivate *d; +}; + +#endif diff --git a/kioslave/file/file.protocol b/kioslave/file/file.protocol new file mode 100644 index 000000000..ae3487999 --- /dev/null +++ b/kioslave/file/file.protocol @@ -0,0 +1,15 @@ +[Protocol] +exec=kio_file +protocol=file +input=none +output=filesystem +listing=Name,Type,Size,Date,AccessDate,Access,Owner,Group,Link +reading=true +writing=true +makedir=true +deleting=true +linking=true +moving=true +maxInstances=4 +DocPath=kioslave/file.html +Class=:local diff --git a/kioslave/ftp/Makefile.am b/kioslave/ftp/Makefile.am new file mode 100644 index 000000000..cfa3de6ad --- /dev/null +++ b/kioslave/ftp/Makefile.am @@ -0,0 +1,16 @@ +INCLUDES= $(all_includes) + +####### Files + +kde_module_LTLIBRARIES = kio_ftp.la + +kio_ftp_la_SOURCES = ftp.cc +kio_ftp_la_LIBADD = $(LIB_KIO) +kio_ftp_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) + +noinst_HEADERS = ftp.h + +kdelnk_DATA = ftp.protocol +kdelnkdir = $(kde_servicesdir) + + diff --git a/kioslave/ftp/configure.in.in b/kioslave/ftp/configure.in.in new file mode 100644 index 000000000..0c94a9b9b --- /dev/null +++ b/kioslave/ftp/configure.in.in @@ -0,0 +1,5 @@ +dnl For kio_ftp +AC_LANG_SAVE +AC_LANG_CPLUSPLUS +AC_CHECK_FUNCS( setfsent ) +AC_LANG_RESTORE diff --git a/kioslave/ftp/ftp.cc b/kioslave/ftp/ftp.cc new file mode 100644 index 000000000..4da1912d7 --- /dev/null +++ b/kioslave/ftp/ftp.cc @@ -0,0 +1,2652 @@ +// -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@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. + + Recommended reading explaining FTP details and quirks: + http://cr.yp.to/ftp.html (by D.J. Bernstein) +*/ + + +#define KIO_FTP_PRIVATE_INCLUDE +#include "ftp.h" + +#include <sys/stat.h> +#ifdef HAVE_SYS_TIME_H +#include <sys/time.h> +#endif +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <netdb.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> + +#if TIME_WITH_SYS_TIME +#include <time.h> +#endif + +#include <qdir.h> + +#include <kdebug.h> +#include <klocale.h> +#include <kinstance.h> +#include <kmimemagic.h> +#include <kmimetype.h> +#include <ksockaddr.h> +#include <ksocketaddress.h> +#include <kio/ioslave_defaults.h> +#include <kio/slaveconfig.h> +#include <kremoteencoding.h> +#include <klargefile.h> + +#ifdef HAVE_STRTOLL + #define charToLongLong(a) strtoll(a, 0, 10) +#else + #define charToLongLong(a) strtol(a, 0, 10) +#endif + +// JPF: a remark on coding style (2004-03-06): +// Some calls to QString::fromLatin1() were removed from the code. In most places +// the KDE code relies on implicit creation of QStrings. Also Qt has a lot of +// const char* overloads, so that using QString::fromLatin1() can be ineffectient! + +#define FTP_LOGIN "anonymous" +#define FTP_PASSWD "anonymous@" + +//#undef kdDebug +#define ENABLE_CAN_RESUME + +// JPF: somebody should find a better solution for this or move this to KIO +// JPF: anyhow, in KDE 3.2.0 I found diffent MAX_IPC_SIZE definitions! +namespace KIO { + enum buffersizes + { /** + * largest buffer size that should be used to transfer data between + * KIO slaves using the data() function + */ + maximumIpcSize = 32 * 1024, + /** + * this is a reasonable value for an initial read() that a KIO slave + * can do to obtain data via a slow network connection. + */ + initialIpcSize = 2 * 1024, + /** + * recommended size of a data block passed to findBufferFileType() + */ + mimimumMimeSize = 1024 + }; + + // JPF: this helper was derived from write_all in file.cc (FileProtocol). + static // JPF: in ftp.cc we make it static + /** + * This helper handles some special issues (blocking and interrupted + * system call) when writing to a file handle. + * + * @return 0 on success or an error code on failure (ERR_COULD_NOT_WRITE, + * ERR_DISK_FULL, ERR_CONNECTION_BROKEN). + */ + int WriteToFile(int fd, const char *buf, size_t len) + { + while (len > 0) + { // JPF: shouldn't there be a KDE_write? + ssize_t written = write(fd, buf, len); + if (written >= 0) + { buf += written; + len -= written; + continue; + } + switch(errno) + { case EINTR: continue; + case EPIPE: return ERR_CONNECTION_BROKEN; + case ENOSPC: return ERR_DISK_FULL; + default: return ERR_COULD_NOT_WRITE; + } + } + return 0; + } +} + +KIO::filesize_t Ftp::UnknownSize = (KIO::filesize_t)-1; + +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_ftp" ); + ( void ) KGlobal::locale(); + + kdDebug(7102) << "Starting " << getpid() << endl; + + if (argc != 4) + { + fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n"); + exit(-1); + } + + Ftp slave(argv[2], argv[3]); + slave.dispatchLoop(); + + kdDebug(7102) << "Done" << endl; + return 0; +} + +//=============================================================================== +// FtpTextReader Read Text lines from a file (or socket) +//=============================================================================== + +void FtpTextReader::textClear() +{ m_iTextLine = m_iTextBuff = 0; + m_szText[0] = 0; + m_bTextEOF = m_bTextTruncated = false; +} + +int FtpTextReader::textRead(FtpSocket *pSock) +{ + // if we have still buffered data then move it to the left + char* pEOL; + if(m_iTextLine < m_iTextBuff) + { m_iTextBuff -= m_iTextLine; + memmove(m_szText, m_szText+m_iTextLine, m_iTextBuff); + pEOL = (char*)memchr(m_szText, '\n', m_iTextBuff); // have a complete line? + } + else + { m_iTextBuff = 0; + pEOL = NULL; + } + m_bTextEOF = m_bTextTruncated = false; + + // read data from the control socket until a complete line is read + int nBytes; + while(pEOL == NULL) + { + if(m_iTextBuff > textReadLimit) + { m_bTextTruncated = true; + m_iTextBuff = textReadLimit; + } + nBytes = pSock->read(m_szText+m_iTextBuff, sizeof(m_szText)-m_iTextBuff); + if(nBytes <= 0) + { + // This error can occur after the server closed the connection (after a timeout) + if(nBytes < 0) + pSock->debugMessage("textRead failed"); + m_bTextEOF = true; + pEOL = m_szText + m_iTextBuff; + } + else + { + m_iTextBuff += nBytes; + pEOL = (char*)memchr(m_szText, '\n', m_iTextBuff); + } + } + + nBytes = pEOL - m_szText; + m_iTextLine = nBytes + 1; + + if(nBytes > textReadLimit) + { m_bTextTruncated = true; + nBytes = textReadLimit; + } + if(nBytes && m_szText[nBytes-1] == '\r') + nBytes--; + m_szText[nBytes] = 0; + return nBytes; +} + +//=============================================================================== +// FtpSocket Helper Class for Data or Control Connections +//=============================================================================== +void FtpSocket::debugMessage(const char* pszMsg) const +{ + kdDebug(7102) << m_pszName << ": " << pszMsg << endl; +} + +int FtpSocket::errorMessage(int iErrorCode, const char* pszMsg) const +{ + kdError(7102) << m_pszName << ": " << pszMsg << endl; + return iErrorCode; +} + +int FtpSocket::connectSocket(int iTimeOutSec, bool bControl) +{ + closeSocket(); + + int iOpt = bControl ? KExtendedSocket::inetSocket + : KExtendedSocket::noResolve; + setSocketFlags(iOpt | socketFlags()); + setTimeout(iTimeOutSec); + + int iCon = KExtendedSocket::connect(); + if(iCon < 0) + { int iErrorCode = (status() == IO_LookupError) + ? ERR_UNKNOWN_HOST : ERR_COULD_NOT_CONNECT; + QString strMsg = KExtendedSocket::strError(status(), systemError()); + strMsg.prepend("connect failed (code %1): "); + return errorMessage(iErrorCode, strMsg.arg(iCon).latin1()); + } + if( !setAddressReusable(true) ) + return errorMessage(ERR_COULD_NOT_CREATE_SOCKET, "setAddressReusable failed"); + + if(!bControl) + { int on=1; + if( !setSocketOption(SO_KEEPALIVE, (char *)&on, sizeof(on)) ) + errorMessage(0, "Keepalive not allowed"); + + struct linger lng = { 1, 120 }; + if( !setSocketOption(SO_LINGER, (char *)&lng, sizeof (lng)) ) + errorMessage(0, "Linger mode was not allowed."); + } + + debugMessage("connected"); + return 0; +} + +void FtpSocket::closeSocket() +{ + if(m_server != -1 || fd() != -1) + debugMessage("disconnected"); + + if(m_server != -1) + { + ::shutdown(m_server, SHUT_RDWR); + ::close(m_server); + m_server = -1; + } + if(socketStatus() > nothing) + reset(); + textClear(); +} + +bool FtpSocket::setSocketOption(int opt, char*arg, socklen_t len) const +{ + return (setsockopt(sock(), SOL_SOCKET, opt, arg, len) != -1); +} + +//=============================================================================== +// Ftp +//=============================================================================== + +Ftp::Ftp( const QCString &pool, const QCString &app ) + : SlaveBase( "ftp", pool, app ) +{ + // init the socket data + m_data = m_control = NULL; + ftpCloseControlConnection(); + + // init other members + m_port = 0; + kdDebug(7102) << "Ftp::Ftp()" << endl; +} + + +Ftp::~Ftp() +{ + kdDebug(7102) << "Ftp::~Ftp()" << endl; + closeConnection(); +} + +/** + * This closes a data connection opened by ftpOpenDataConnection(). + */ +void Ftp::ftpCloseDataConnection() +{ + if(m_data != NULL) + { delete m_data; + m_data = NULL; + } +} + +/** + * This closes a control connection opened by ftpOpenControlConnection() and reinits the + * related states. This method gets called from the constructor with m_control = NULL. + */ +void Ftp::ftpCloseControlConnection() +{ + m_extControl = 0; + if(m_control) + delete m_control; + m_control = NULL; + m_cDataMode = 0; + m_bLoggedOn = false; // logon needs control connction + m_bTextMode = false; + m_bBusy = false; +} + +/** + * Returns the last response from the server (iOffset >= 0) -or- reads a new response + * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0). + */ +const char* Ftp::ftpResponse(int iOffset) +{ + assert(m_control != NULL); // must have control connection socket + const char *pTxt = m_control->textLine(); + + // read the next line ... + if(iOffset < 0) + { + int iMore = 0; + m_iRespCode = 0; + + // If the server sends multiline responses "nnn-text" we loop here until + // a final "nnn text" line is reached. Only data from the final line will + // be stored. Some servers (OpenBSD) send a single "nnn-" followed by + // optional lines that start with a space and a final "nnn text" line. + do { + int nBytes = m_control->textRead(); + int iCode = atoi(pTxt); + if(iCode > 0) m_iRespCode = iCode; + + // ignore lines starting with a space in multiline response + if(iMore != 0 && pTxt[0] == 32) + ; + // otherwise the line should start with "nnn-" or "nnn " + else if(nBytes < 4 || iCode < 100) + iMore = 0; + // we got a valid line, now check for multiline responses ... + else if(iMore == 0 && pTxt[3] == '-') + iMore = iCode; + // "nnn " ends multiline mode ... + else if(iMore != 0 && (iMore != iCode || pTxt[3] != '-')) + iMore = 0; + + if(iMore != 0) + kdDebug(7102) << " > " << pTxt << endl; + } while(iMore != 0); + kdDebug(7102) << "resp> " << pTxt << endl; + + m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0; + } + + // return text with offset ... + while(iOffset-- > 0 && pTxt[0]) + pTxt++; + return pTxt; +} + + +void Ftp::closeConnection() +{ + if(m_control != NULL || m_data != NULL) + kdDebug(7102) << "Ftp::closeConnection m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy << endl; + + if(m_bBusy) // ftpCloseCommand not called + { + kdWarning(7102) << "Ftp::closeConnection Abandoned data stream" << endl; + ftpCloseDataConnection(); + } + + if(m_bLoggedOn) // send quit + { + if( !ftpSendCmd( "quit", 0 ) || (m_iRespType != 2) ) + kdWarning(7102) << "Ftp::closeConnection QUIT returned error: " << m_iRespCode << endl; + } + + // close the data and control connections ... + ftpCloseDataConnection(); + ftpCloseControlConnection(); +} + +void Ftp::setHost( const QString& _host, int _port, const QString& _user, + const QString& _pass ) +{ + kdDebug(7102) << "Ftp::setHost (" << getpid() << "): " << _host << endl; + + m_proxyURL = metaData("UseProxy"); + m_bUseProxy = (m_proxyURL.isValid() && m_proxyURL.protocol() == "ftp"); + + if ( m_host != _host || m_port != _port || + m_user != _user || m_pass != _pass ) + closeConnection(); + + m_host = _host; + m_port = _port; + m_user = _user; + m_pass = _pass; +} + +void Ftp::openConnection() +{ + ftpOpenConnection(loginExplicit); +} + +bool Ftp::ftpOpenConnection (LoginMode loginMode) +{ + // check for implicit login if we are already logged on ... + if(loginMode == loginImplicit && m_bLoggedOn) + { + assert(m_control != NULL); // must have control connection socket + return true; + } + + kdDebug(7102) << "ftpOpenConnection " << m_host << ":" << m_port << " " + << m_user << " [password hidden]" << endl; + + infoMessage( i18n("Opening connection to host %1").arg(m_host) ); + + if ( m_host.isEmpty() ) + { + error( ERR_UNKNOWN_HOST, QString::null ); + return false; + } + + assert( !m_bLoggedOn ); + + m_initialPath = QString::null; + m_currentPath = QString::null; + + QString host = m_bUseProxy ? m_proxyURL.host() : m_host; + unsigned short int port = m_bUseProxy ? m_proxyURL.port() : m_port; + + if (!ftpOpenControlConnection(host, port) ) + return false; // error emitted by ftpOpenControlConnection + infoMessage( i18n("Connected to host %1").arg(m_host) ); + + if(loginMode != loginDefered) + { + m_bLoggedOn = ftpLogin(); + if( !m_bLoggedOn ) + return false; // error emitted by ftpLogin + } + + m_bTextMode = config()->readBoolEntry("textmode", false); + connected(); + return true; +} + + +/** + * Called by @ref openConnection. It opens the control connection to the ftp server. + * + * @return true on success. + */ +bool Ftp::ftpOpenControlConnection( const QString &host, unsigned short int port ) +{ + if ( port == 0 ) { + struct servent *pse; + if ( ( pse = getservbyname( "ftp", "tcp" ) ) == NULL ) + port = 21; + else + port = ntohs(pse->s_port); + } + + // implicitly close, then try to open a new connection ... + closeConnection(); + int iErrorCode = ERR_OUT_OF_MEMORY; + QString sErrorMsg; + m_control = new FtpSocket("CNTL"); + if(m_control != NULL) + { + // now connect to the server and read the login message ... + m_control->setAddress(host, port); + iErrorCode = m_control->connectSocket(connectTimeout(), true); + sErrorMsg = host; + + // on connect success try to read the server message... + if(iErrorCode == 0) + { + const char* psz = ftpResponse(-1); + if(m_iRespType != 2) + { // login not successful, do we have an message text? + if(psz[0]) + sErrorMsg = i18n("%1.\n\nReason: %2").arg(host).arg(psz); + iErrorCode = ERR_COULD_NOT_CONNECT; + } + } + } + + // if there was a problem - report it ... + if(iErrorCode == 0) // OK, return success + return true; + closeConnection(); // clean-up on error + error(iErrorCode, sErrorMsg); + return false; +} + +/** + * Called by @ref openConnection. It logs us in. + * @ref m_initialPath is set to the current working directory + * if logging on was successful. + * + * @return true on success. + */ +bool Ftp::ftpLogin() +{ + infoMessage( i18n("Sending login information") ); + + assert( !m_bLoggedOn ); + + QString user = m_user; + QString pass = m_pass; + + if ( config()->readBoolEntry("EnableAutoLogin") ) + { + QString au = config()->readEntry("autoLoginUser"); + if ( !au.isEmpty() ) + { + user = au; + pass = config()->readEntry("autoLoginPass"); + } + } + + // Try anonymous login if both username/password + // information is blank. + if (user.isEmpty() && pass.isEmpty()) + { + user = FTP_LOGIN; + pass = FTP_PASSWD; + } + + AuthInfo info; + info.url.setProtocol( "ftp" ); + info.url.setHost( m_host ); + info.url.setPort( m_port ); + info.url.setUser( user ); + + QCString tempbuf; + int failedAuth = 0; + + do + { + // Check the cache and/or prompt user for password if 1st + // login attempt failed OR the user supplied a login name, + // but no password. + if ( failedAuth > 0 || (!user.isEmpty() && pass.isEmpty()) ) + { + QString errorMsg; + kdDebug(7102) << "Prompting user for login info..." << endl; + + // Ask user if we should retry after when login fails! + if( failedAuth > 0 ) + { + errorMsg = i18n("Message sent:\nLogin using username=%1 and " + "password=[hidden]\n\nServer replied:\n%2\n\n" + ).arg(user).arg(ftpResponse(0)); + } + + if ( user != FTP_LOGIN ) + info.username = user; + + info.prompt = i18n("You need to supply a username and a password " + "to access this site."); + info.commentLabel = i18n( "Site:" ); + info.comment = i18n("<b>%1</b>").arg( m_host ); + info.keepPassword = true; // Prompt the user for persistence as well. + info.readOnly = (!m_user.isEmpty() && m_user != FTP_LOGIN); + + bool disablePassDlg = config()->readBoolEntry( "DisablePassDlg", false ); + if ( disablePassDlg || !openPassDlg( info, errorMsg ) ) + { + error( ERR_USER_CANCELED, m_host ); + return false; + } + else + { + user = info.username; + pass = info.password; + } + } + + tempbuf = "USER "; + tempbuf += user.latin1(); + if ( m_bUseProxy ) + { + tempbuf += '@'; + tempbuf += m_host.latin1(); + if ( m_port > 0 && m_port != DEFAULT_FTP_PORT ) + { + tempbuf += ':'; + tempbuf += QString::number(m_port).latin1(); + } + } + + kdDebug(7102) << "Sending Login name: " << tempbuf << endl; + + bool loggedIn = ( ftpSendCmd(tempbuf) && (m_iRespCode == 230) ); + bool needPass = (m_iRespCode == 331); + // Prompt user for login info if we do not + // get back a "230" or "331". + if ( !loggedIn && !needPass ) + { + kdDebug(7102) << "Login failed: " << ftpResponse(0) << endl; + ++failedAuth; + continue; // Well we failed, prompt the user please!! + } + + if( needPass ) + { + tempbuf = "pass "; + tempbuf += pass.latin1(); + kdDebug(7102) << "Sending Login password: " << "[protected]" << endl; + loggedIn = ( ftpSendCmd(tempbuf) && (m_iRespCode == 230) ); + } + + if ( loggedIn ) + { + // Do not cache the default login!! + if( user != FTP_LOGIN && pass != FTP_PASSWD ) + cacheAuthentication( info ); + failedAuth = -1; + } + + } while( ++failedAuth ); + + + kdDebug(7102) << "Login OK" << endl; + infoMessage( i18n("Login OK") ); + + // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix: + // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint + if( ftpSendCmd("SYST") && (m_iRespType == 2) ) + { + if( !strncmp( ftpResponse(0), "215 Windows_NT", 14 ) ) // should do for any version + { + ftpSendCmd( "site dirstyle" ); + // Check if it was already in Unix style + // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk> + if( !strncmp( ftpResponse(0), "200 MSDOS-like directory output is on", 37 )) + //It was in Unix style already! + ftpSendCmd( "site dirstyle" ); + // windows won't support chmod before KDE konquers their desktop... + m_extControl |= chmodUnknown; + } + } + else + kdWarning(7102) << "SYST failed" << endl; + + if ( config()->readBoolEntry ("EnableAutoLoginMacro") ) + ftpAutoLoginMacro (); + + // Get the current working directory + kdDebug(7102) << "Searching for pwd" << endl; + if( !ftpSendCmd("PWD") || (m_iRespType != 2) ) + { + kdDebug(7102) << "Couldn't issue pwd command" << endl; + error( ERR_COULD_NOT_LOGIN, i18n("Could not login to %1.").arg(m_host) ); // or anything better ? + return false; + } + + QString sTmp = remoteEncoding()->decode( ftpResponse(3) ); + int iBeg = sTmp.find('"'); + int iEnd = sTmp.findRev('"'); + if(iBeg > 0 && iBeg < iEnd) + { + m_initialPath = sTmp.mid(iBeg+1, iEnd-iBeg-1); + if(m_initialPath[0] != '/') m_initialPath.prepend('/'); + kdDebug(7102) << "Initial path set to: " << m_initialPath << endl; + m_currentPath = m_initialPath; + } + return true; +} + +void Ftp::ftpAutoLoginMacro () +{ + QString macro = metaData( "autoLoginMacro" ); + + if ( macro.isEmpty() ) + return; + + QStringList list = QStringList::split('\n', macro); + + for(QStringList::Iterator it = list.begin() ; it != list.end() ; ++it ) + { + if ( (*it).startsWith("init") ) + { + list = QStringList::split( '\\', macro); + it = list.begin(); + ++it; // ignore the macro name + + for( ; it != list.end() ; ++it ) + { + // TODO: Add support for arbitrary commands + // besides simply changing directory!! + if ( (*it).startsWith( "cwd" ) ) + ftpFolder( (*it).mid(4).stripWhiteSpace(), false ); + } + + break; + } + } +} + + +/** + * ftpSendCmd - send a command (@p cmd) and read response + * + * @param maxretries number of time it should retry. Since it recursively + * calls itself if it can't read the answer (this happens especially after + * timeouts), we need to limit the recursiveness ;-) + * + * return true if any response received, false on error + */ +bool Ftp::ftpSendCmd( const QCString& cmd, int maxretries ) +{ + assert(m_control != NULL); // must have control connection socket + + if ( cmd.find( '\r' ) != -1 || cmd.find( '\n' ) != -1) + { + kdWarning(7102) << "Invalid command received (contains CR or LF):" + << cmd.data() << endl; + error( ERR_UNSUPPORTED_ACTION, m_host ); + return false; + } + + // Don't print out the password... + bool isPassCmd = (cmd.left(4).lower() == "pass"); + if ( !isPassCmd ) + kdDebug(7102) << "send> " << cmd.data() << endl; + else + kdDebug(7102) << "send> pass [protected]" << endl; + + // Send the message... + QCString buf = cmd; + buf += "\r\n"; // Yes, must use CR/LF - see http://cr.yp.to/ftp/request.html + int num = m_control->write(buf.data(), buf.length()); + + // If we were able to successfully send the command, then we will + // attempt to read the response. Otherwise, take action to re-attempt + // the login based on the maximum number of retires specified... + if( num > 0 ) + ftpResponse(-1); + else + { m_iRespType = m_iRespCode = 0; + m_control->textClear(); + } + + // If respCh is NULL or the response is 421 (Timed-out), we try to re-send + // the command based on the value of maxretries. + if( (m_iRespType <= 0) || (m_iRespCode == 421) ) + { + // We have not yet logged on... + if (!m_bLoggedOn) + { + // The command was sent from the ftpLogin function, i.e. we are actually + // attempting to login in. NOTE: If we already sent the username, we + // return false and let the user decide whether (s)he wants to start from + // the beginning... + if (maxretries > 0 && !isPassCmd) + { + closeConnection (); + if( ftpOpenConnection(loginDefered) ) + ftpSendCmd ( cmd, maxretries - 1 ); + } + + return false; + } + else + { + if ( maxretries < 1 ) + return false; + else + { + kdDebug(7102) << "Was not able to communicate with " << m_host << endl + << "Attempting to re-establish connection." << endl; + + closeConnection(); // Close the old connection... + openConnection(); // Attempt to re-establish a new connection... + + if (!m_bLoggedOn) + { + if (m_control != NULL) // if openConnection succeeded ... + { + kdDebug(7102) << "Login failure, aborting" << endl; + error (ERR_COULD_NOT_LOGIN, m_host); + closeConnection (); + } + return false; + } + + kdDebug(7102) << "Logged back in, re-issuing command" << endl; + + // If we were able to login, resend the command... + if (maxretries) + maxretries--; + + return ftpSendCmd( cmd, maxretries ); + } + } + } + + return true; +} + +/* + * ftpOpenPASVDataConnection - set up data connection, using PASV mode + * + * return 1 if successful, 0 otherwise + * doesn't set error message, since non-pasv mode will always be tried if + * this one fails + */ +int Ftp::ftpOpenPASVDataConnection() +{ + assert(m_control != NULL); // must have control connection socket + assert(m_data == NULL); // ... but no data connection + + // Check that we can do PASV + const KSocketAddress *sa = m_control->peerAddress(); + if (sa != NULL && sa->family() != PF_INET) + return ERR_INTERNAL; // no PASV for non-PF_INET connections + + const KInetSocketAddress *sin = static_cast<const KInetSocketAddress*>(sa); + + if (m_extControl & pasvUnknown) + return ERR_INTERNAL; // already tried and got "unknown command" + + m_bPasv = true; + + /* Let's PASsiVe*/ + if( !ftpSendCmd("PASV") || (m_iRespType != 2) ) + { + kdDebug(7102) << "PASV attempt failed" << endl; + // unknown command? + if( m_iRespType == 5 ) + { + kdDebug(7102) << "disabling use of PASV" << endl; + m_extControl |= pasvUnknown; + } + return ERR_INTERNAL; + } + + // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)' + // but anonftpd gives '227 =160,39,200,55,6,245' + int i[6]; + const char *start = strchr(ftpResponse(3), '('); + if ( !start ) + start = strchr(ftpResponse(3), '='); + if ( !start || + ( sscanf(start, "(%d,%d,%d,%d,%d,%d)",&i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 && + sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 ) ) + { + kdError(7102) << "parsing IP and port numbers failed. String parsed: " << start << endl; + return ERR_INTERNAL; + } + + // Make hostname and port number ... + int port = i[4] << 8 | i[5]; + + // we ignore the host part on purpose for two reasons + // a) it might be wrong anyway + // b) it would make us being suceptible to a port scanning attack + + // now connect the data socket ... + m_data = new FtpSocket("PASV"); + m_data->setAddress(sin->nodeName(), port); + + kdDebug(7102) << "Connecting to " << sin->nodeName() << " on port " << port << endl; + return m_data->connectSocket(connectTimeout(), false); +} + +/* + * ftpOpenEPSVDataConnection - opens a data connection via EPSV + */ +int Ftp::ftpOpenEPSVDataConnection() +{ + assert(m_control != NULL); // must have control connection socket + assert(m_data == NULL); // ... but no data connection + + const KSocketAddress *sa = m_control->peerAddress(); + int portnum; + // we are sure sa is a KInetSocketAddress, because we asked for KExtendedSocket::inetSocket + // when we connected + const KInetSocketAddress *sin = static_cast<const KInetSocketAddress*>(sa); + + if (m_extControl & epsvUnknown || sa == NULL) + return ERR_INTERNAL; + + m_bPasv = true; + if( !ftpSendCmd("EPSV") || (m_iRespType != 2) ) + { + // unknown command? + if( m_iRespType == 5 ) + { + kdDebug(7102) << "disabling use of EPSV" << endl; + m_extControl |= epsvUnknown; + } + return ERR_INTERNAL; + } + + const char *start = strchr(ftpResponse(3), '|'); + if ( !start || sscanf(start, "|||%d|", &portnum) != 1) + return ERR_INTERNAL; + + m_data = new FtpSocket("EPSV"); + m_data->setAddress(sin->nodeName(), portnum); + return m_data->connectSocket(connectTimeout(), false) != 0; +} + +/* + * ftpOpenEPRTDataConnection + * @return 0 on success, ERR_INTERNAL if mode not acceptable -or- a fatal error code + */ +int Ftp::ftpOpenEPRTDataConnection() +{ + assert(m_control != NULL); // must have control connection socket + assert(m_data == NULL); // ... but no data connection + + // yes, we are sure this is a KInetSocketAddress + const KInetSocketAddress *sin = static_cast<const KInetSocketAddress*>(m_control->localAddress()); + m_bPasv = false; + if (m_extControl & eprtUnknown || sin == NULL) + return ERR_INTERNAL; + + m_data = new FtpSocket("EPRT"); + m_data->setHost(sin->nodeName()); + m_data->setPort(0); // setting port to 0 will make us bind to a random, free port + m_data->setSocketFlags(KExtendedSocket::noResolve | KExtendedSocket::passiveSocket | + KExtendedSocket::inetSocket); + + if (m_data->listen(1) < 0) + return ERR_COULD_NOT_LISTEN; + + sin = static_cast<const KInetSocketAddress*>(m_data->localAddress()); + if (sin == NULL) + return ERR_INTERNAL; + + // QString command = QString::fromLatin1("eprt |%1|%2|%3|").arg(sin->ianaFamily()) + // .arg(sin->nodeName()) + // .arg(sin->port()); + QCString command; + command.sprintf("eprt |%d|%s|%d|", sin->ianaFamily(), + sin->nodeName().latin1(), sin->port()); + + // FIXME! Encoding for hostnames? + if( ftpSendCmd(command) && (m_iRespType == 2) ) + return 0; + + // unknown command? + if( m_iRespType == 5 ) + { + kdDebug(7102) << "disabling use of EPRT" << endl; + m_extControl |= eprtUnknown; + } + return ERR_INTERNAL; +} + +/* + * ftpOpenDataConnection - set up data connection + * + * The routine calls several ftpOpenXxxxConnection() helpers to find + * the best connection mode. If a helper cannot connect if returns + * ERR_INTERNAL - so this is not really an error! All other error + * codes are treated as fatal, e.g. they are passed back to the caller + * who is responsible for calling error(). ftpOpenPortDataConnection + * can be called as last try and it does never return ERR_INTERNAL. + * + * @return 0 if successful, err code otherwise + */ +int Ftp::ftpOpenDataConnection() +{ + // make sure that we are logged on and have no data connection... + assert( m_bLoggedOn ); + ftpCloseDataConnection(); + + int iErrCode = 0; + int iErrCodePASV = 0; // Remember error code from PASV + + // First try passive (EPSV & PASV) modes + if( !config()->readBoolEntry("DisablePassiveMode", false) ) + { + iErrCode = ftpOpenPASVDataConnection(); + if(iErrCode == 0) + return 0; // success + iErrCodePASV = iErrCode; + ftpCloseDataConnection(); + + if( !config()->readBoolEntry("DisableEPSV", false) ) + { + iErrCode = ftpOpenEPSVDataConnection(); + if(iErrCode == 0) + return 0; // success + ftpCloseDataConnection(); + } + + // if we sent EPSV ALL already and it was accepted, then we can't + // use active connections any more + if (m_extControl & epsvAllSent) + return iErrCodePASV ? iErrCodePASV : iErrCode; + } + + if( !config()->readBoolEntry("DisableEPRT", false) ) + { + iErrCode = ftpOpenEPRTDataConnection(); + if(iErrCode == 0) + return 0; // success + ftpCloseDataConnection(); + } + + // fall back to port mode + iErrCode = ftpOpenPortDataConnection(); + if(iErrCode == 0) + return 0; // success + + ftpCloseDataConnection(); + // prefer to return the error code from PASV if any, since that's what should have worked in the first place + return iErrCodePASV ? iErrCodePASV : iErrCode; +} + +/* + * ftpOpenPortDataConnection - set up data connection + * + * @return 0 if successfull, err code otherwise (but never ERR_INTERNAL + * because this is the last connection mode that is tried) + */ +int Ftp::ftpOpenPortDataConnection() +{ + assert(m_control != NULL); // must have control connection socket + assert(m_data == NULL); // ... but no data connection + + m_bPasv = false; + + // create a socket, bind it and let it listen ... + m_data = new FtpSocket("PORT"); + m_data->setSocketFlags(KExtendedSocket::noResolve | KExtendedSocket::passiveSocket | + KExtendedSocket::inetSocket); + + // yes, we are sure this is a KInetSocketAddress + const KInetSocketAddress* pAddr = static_cast<const KInetSocketAddress*>(m_control->localAddress()); + m_data->setAddress(pAddr->nodeName(), "0"); + m_data->setAddressReusable(true); + + if(m_data->listen(1) < 0) + return ERR_COULD_NOT_LISTEN; + struct linger lng = { 0, 0 }; + if ( !m_data->setSocketOption(SO_LINGER, (char*)&lng, sizeof(lng)) ) + return ERR_COULD_NOT_CREATE_SOCKET; + + // send the PORT command ... + pAddr = static_cast<const KInetSocketAddress*>(m_data->localAddress()); + struct sockaddr* psa = (struct sockaddr*)pAddr->addressV4(); + unsigned char* pData = (unsigned char*)(psa->sa_data); + QCString portCmd; + portCmd.sprintf("port %d,%d,%d,%d,%d,%d", + pData[2], pData[3], pData[4], pData[5], pData[0], pData[1]); + if( ftpSendCmd(portCmd) && (m_iRespType == 2) ) + return 0; + return ERR_COULD_NOT_CONNECT; +} + +/* + * ftpAcceptConnect - wait for incoming connection + * Used by @ref ftpOpenCommand + * + * return false on error or timeout + */ +int Ftp::ftpAcceptConnect() +{ + assert(m_data != NULL); + + if ( m_bPasv ) + { + m_data->setServer(-1); + return true; + } + + int sSock = m_data->fd(); + struct sockaddr addr; + for(;;) + { + fd_set mask; + FD_ZERO(&mask); + FD_SET(sSock,&mask); + int r = KSocks::self()->select(sSock + 1, &mask, NULL, NULL, 0L); + if( r < 0 && errno != EINTR && errno != EAGAIN ) + continue; + if( r > 0 ) + break; + } + + ksocklen_t l = sizeof(addr); + m_data->setServer( KSocks::self()->accept(sSock, &addr, &l) ); + return (m_data->server() != -1); +} + +bool Ftp::ftpOpenCommand( const char *_command, const QString & _path, char _mode, + int errorcode, KIO::fileoffset_t _offset ) +{ + int errCode = 0; + if( !ftpDataMode(_mode) ) + errCode = ERR_COULD_NOT_CONNECT; + else + errCode = ftpOpenDataConnection(); + + if(errCode != 0) + { + error(errCode, m_host); + return false; + } + + if ( _offset > 0 ) { + // send rest command if offset > 0, this applies to retr and stor commands + char buf[100]; + sprintf(buf, "rest %lld", _offset); + if ( !ftpSendCmd( buf ) ) + return false; + if( m_iRespType != 3 ) + { + error( ERR_CANNOT_RESUME, _path ); // should never happen + return false; + } + } + + QCString tmp = _command; + QString errormessage; + + if ( !_path.isEmpty() ) { + tmp += " "; + tmp += remoteEncoding()->encode(_path); + } + + if( !ftpSendCmd( tmp ) || (m_iRespType != 1) ) + { + if( _offset > 0 && strcmp(_command, "retr") == 0 && (m_iRespType == 4) ) + errorcode = ERR_CANNOT_RESUME; + // The error here depends on the command + errormessage = _path; + } + + else + { + // Only now we know for sure that we can resume + if ( _offset > 0 && strcmp(_command, "retr") == 0 ) + canResume(); + + if( ftpAcceptConnect() ) + { m_bBusy = true; // cleared in ftpCloseCommand + return true; + } + errorcode = ERR_COULD_NOT_ACCEPT; + } + + error(errorcode, errormessage); + return false; +} + + +bool Ftp::ftpCloseCommand() +{ + // first close data sockets (if opened), then read response that + // we got for whatever was used in ftpOpenCommand ( should be 226 ) + if(m_data) + { + delete m_data; + m_data = NULL; + } + if(!m_bBusy) + return true; + + kdDebug(7102) << "ftpCloseCommand: reading command result" << endl; + m_bBusy = false; + + if(!ftpResponse(-1) || (m_iRespType != 2) ) + { + kdDebug(7102) << "ftpCloseCommand: no transfer complete message" << endl; + return false; + } + return true; +} + +void Ftp::mkdir( const KURL & url, int permissions ) +{ + if( !ftpOpenConnection(loginImplicit) ) + return; + + QString path = remoteEncoding()->encode(url); + QCString buf = "mkd "; + buf += remoteEncoding()->encode(path); + + if( !ftpSendCmd( buf ) || (m_iRespType != 2) ) + { + QString currentPath( m_currentPath ); + + // Check whether or not mkdir failed because + // the directory already exists... + if( ftpFolder( path, false ) ) + { + error( ERR_DIR_ALREADY_EXIST, path ); + // Change the directory back to what it was... + (void) ftpFolder( currentPath, false ); + return; + } + + error( ERR_COULD_NOT_MKDIR, path ); + return; + } + + if ( permissions != -1 ) + { + // chmod the dir we just created, ignoring errors. + (void) ftpChmod( path, permissions ); + } + + finished(); +} + +void Ftp::rename( const KURL& src, const KURL& dst, bool overwrite ) +{ + if( !ftpOpenConnection(loginImplicit) ) + return; + + // The actual functionality is in ftpRename because put needs it + if ( ftpRename( src.path(), dst.path(), overwrite ) ) + finished(); + else + error( ERR_CANNOT_RENAME, src.path() ); +} + +bool Ftp::ftpRename( const QString & src, const QString & dst, bool overwrite ) +{ + assert( m_bLoggedOn ); + + // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793). + if (!overwrite) { + if (ftpSize(dst, 'I')) { + error(ERR_FILE_ALREADY_EXIST, dst); + return false; + } + } + if (ftpFolder(dst, false)) { + error(ERR_DIR_ALREADY_EXIST, dst); + return false; + } + + int pos = src.findRev("/"); + if( !ftpFolder(src.left(pos+1), false) ) + return false; + + QCString from_cmd = "RNFR "; + from_cmd += remoteEncoding()->encode(src.mid(pos+1)); + if( !ftpSendCmd( from_cmd ) || (m_iRespType != 3) ) + return false; + + QCString to_cmd = "RNTO "; + to_cmd += remoteEncoding()->encode(dst); + if( !ftpSendCmd( to_cmd ) || (m_iRespType != 2) ) + return false; + + return true; +} + +void Ftp::del( const KURL& url, bool isfile ) +{ + if( !ftpOpenConnection(loginImplicit) ) + return; + + // When deleting a directory, we must exit from it first + // The last command probably went into it (to stat it) + if ( !isfile ) + ftpFolder(remoteEncoding()->directory(url), false); // ignore errors + + QCString cmd = isfile ? "DELE " : "RMD "; + cmd += remoteEncoding()->encode(url); + + if( !ftpSendCmd( cmd ) || (m_iRespType != 2) ) + error( ERR_CANNOT_DELETE, url.path() ); + else + finished(); +} + +bool Ftp::ftpChmod( const QString & path, int permissions ) +{ + assert( m_bLoggedOn ); + + if(m_extControl & chmodUnknown) // previous errors? + return false; + + // we need to do bit AND 777 to get permissions, in case + // we were sent a full mode (unlikely) + QCString cmd; + cmd.sprintf("SITE CHMOD %o ", permissions & 511 ); + cmd += remoteEncoding()->encode(path); + + ftpSendCmd(cmd); + if(m_iRespType == 2) + return true; + + if(m_iRespCode == 500) + { + m_extControl |= chmodUnknown; + kdDebug(7102) << "ftpChmod: CHMOD not supported - disabling"; + } + return false; +} + +void Ftp::chmod( const KURL & url, int permissions ) +{ + if( !ftpOpenConnection(loginImplicit) ) + return; + + if ( !ftpChmod( url.path(), permissions ) ) + error( ERR_CANNOT_CHMOD, url.path() ); + else + finished(); +} + +void Ftp::ftpCreateUDSEntry( const QString & filename, FtpEntry& ftpEnt, UDSEntry& entry, bool isDir ) +{ + assert(entry.count() == 0); // by contract :-) + UDSAtom atom; + atom.m_uds = UDS_NAME; + atom.m_str = filename; + entry.append( atom ); + + atom.m_uds = UDS_SIZE; + atom.m_long = ftpEnt.size; + entry.append( atom ); + + atom.m_uds = UDS_MODIFICATION_TIME; + atom.m_long = ftpEnt.date; + entry.append( atom ); + + atom.m_uds = UDS_ACCESS; + atom.m_long = ftpEnt.access; + entry.append( atom ); + + atom.m_uds = UDS_USER; + atom.m_str = ftpEnt.owner; + entry.append( atom ); + + if ( !ftpEnt.group.isEmpty() ) + { + atom.m_uds = UDS_GROUP; + atom.m_str = ftpEnt.group; + entry.append( atom ); + } + + if ( !ftpEnt.link.isEmpty() ) + { + atom.m_uds = UDS_LINK_DEST; + atom.m_str = ftpEnt.link; + entry.append( atom ); + + KMimeType::Ptr mime = KMimeType::findByURL( KURL("ftp://host/" + filename ) ); + // Links on ftp sites are often links to dirs, and we have no way to check + // that. Let's do like Netscape : assume dirs generally. + // But we do this only when the mimetype can't be known from the filename. + // --> we do better than Netscape :-) + if ( mime->name() == KMimeType::defaultMimeType() ) + { + kdDebug(7102) << "Setting guessed mime type to inode/directory for " << filename << endl; + atom.m_uds = UDS_GUESSED_MIME_TYPE; + atom.m_str = "inode/directory"; + entry.append( atom ); + isDir = true; + } + } + + atom.m_uds = UDS_FILE_TYPE; + atom.m_long = isDir ? S_IFDIR : ftpEnt.type; + entry.append( atom ); + + /* atom.m_uds = UDS_ACCESS_TIME; + atom.m_long = buff.st_atime; + entry.append( atom ); + + atom.m_uds = UDS_CREATION_TIME; + atom.m_long = buff.st_ctime; + entry.append( atom ); */ +} + + +void Ftp::ftpShortStatAnswer( const QString& filename, bool isDir ) +{ + UDSEntry entry; + UDSAtom atom; + + atom.m_uds = KIO::UDS_NAME; + atom.m_str = filename; + entry.append( atom ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = isDir ? S_IFDIR : S_IFREG; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + entry.append( atom ); + + // No details about size, ownership, group, etc. + + statEntry(entry); + finished(); +} + +void Ftp::ftpStatAnswerNotFound( const QString & path, const QString & filename ) +{ + // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source") + // When e.g. uploading a file, we still need stat() to return "not found" + // when the file doesn't exist. + QString statSide = metaData("statSide"); + kdDebug(7102) << "Ftp::stat statSide=" << statSide << endl; + if ( statSide == "source" ) + { + kdDebug(7102) << "Not found, but assuming found, because some servers don't allow listing" << endl; + // MS Server is incapable of handling "list <blah>" in a case insensitive way + // But "retr <blah>" works. So lie in stat(), to get going... + // + // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run + // where listing permissions are denied, but downloading is still possible. + ftpShortStatAnswer( filename, false /*file, not dir*/ ); + + return; + } + + error( ERR_DOES_NOT_EXIST, path ); +} + +void Ftp::stat( const KURL &url) +{ + kdDebug(7102) << "Ftp::stat : path='" << url.path() << "'" << endl; + if( !ftpOpenConnection(loginImplicit) ) + return; + + QString path = QDir::cleanDirPath( url.path() ); + kdDebug(7102) << "Ftp::stat : cleaned path='" << path << "'" << endl; + + // We can't stat root, but we know it's a dir. + if( path.isEmpty() || path == "/" ) + { + UDSEntry entry; + UDSAtom atom; + + atom.m_uds = KIO::UDS_NAME; + atom.m_str = QString::null; + entry.append( atom ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = S_IFDIR; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + entry.append( atom ); + + atom.m_uds = KIO::UDS_USER; + atom.m_str = "root"; + entry.append( atom ); + atom.m_uds = KIO::UDS_GROUP; + entry.append( atom ); + + // no size + + statEntry( entry ); + finished(); + return; + } + + KURL tempurl( url ); + tempurl.setPath( path ); // take the clean one + QString listarg; // = tempurl.directory(false /*keep trailing slash*/); + QString parentDir; + QString filename = tempurl.fileName(); + Q_ASSERT(!filename.isEmpty()); + QString search = filename; + + // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info) + // if it doesn't work, it's a file (and then we'll use dir filename) + bool isDir = ftpFolder(path, false); + + // if we're only interested in "file or directory", we should stop here + QString sDetails = metaData("details"); + int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); + kdDebug(7102) << "Ftp::stat details=" << details << endl; + if ( details == 0 ) + { + if ( !isDir && !ftpSize( path, 'I' ) ) // ok, not a dir -> is it a file ? + { // no -> it doesn't exist at all + ftpStatAnswerNotFound( path, filename ); + return; + } + ftpShortStatAnswer( filename, isDir ); // successfully found a dir or a file -> done + return; + } + + if (!isDir) + { + // It is a file or it doesn't exist, try going to parent directory + parentDir = tempurl.directory(false /*keep trailing slash*/); + // With files we can do "LIST <filename>" to avoid listing the whole dir + listarg = filename; + } + else + { + // --- New implementation: + // Don't list the parent dir. Too slow, might not show it, etc. + // Just return that it's a dir. + UDSEntry entry; + UDSAtom atom; + + atom.m_uds = KIO::UDS_NAME; + atom.m_str = filename; + entry.append( atom ); + + atom.m_uds = KIO::UDS_FILE_TYPE; + atom.m_long = S_IFDIR; + entry.append( atom ); + + atom.m_uds = KIO::UDS_ACCESS; + atom.m_long = S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + entry.append( atom ); + + // No clue about size, ownership, group, etc. + + statEntry(entry); + finished(); + return; + + // --- Old implementation: +#if 0 + // It's a dir, remember that + // Reason: it could be a symlink to a dir, in which case ftpReadDir + // in the parent dir will have no idea about that. But we know better. + isDir = true; + // If the dir starts with '.', we'll need '-a' to see it in the listing. + if ( search[0] == '.' ) + listarg = "-a"; + parentDir = ".."; +#endif + } + + // Now cwd the parent dir, to prepare for listing + if( !ftpFolder(parentDir, true) ) + return; + + if( !ftpOpenCommand( "list", listarg, 'I', ERR_DOES_NOT_EXIST ) ) + { + kdError(7102) << "COULD NOT LIST" << endl; + return; + } + kdDebug(7102) << "Starting of list was ok" << endl; + + Q_ASSERT( !search.isEmpty() && search != "/" ); + + bool bFound = false; + KURL linkURL; + FtpEntry ftpEnt; + while( ftpReadDir(ftpEnt) ) + { + // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at) + // return only the filename when doing "dir /full/path/to/file" + if ( !bFound ) + { + if ( ( search == ftpEnt.name || filename == ftpEnt.name ) ) { + if ( !filename.isEmpty() ) { + bFound = true; + UDSEntry entry; + ftpCreateUDSEntry( filename, ftpEnt, entry, isDir ); + statEntry( entry ); + } + } else if ( isDir && ( ftpEnt.name == listarg || ftpEnt.name+'/' == listarg ) ) { + // Damn, the dir we're trying to list is in fact a symlink + // Follow it and try again + if ( ftpEnt.link.isEmpty() ) + kdWarning(7102) << "Got " << listarg << " as answer, but empty link!" << endl; + else + { + linkURL = url; + kdDebug(7102) << "ftpEnt.link=" << ftpEnt.link << endl; + if ( ftpEnt.link[0] == '/' ) + linkURL.setPath( ftpEnt.link ); // Absolute link + else + { + // Relative link (stat will take care of cleaning ../.. etc.) + linkURL.setPath( listarg ); // this is what we were listing (the link) + linkURL.setPath( linkURL.directory() ); // go up one dir + linkURL.addPath( ftpEnt.link ); // replace link by its destination + kdDebug(7102) << "linkURL now " << linkURL.prettyURL() << endl; + } + // Re-add the filename we're looking for + linkURL.addPath( filename ); + } + bFound = true; + } + } + + // kdDebug(7102) << ftpEnt.name << endl; + } + + ftpCloseCommand(); // closes the data connection only + + if ( !bFound ) + { + ftpStatAnswerNotFound( path, filename ); + return; + } + + if ( !linkURL.isEmpty() ) + { + if ( linkURL == url || linkURL == tempurl ) + { + error( ERR_CYCLIC_LINK, linkURL.prettyURL() ); + return; + } + stat( linkURL ); + return; + } + + kdDebug(7102) << "stat : finished successfully" << endl; + finished(); +} + + +void Ftp::listDir( const KURL &url ) +{ + kdDebug(7102) << "Ftp::listDir " << url.prettyURL() << endl; + if( !ftpOpenConnection(loginImplicit) ) + return; + + // No path specified ? + QString path = url.path(); + if ( path.isEmpty() ) + { + KURL realURL; + realURL.setProtocol( "ftp" ); + if ( m_user != FTP_LOGIN ) + realURL.setUser( m_user ); + // We set the password, so that we don't ask for it if it was given + if ( m_pass != FTP_PASSWD ) + realURL.setPass( m_pass ); + realURL.setHost( m_host ); + realURL.setPort( m_port ); + if ( m_initialPath.isEmpty() ) + m_initialPath = "/"; + realURL.setPath( m_initialPath ); + kdDebug(7102) << "REDIRECTION to " << realURL.prettyURL() << endl; + redirection( realURL ); + finished(); + return; + } + + kdDebug(7102) << "hunting for path '" << path << "'" << endl; + + if (!ftpOpenDir( path ) ) + { + if ( ftpSize( path, 'I' ) ) // is it a file ? + { + error( ERR_IS_FILE, path ); + return; + } + // not sure which to emit + //error( ERR_DOES_NOT_EXIST, path ); + error( ERR_CANNOT_ENTER_DIRECTORY, path ); + return; + } + + UDSEntry entry; + FtpEntry ftpEnt; + while( ftpReadDir(ftpEnt) ) + { + //kdDebug(7102) << ftpEnt.name << endl; + //Q_ASSERT( !ftpEnt.name.isEmpty() ); + if ( !ftpEnt.name.isEmpty() ) + { + //if ( S_ISDIR( (mode_t)ftpEnt.type ) ) + // kdDebug(7102) << "is a dir" << endl; + //if ( !ftpEnt.link.isEmpty() ) + // kdDebug(7102) << "is a link to " << ftpEnt.link << endl; + entry.clear(); + ftpCreateUDSEntry( ftpEnt.name, ftpEnt, entry, false ); + listEntry( entry, false ); + } + } + listEntry( entry, true ); // ready + ftpCloseCommand(); // closes the data connection only + finished(); +} + +void Ftp::slave_status() +{ + kdDebug(7102) << "Got slave_status host = " << (m_host.ascii() ? m_host.ascii() : "[None]") << " [" << (m_bLoggedOn ? "Connected" : "Not connected") << "]" << endl; + slaveStatus( m_host, m_bLoggedOn ); +} + +bool Ftp::ftpOpenDir( const QString & path ) +{ + //QString path( _url.path(-1) ); + + // We try to change to this directory first to see whether it really is a directory. + // (And also to follow symlinks) + QString tmp = path.isEmpty() ? QString("/") : path; + + // We get '550', whether it's a file or doesn't exist... + if( !ftpFolder(tmp, false) ) + return false; + + // Don't use the path in the list command: + // We changed into this directory anyway - so it's enough just to send "list". + // We use '-a' because the application MAY be interested in dot files. + // The only way to really know would be to have a metadata flag for this... + // Since some windows ftp server seems not to support the -a argument, we use a fallback here. + // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com) + if( !ftpOpenCommand( "list -la", QString::null, 'I', ERR_CANNOT_ENTER_DIRECTORY ) ) + { + if ( !ftpOpenCommand( "list", QString::null, 'I', ERR_CANNOT_ENTER_DIRECTORY ) ) + { + kdWarning(7102) << "Can't open for listing" << endl; + return false; + } + } + kdDebug(7102) << "Starting of list was ok" << endl; + return true; +} + +bool Ftp::ftpReadDir(FtpEntry& de) +{ + assert(m_data != NULL); + + // get a line from the data connecetion ... + while( !m_data->textEOF() ) + { + if(m_data->textRead() <= 0) + continue; + if(m_data->textTooLong()) + kdWarning(7102) << "ftpReadDir line too long - truncated" << endl; + + const char* buffer = m_data->textLine(); + kdDebug(7102) << "dir > " << buffer << endl; + + //Normally the listing looks like + // -rw-r--r-- 1 dfaure dfaure 102 Nov 9 12:30 log + // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442) + // d [RWCEAFMS] Admin 512 Oct 13 2004 PSI + + // we should always get the following 5 fields ... + const char *p_access, *p_junk, *p_owner, *p_group, *p_size; + if( (p_access = strtok((char*)buffer," ")) == 0) continue; + if( (p_junk = strtok(NULL," ")) == 0) continue; + if( (p_owner = strtok(NULL," ")) == 0) continue; + if( (p_group = strtok(NULL," ")) == 0) continue; + if( (p_size = strtok(NULL," ")) == 0) continue; + + //kdDebug(7102) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size << endl; + + de.access = 0; + if ( strlen( p_access ) == 1 && p_junk[0] == '[' ) { // Netware + de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions + } + + const char *p_date_1, *p_date_2, *p_date_3, *p_name; + + // A special hack for "/dev". A listing may look like this: + // crw-rw-rw- 1 root root 1, 5 Jun 29 1997 zero + // So we just ignore the number in front of the ",". Ok, its a hack :-) + if ( strchr( p_size, ',' ) != 0L ) + { + //kdDebug(7102) << "Size contains a ',' -> reading size again (/dev hack)" << endl; + if ((p_size = strtok(NULL," ")) == 0) + continue; + } + + // Check whether the size we just read was really the size + // or a month (this happens when the server lists no group) + // Used to be the case on sunsite.uio.no, but not anymore + // This is needed for the Netware case, too. + if ( !isdigit( *p_size ) ) + { + p_date_1 = p_size; + p_size = p_group; + p_group = 0; + //kdDebug(7102) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1 << endl; + } + else + { + p_date_1 = strtok(NULL," "); + //kdDebug(7102) << "Size has a digit -> ok. p_date_1=" << p_date_1 << endl; + } + + if ( p_date_1 != 0 && + (p_date_2 = strtok(NULL," ")) != 0 && + (p_date_3 = strtok(NULL," ")) != 0 && + (p_name = strtok(NULL,"\r\n")) != 0 ) + { + { + QCString tmp( p_name ); + if ( p_access[0] == 'l' ) + { + int i = tmp.findRev( " -> " ); + if ( i != -1 ) { + de.link = remoteEncoding()->decode(p_name + i + 4); + tmp.truncate( i ); + } + else + de.link = QString::null; + } + else + de.link = QString::null; + + if ( tmp[0] == '/' ) // listing on ftp://ftp.gnupg.org/ starts with '/' + tmp.remove( 0, 1 ); + + if (tmp.find('/') != -1) + continue; // Don't trick us! + // Some sites put more than one space between the date and the name + // e.g. ftp://ftp.uni-marburg.de/mirror/ + de.name = remoteEncoding()->decode(tmp.stripWhiteSpace()); + } + + de.type = S_IFREG; + switch ( p_access[0] ) { + case 'd': + de.type = S_IFDIR; + break; + case 's': + de.type = S_IFSOCK; + break; + case 'b': + de.type = S_IFBLK; + break; + case 'c': + de.type = S_IFCHR; + break; + case 'l': + de.type = S_IFREG; + // we don't set S_IFLNK here. de.link says it. + break; + default: + break; + } + + if ( p_access[1] == 'r' ) + de.access |= S_IRUSR; + if ( p_access[2] == 'w' ) + de.access |= S_IWUSR; + if ( p_access[3] == 'x' || p_access[3] == 's' ) + de.access |= S_IXUSR; + if ( p_access[4] == 'r' ) + de.access |= S_IRGRP; + if ( p_access[5] == 'w' ) + de.access |= S_IWGRP; + if ( p_access[6] == 'x' || p_access[6] == 's' ) + de.access |= S_IXGRP; + if ( p_access[7] == 'r' ) + de.access |= S_IROTH; + if ( p_access[8] == 'w' ) + de.access |= S_IWOTH; + if ( p_access[9] == 'x' || p_access[9] == 't' ) + de.access |= S_IXOTH; + if ( p_access[3] == 's' || p_access[3] == 'S' ) + de.access |= S_ISUID; + if ( p_access[6] == 's' || p_access[6] == 'S' ) + de.access |= S_ISGID; + if ( p_access[9] == 't' || p_access[9] == 'T' ) + de.access |= S_ISVTX; + + de.owner = remoteEncoding()->decode(p_owner); + de.group = remoteEncoding()->decode(p_group); + de.size = charToLongLong(p_size); + + // Parsing the date is somewhat tricky + // Examples : "Oct 6 22:49", "May 13 1999" + + // First get current time - we need the current month and year + time_t currentTime = time( 0L ); + struct tm * tmptr = gmtime( ¤tTime ); + int currentMonth = tmptr->tm_mon; + //kdDebug(7102) << "Current time :" << asctime( tmptr ) << endl; + // Reset time fields + tmptr->tm_isdst = -1; // We do not know anything about day saving time (of any random day of the year) + tmptr->tm_sec = 0; + tmptr->tm_min = 0; + tmptr->tm_hour = 0; + // Get day number (always second field) + tmptr->tm_mday = atoi( p_date_2 ); + // Get month from first field + // NOTE : no, we don't want to use KLocale here + // It seems all FTP servers use the English way + //kdDebug(7102) << "Looking for month " << p_date_1 << endl; + static const char * s_months[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + for ( int c = 0 ; c < 12 ; c ++ ) + if ( !strcmp( p_date_1, s_months[c]) ) + { + //kdDebug(7102) << "Found month " << c << " for " << p_date_1 << endl; + tmptr->tm_mon = c; + break; + } + + // Parse third field + if ( strlen( p_date_3 ) == 4 ) // 4 digits, looks like a year + tmptr->tm_year = atoi( p_date_3 ) - 1900; + else + { + // otherwise, the year is implicit + // according to man ls, this happens when it is between than 6 months + // old and 1 hour in the future. + // So the year is : current year if tm_mon <= currentMonth+1 + // otherwise current year minus one + // (The +1 is a security for the "+1 hour" at the end of the month issue) + if ( tmptr->tm_mon > currentMonth + 1 ) + tmptr->tm_year--; + + // and p_date_3 contains probably a time + char * semicolon; + if ( ( semicolon = (char*)strchr( p_date_3, ':' ) ) ) + { + *semicolon = '\0'; + tmptr->tm_min = atoi( semicolon + 1 ); + tmptr->tm_hour = atoi( p_date_3 ); + } + else + kdWarning(7102) << "Can't parse third field " << p_date_3 << endl; + } + + //kdDebug(7102) << asctime( tmptr ) << endl; + de.date = mktime( tmptr ); + return true; + } + } // line invalid, loop to get another line + return false; +} + +//=============================================================================== +// public: get download file from server +// helper: ftpGet called from get() and copy() +//=============================================================================== +void Ftp::get( const KURL & url ) +{ + kdDebug(7102) << "Ftp::get " << url.url() << endl; + int iError = 0; + ftpGet(iError, -1, url, 0); // iError gets status + if(iError) // can have only server side errs + error(iError, url.path()); + ftpCloseCommand(); // must close command! +} + +Ftp::StatusCode Ftp::ftpGet(int& iError, int iCopyFile, const KURL& url, KIO::fileoffset_t llOffset) +{ + // Calls error() by itself! + if( !ftpOpenConnection(loginImplicit) ) + return statusServerError; + + // Try to find the size of the file (and check that it exists at + // the same time). If we get back a 550, "File does not exist" + // or "not a plain file", check if it is a directory. If it is a + // directory, return an error; otherwise simply try to retrieve + // the request... + if ( !ftpSize( url.path(), '?' ) && (m_iRespCode == 550) && + ftpFolder(url.path(), false) ) + { + // Ok it's a dir in fact + kdDebug(7102) << "ftpGet: it is a directory in fact" << endl; + iError = ERR_IS_DIRECTORY; + return statusServerError; + } + + QString resumeOffset = metaData("resume"); + if ( !resumeOffset.isEmpty() ) + { + llOffset = resumeOffset.toLongLong(); + kdDebug(7102) << "ftpGet: got offset from metadata : " << llOffset << endl; + } + + if( !ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset) ) + { + kdWarning(7102) << "ftpGet: Can't open for reading" << endl; + return statusServerError; + } + + // Read the size from the response string + if(m_size == UnknownSize) + { + const char* psz = strrchr( ftpResponse(4), '(' ); + if(psz) m_size = charToLongLong(psz+1); + if (!m_size) m_size = UnknownSize; + } + + KIO::filesize_t bytesLeft = 0; + if ( m_size != UnknownSize ) + bytesLeft = m_size - llOffset; + + kdDebug(7102) << "ftpGet: starting with offset=" << llOffset << endl; + KIO::fileoffset_t processed_size = llOffset; + + QByteArray array; + bool mimetypeEmitted = false; + char buffer[maximumIpcSize]; + // start whith small data chunks in case of a slow data source (modem) + // - unfortunately this has a negative impact on performance for large + // - files - so we will increase the block size after a while ... + int iBlockSize = initialIpcSize; + int iBufferCur = 0; + + while(m_size == UnknownSize || bytesLeft > 0) + { // let the buffer size grow if the file is larger 64kByte ... + if(processed_size-llOffset > 1024 * 64) + iBlockSize = maximumIpcSize; + + // read the data and detect EOF or error ... + if(iBlockSize+iBufferCur > (int)sizeof(buffer)) + iBlockSize = sizeof(buffer) - iBufferCur; + int n = m_data->read( buffer+iBufferCur, iBlockSize ); + if(n <= 0) + { // this is how we detect EOF in case of unknown size + if( m_size == UnknownSize && n == 0 ) + break; + // unexpected eof. Happens when the daemon gets killed. + iError = ERR_COULD_NOT_READ; + return statusServerError; + } + processed_size += n; + + // collect very small data chunks in buffer before processing ... + if(m_size != UnknownSize) + { + bytesLeft -= n; + iBufferCur += n; + if(iBufferCur < mimimumMimeSize && bytesLeft > 0) + { + processedSize( processed_size ); + continue; + } + n = iBufferCur; + iBufferCur = 0; + } + + // get the mime type and set the total size ... + if(!mimetypeEmitted) + { + mimetypeEmitted = true; + + // We need a KMimeType::findByNameAndContent(data,filename) + // For now we do: find by extension, and if not found (or extension not reliable) + // then find by content. + bool accurate = false; + KMimeType::Ptr mime = KMimeType::findByURL( url, 0, false, true, &accurate ); + if ( !mime || mime->name() == KMimeType::defaultMimeType() + || !accurate ) + { + array.setRawData(buffer, n); + KMimeMagicResult * result = KMimeMagic::self()->findBufferFileType(array, url.fileName()); + array.resetRawData(buffer, n); + if ( result->mimeType() != KMimeType::defaultMimeType() ) + mime = KMimeType::mimeType( result->mimeType() ); + } + + kdDebug(7102) << "ftpGet: Emitting mimetype " << mime->name() << endl; + mimeType( mime->name() ); + if( m_size != UnknownSize ) // Emit total size AFTER mimetype + totalSize( m_size ); + } + + // write output file or pass to data pump ... + if(iCopyFile == -1) + { + array.setRawData(buffer, n); + data( array ); + array.resetRawData(buffer, n); + } + else if( (iError = WriteToFile(iCopyFile, buffer, n)) != 0) + return statusClientError; // client side error + processedSize( processed_size ); + } + + kdDebug(7102) << "ftpGet: done" << endl; + if(iCopyFile == -1) // must signal EOF to data pump ... + data(array); // array is empty and must be empty! + + processedSize( m_size == UnknownSize ? processed_size : m_size ); + kdDebug(7102) << "ftpGet: emitting finished()" << endl; + finished(); + return statusSuccess; +} + +/* +void Ftp::mimetype( const KURL& url ) +{ + if( !ftpOpenConnection(loginImplicit) ) + return; + + if ( !ftpOpenCommand( "retr", url.path(), 'I', ERR_CANNOT_OPEN_FOR_READING, 0 ) ) { + kdWarning(7102) << "Can't open for reading" << endl; + return; + } + char buffer[ 2048 ]; + QByteArray array; + // Get one chunk of data only and send it, KIO::Job will determine the + // mimetype from it using KMimeMagic + int n = m_data->read( buffer, 2048 ); + array.setRawData(buffer, n); + data( array ); + array.resetRawData(buffer, n); + + kdDebug(7102) << "aborting" << endl; + ftpAbortTransfer(); + + kdDebug(7102) << "finished" << endl; + finished(); + kdDebug(7102) << "after finished" << endl; +} + +void Ftp::ftpAbortTransfer() +{ + // RFC 959, page 34-35 + // IAC (interpret as command) = 255 ; IP (interrupt process) = 254 + // DM = 242 (data mark) + char msg[4]; + // 1. User system inserts the Telnet "Interrupt Process" (IP) signal + // in the Telnet stream. + msg[0] = (char) 255; //IAC + msg[1] = (char) 254; //IP + (void) send(sControl, msg, 2, 0); + // 2. User system sends the Telnet "Sync" signal. + msg[0] = (char) 255; //IAC + msg[1] = (char) 242; //DM + if (send(sControl, msg, 2, MSG_OOB) != 2) + ; // error... + + // Send ABOR + kdDebug(7102) << "send ABOR" << endl; + QCString buf = "ABOR\r\n"; + if ( KSocks::self()->write( sControl, buf.data(), buf.length() ) <= 0 ) { + error( ERR_COULD_NOT_WRITE, QString::null ); + return; + } + + // + kdDebug(7102) << "read resp" << endl; + if ( readresp() != '2' ) + { + error( ERR_COULD_NOT_READ, QString::null ); + return; + } + + kdDebug(7102) << "close sockets" << endl; + closeSockets(); +} +*/ + +//=============================================================================== +// public: put upload file to server +// helper: ftpPut called from put() and copy() +//=============================================================================== +void Ftp::put(const KURL& url, int permissions, bool overwrite, bool resume) +{ + kdDebug(7102) << "Ftp::put " << url.url() << endl; + int iError = 0; // iError gets status + ftpPut(iError, -1, url, permissions, overwrite, resume); + if(iError) // can have only server side errs + error(iError, url.path()); + ftpCloseCommand(); // must close command! +} + +Ftp::StatusCode Ftp::ftpPut(int& iError, int iCopyFile, const KURL& dest_url, + int permissions, bool overwrite, bool resume) +{ + if( !ftpOpenConnection(loginImplicit) ) + return statusServerError; + + // Don't use mark partial over anonymous FTP. + // My incoming dir allows put but not rename... + bool bMarkPartial; + if (m_user.isEmpty () || m_user == FTP_LOGIN) + bMarkPartial = false; + else + bMarkPartial = config()->readBoolEntry("MarkPartial", true); + + QString dest_orig = dest_url.path(); + QString dest_part( dest_orig ); + dest_part += ".part"; + + if ( ftpSize( dest_orig, 'I' ) ) + { + if ( m_size == 0 ) + { // delete files with zero size + QCString cmd = "DELE "; + cmd += remoteEncoding()->encode(dest_orig); + if( !ftpSendCmd( cmd ) || (m_iRespType != 2) ) + { + iError = ERR_CANNOT_DELETE_PARTIAL; + return statusServerError; + } + } + else if ( !overwrite && !resume ) + { + iError = ERR_FILE_ALREADY_EXIST; + return statusServerError; + } + else if ( bMarkPartial ) + { // when using mark partial, append .part extension + if ( !ftpRename( dest_orig, dest_part, true ) ) + { + iError = ERR_CANNOT_RENAME_PARTIAL; + return statusServerError; + } + } + // Don't chmod an existing file + permissions = -1; + } + else if ( bMarkPartial && ftpSize( dest_part, 'I' ) ) + { // file with extension .part exists + if ( m_size == 0 ) + { // delete files with zero size + QCString cmd = "DELE "; + cmd += remoteEncoding()->encode(dest_part); + if ( !ftpSendCmd( cmd ) || (m_iRespType != 2) ) + { + iError = ERR_CANNOT_DELETE_PARTIAL; + return statusServerError; + } + } + else if ( !overwrite && !resume ) + { + resume = canResume (m_size); + if (!resume) + { + iError = ERR_FILE_ALREADY_EXIST; + return statusServerError; + } + } + } + else + m_size = 0; + + QString dest; + + // if we are using marking of partial downloads -> add .part extension + if ( bMarkPartial ) { + kdDebug(7102) << "Adding .part extension to " << dest_orig << endl; + dest = dest_part; + } else + dest = dest_orig; + + KIO::fileoffset_t offset = 0; + + // set the mode according to offset + if( resume && m_size > 0 ) + { + offset = m_size; + if(iCopyFile != -1) + { + if( KDE_lseek(iCopyFile, offset, SEEK_SET) < 0 ) + { + iError = ERR_CANNOT_RESUME; + return statusClientError; + } + } + } + + if (! ftpOpenCommand( "stor", dest, '?', ERR_COULD_NOT_WRITE, offset ) ) + return statusServerError; + + kdDebug(7102) << "ftpPut: starting with offset=" << offset << endl; + KIO::fileoffset_t processed_size = offset; + + QByteArray buffer; + int result; + int iBlockSize = initialIpcSize; + // Loop until we got 'dataEnd' + do + { + if(iCopyFile == -1) + { + dataReq(); // Request for data + result = readData( buffer ); + } + else + { // let the buffer size grow if the file is larger 64kByte ... + if(processed_size-offset > 1024 * 64) + iBlockSize = maximumIpcSize; + buffer.resize(iBlockSize); + result = ::read(iCopyFile, buffer.data(), buffer.size()); + if(result < 0) + iError = ERR_COULD_NOT_WRITE; + else + buffer.resize(result); + } + + if (result > 0) + { + m_data->write( buffer.data(), buffer.size() ); + processed_size += result; + processedSize (processed_size); + } + } + while ( result > 0 ); + + if (result != 0) // error + { + ftpCloseCommand(); // don't care about errors + kdDebug(7102) << "Error during 'put'. Aborting." << endl; + if (bMarkPartial) + { + // Remove if smaller than minimum size + if ( ftpSize( dest, 'I' ) && + ( processed_size < (unsigned long) config()->readNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE) ) ) + { + QCString cmd = "DELE "; + cmd += remoteEncoding()->encode(dest); + (void) ftpSendCmd( cmd ); + } + } + return statusServerError; + } + + if ( !ftpCloseCommand() ) + { + iError = ERR_COULD_NOT_WRITE; + return statusServerError; + } + + // after full download rename the file back to original name + if ( bMarkPartial ) + { + kdDebug(7102) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")" << endl; + if ( !ftpRename( dest, dest_orig, true ) ) + { + iError = ERR_CANNOT_RENAME_PARTIAL; + return statusServerError; + } + } + + // set final permissions + if ( permissions != -1 ) + { + if ( m_user == FTP_LOGIN ) + kdDebug(7102) << "Trying to chmod over anonymous FTP ???" << endl; + // chmod the file we just put + if ( ! ftpChmod( dest_orig, permissions ) ) + { + // To be tested + //if ( m_user != FTP_LOGIN ) + // warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) ); + } + } + + // We have done our job => finish + finished(); + return statusSuccess; +} + + +/** Use the SIZE command to get the file size. + Warning : the size depends on the transfer mode, hence the second arg. */ +bool Ftp::ftpSize( const QString & path, char mode ) +{ + m_size = UnknownSize; + if( !ftpDataMode(mode) ) + return false; + + QCString buf; + buf = "SIZE "; + buf += remoteEncoding()->encode(path); + if( !ftpSendCmd( buf ) || (m_iRespType != 2) ) + return false; + + // skip leading "213 " (response code) + const char* psz = ftpResponse(4); + if(!psz) + return false; + m_size = charToLongLong(psz); + if (!m_size) m_size = UnknownSize; + return true; +} + +// Today the differences between ASCII and BINARY are limited to +// CR or CR/LF line terminators. Many servers ignore ASCII (like +// win2003 -or- vsftp with default config). In the early days of +// computing, when even text-files had structure, this stuff was +// more important. +// Theoretically "list" could return different results in ASCII +// and BINARY mode. But again, most servers ignore ASCII here. +bool Ftp::ftpDataMode(char cMode) +{ + if(cMode == '?') cMode = m_bTextMode ? 'A' : 'I'; + else if(cMode == 'a') cMode = 'A'; + else if(cMode != 'A') cMode = 'I'; + + kdDebug(7102) << "ftpDataMode: want '" << cMode << "' has '" << m_cDataMode << "'" << endl; + if(m_cDataMode == cMode) + return true; + + QCString buf; + buf.sprintf("TYPE %c", cMode); + if( !ftpSendCmd(buf) || (m_iRespType != 2) ) + return false; + m_cDataMode = cMode; + return true; +} + + +bool Ftp::ftpFolder(const QString& path, bool bReportError) +{ + QString newPath = path; + int iLen = newPath.length(); + if(iLen > 1 && newPath[iLen-1] == '/') newPath.truncate(iLen-1); + + //kdDebug(7102) << "ftpFolder: want '" << newPath << "' has '" << m_currentPath << "'" << endl; + if(m_currentPath == newPath) + return true; + + QCString tmp = "cwd "; + tmp += remoteEncoding()->encode(newPath); + if( !ftpSendCmd(tmp) ) + return false; // connection failure + if(m_iRespType != 2) + { + if(bReportError) + error(ERR_CANNOT_ENTER_DIRECTORY, path); + return false; // not a folder + } + m_currentPath = newPath; + return true; +} + + +//=============================================================================== +// public: copy don't use kio data pump if one side is a local file +// helper: ftpCopyPut called from copy() on upload +// helper: ftpCopyGet called from copy() on download +//=============================================================================== +void Ftp::copy( const KURL &src, const KURL &dest, int permissions, bool overwrite ) +{ + int iError = 0; + int iCopyFile = -1; + StatusCode cs = statusSuccess; + bool bSrcLocal = src.isLocalFile(); + bool bDestLocal = dest.isLocalFile(); + QString sCopyFile; + + if(bSrcLocal && !bDestLocal) // File -> Ftp + { + sCopyFile = src.path(); + kdDebug(7102) << "Ftp::copy local file '" << sCopyFile << "' -> ftp '" << dest.path() << "'" << endl; + cs = ftpCopyPut(iError, iCopyFile, sCopyFile, dest, permissions, overwrite); + if( cs == statusServerError) sCopyFile = dest.url(); + } + else if(!bSrcLocal && bDestLocal) // Ftp -> File + { + sCopyFile = dest.path(); + kdDebug(7102) << "Ftp::copy ftp '" << src.path() << "' -> local file '" << sCopyFile << "'" << endl; + cs = ftpCopyGet(iError, iCopyFile, sCopyFile, src, permissions, overwrite); + if( cs == statusServerError ) sCopyFile = src.url(); + } + else { + error( ERR_UNSUPPORTED_ACTION, QString::null ); + return; + } + + // perform clean-ups and report error (if any) + if(iCopyFile != -1) + ::close(iCopyFile); + if(iError) + error(iError, sCopyFile); + ftpCloseCommand(); // must close command! +} + + +Ftp::StatusCode Ftp::ftpCopyPut(int& iError, int& iCopyFile, QString sCopyFile, + const KURL& url, int permissions, bool overwrite) +{ + // check if source is ok ... + KDE_struct_stat buff; + QCString sSrc( QFile::encodeName(sCopyFile) ); + bool bSrcExists = (KDE_stat( sSrc.data(), &buff ) != -1); + if(bSrcExists) + { if(S_ISDIR(buff.st_mode)) + { + iError = ERR_IS_DIRECTORY; + return statusClientError; + } + } + else + { + iError = ERR_DOES_NOT_EXIST; + return statusClientError; + } + + iCopyFile = KDE_open( sSrc.data(), O_RDONLY ); + if(iCopyFile == -1) + { + iError = ERR_CANNOT_OPEN_FOR_READING; + return statusClientError; + } + + // delegate the real work (iError gets status) ... + totalSize(buff.st_size); +#ifdef ENABLE_CAN_RESUME + return ftpPut(iError, iCopyFile, url, permissions, overwrite, false); +#else + return ftpPut(iError, iCopyFile, url, permissions, overwrite, true); +#endif +} + + +Ftp::StatusCode Ftp::ftpCopyGet(int& iError, int& iCopyFile, const QString sCopyFile, + const KURL& url, int permissions, bool overwrite) +{ + // check if destination is ok ... + KDE_struct_stat buff; + QCString sDest( QFile::encodeName(sCopyFile) ); + bool bDestExists = (KDE_stat( sDest.data(), &buff ) != -1); + if(bDestExists) + { if(S_ISDIR(buff.st_mode)) + { + iError = ERR_IS_DIRECTORY; + return statusClientError; + } + if(!overwrite) + { + iError = ERR_FILE_ALREADY_EXIST; + return statusClientError; + } + } + + // do we have a ".part" file? + QCString sPart = QFile::encodeName(sCopyFile + ".part"); + bool bResume = false; + bool bPartExists = (KDE_stat( sPart.data(), &buff ) != -1); + const bool bMarkPartial = config()->readBoolEntry("MarkPartial", true); + + if(!bMarkPartial) + { + sPart = QFile::encodeName(sCopyFile); + } + else if(bPartExists && buff.st_size > 0) + { // must not be a folder! please fix a similar bug in kio_file!! + if(S_ISDIR(buff.st_mode)) + { + iError = ERR_DIR_ALREADY_EXIST; + return statusClientError; // client side error + } + //doesn't work for copy? -> design flaw? +#ifdef ENABLE_CAN_RESUME + bResume = canResume( buff.st_size ); +#else + bResume = true; +#endif + } + + if(bPartExists && !bResume) // get rid of an unwanted ".part" file + remove(sPart.data()); + + // JPF: in kio_file overwrite disables ".part" operations. I do not believe + // JPF: that this is a good behaviour! + if(bDestExists) // must delete for overwrite + remove(sDest.data()); + + // WABA: Make sure that we keep writing permissions ourselves, + // otherwise we can be in for a surprise on NFS. + mode_t initialMode; + if (permissions != -1) + initialMode = permissions | S_IWUSR; + else + initialMode = 0666; + + // open the output file ... + KIO::fileoffset_t hCopyOffset = 0; + if(bResume) + { + iCopyFile = KDE_open( sPart.data(), O_RDWR ); // append if resuming + hCopyOffset = KDE_lseek(iCopyFile, 0, SEEK_END); + if(hCopyOffset < 0) + { + iError = ERR_CANNOT_RESUME; + return statusClientError; // client side error + } + kdDebug(7102) << "copy: resuming at " << hCopyOffset << endl; + } + else + iCopyFile = KDE_open(sPart.data(), O_CREAT | O_TRUNC | O_WRONLY, initialMode); + + if(iCopyFile == -1) + { + kdDebug(7102) << "copy: ### COULD NOT WRITE " << sCopyFile << endl; + iError = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED + : ERR_CANNOT_OPEN_FOR_WRITING; + return statusClientError; + } + + // delegate the real work (iError gets status) ... + StatusCode iRes = ftpGet(iError, iCopyFile, url, hCopyOffset); + if( ::close(iCopyFile) && iRes == statusSuccess ) + { + iError = ERR_COULD_NOT_WRITE; + iRes = statusClientError; + } + + // handle renaming or deletion of a partial file ... + if(bMarkPartial) + { + if(iRes == statusSuccess) + { // rename ".part" on success + if ( ::rename( sPart.data(), sDest.data() ) ) + { + kdDebug(7102) << "copy: cannot rename " << sPart << " to " << sDest << endl; + iError = ERR_CANNOT_RENAME_PARTIAL; + iRes = statusClientError; + } + } + else if(KDE_stat( sPart.data(), &buff ) == 0) + { // should a very small ".part" be deleted? + int size = config()->readNumEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); + if (buff.st_size < size) + remove(sPart.data()); + } + } + return iRes; +} diff --git a/kioslave/ftp/ftp.h b/kioslave/ftp/ftp.h new file mode 100644 index 000000000..e754152d2 --- /dev/null +++ b/kioslave/ftp/ftp.h @@ -0,0 +1,598 @@ +// -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@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. +*/ + +// $Id$ + +#ifndef __ftp_h__ +#define __ftp_h__ + +#include <config.h> + +#include <sys/types.h> +#include <sys/socket.h> + +#include <qcstring.h> +#include <qstring.h> + +#include <kurl.h> +#include <kio/slavebase.h> +#include <kextsock.h> +#include <ksocks.h> + +struct FtpEntry +{ + QString name; + QString owner; + QString group; + QString link; + + KIO::filesize_t size; + mode_t type; + mode_t access; + time_t date; +}; + +//=============================================================================== +// FtpTextReader A helper class to read text lines from a socket +//=============================================================================== + +#ifdef KIO_FTP_PRIVATE_INCLUDE +class FtpSocket; + +class FtpTextReader +{ +public: + FtpTextReader() { textClear(); } + +/** + * Resets the status of the object, also called from xtor + */ + void textClear(); + +/** + * Read a line from the socket into m_szText. Only the first RESP_READ_LIMIT + * characters are copied. If the server response is longer all extra data up to + * the new-line gets discarded. An ending CR gets stripped. The number of chars + * in the buffer is returned. Use textToLong() to check for truncation! + */ + int textRead(FtpSocket *pSock); + +/** + * An accessor to the data read by textRead() + */ + const char* textLine() const { return m_szText; } + +/** + * Returns true if the last textRead() resulted in a truncated line + */ + bool textTooLong() const { return m_bTextTruncated; } + +/** + * Returns true if the last textRead() got an EOF or an error + */ + bool textEOF() const { return m_bTextEOF; } + + enum { + + /** + * This is the physical size of m_szText. Only up to textReadLimit + * characters are used to store a server reply. If the server reply + * is longer, the stored line gets truncated - see textTooLong()! + */ + textReadBuffer = 2048, + +/** + * Max number of chars returned from textLine(). If the server + * sends more all chars until the next new-line are discarded. + */ + textReadLimit = 1024 + }; + +private: + /** + * textRead() sets this true on trucation (e.g. line too long) + */ + bool m_bTextTruncated; + + /** + * textRead() sets this true if the read returns 0 bytes or error + */ + bool m_bTextEOF; + + /** + * textRead() fills this buffer with data + */ + char m_szText[textReadBuffer]; + + /** + * the number of bytes in the current response line + */ + int m_iTextLine; + + /** + * the number of bytes in the response buffer (includes m_iRespLine) + */ + int m_iTextBuff; +}; +#endif // KIO_FTP_PRIVATE_INCLUDE + +//=============================================================================== +// FtpSocket Helper Class for Data or Control Connections +//=============================================================================== +#ifdef KIO_FTP_PRIVATE_INCLUDE +class FtpSocket : public FtpTextReader, public KExtendedSocket +{ +private: + // hide the default xtor + FtpSocket() {} +public: +/** + * The one and only public xtor. The string data passed to the + * xtor must remain valid during the object's lifetime - it is + * used in debug messages to identify the socket instance. + */ + FtpSocket(const char* pszName) + { + m_pszName = pszName; + m_server = -1; + } + + ~FtpSocket() { closeSocket(); } + +/** + * Resets the status of the object, also called from xtor + */ + void closeSocket(); + +/** + * We may have a server connection socket if not in passive mode. This + * routine returns the server socket set by setServer. The sock() + * function will return the server socket - if it is set. + */ + int server() const { return m_server; } + +/** + * Set the server socket if arg >= 0, otherwise clear it. + */ + void setServer(int i) { m_server = (i >= 0) ? i : -1; } + +/** + * returns the effective socket that user used for read/write. See server() + */ + int sock() const { return (m_server != -1) ? m_server : fd(); } + +/** + * output an debug message via kdDebug + */ + void debugMessage(const char* pszMsg) const; + +/** + * output an error message via kdError, returns iErrorCode + */ + int errorMessage(int iErrorCode, const char* pszMsg) const; + +/** + * connect socket and set some options (reuse, keepalive, linger) + */ + int connectSocket(int iTimeOutSec, bool bControl); + +/** + * utility to simplify calls to ::setsockopt(). Uses sock(). + */ + bool setSocketOption(int opt, char*arg, socklen_t len) const; + +/** + * utility to read data from the effective socket, see sock() + */ + long read(void* pData, long iMaxlen) + { + return KSocks::self()->read(sock(), pData, iMaxlen); + } + +/** + * utility to write data to the effective socket, see sock() + */ + long write(void* pData, long iMaxlen) + { + return KSocks::self()->write(sock(), pData, iMaxlen); + } + +/** + * Use the inherited FtpTextReader to read a line from the socket + */ + int textRead() + { + return FtpTextReader::textRead(this); + } + +private: + const char* m_pszName; // set by the xtor, used for debug output + int m_server; // socket override, see setSock() +}; +#else + class FtpSocket; +#endif // KIO_FTP_PRIVATE_INCLUDE + +//=============================================================================== +// Ftp +//=============================================================================== +class Ftp : public KIO::SlaveBase +{ + // Ftp() {} + +public: + Ftp( const QCString &pool, const QCString &app ); + virtual ~Ftp(); + + virtual void setHost( const QString& host, int port, const QString& user, const QString& pass ); + + /** + * Connects to a ftp server and logs us in + * m_bLoggedOn is set to true if logging on was successful. + * It is set to false if the connection becomes closed. + * + */ + virtual void openConnection(); + + /** + * Closes the connection + */ + virtual void closeConnection(); + + virtual void stat( const KURL &url ); + + 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 del( const KURL & url, bool isfile ); + virtual void chmod( const KURL & url, int permissions ); + + virtual void get( const KURL& url ); + virtual void put( const KURL& url, int permissions, bool overwrite, bool resume); + //virtual void mimetype( const KURL& url ); + + virtual void slave_status(); + + /** + * Handles the case that one side of the job is a local file + */ + virtual void copy( const KURL &src, const KURL &dest, int permissions, bool overwrite ); + +private: + // ------------------------------------------------------------------------ + // All the methods named ftpXyz are lowlevel methods that are not exported. + // The implement functionality used by the public high-level methods. Some + // low-level methods still use error() to emit errors. This behaviour is not + // recommended - please return a boolean status or an error code instead! + // ------------------------------------------------------------------------ + + /** + * Status Code returned from ftpPut() and ftpGet(), used to select + * source or destination url for error messages + */ + typedef enum { + statusSuccess, + statusClientError, + statusServerError + } StatusCode; + + /** + * Login Mode for ftpOpenConnection + */ + typedef enum { + loginDefered, + loginExplicit, + loginImplicit + } LoginMode; + + /** + * Connect and login to the FTP server. + * + * @param loginMode controls if login info should be sent<br> + * loginDefered - must not be logged on, no login info is sent<br> + * loginExplicit - must not be logged on, login info is sent<br> + * loginImplicit - login info is sent if not logged on + * + * @return true on success (a login failure would return false). + */ + bool ftpOpenConnection (LoginMode loginMode); + + /** + * Executes any auto login macro's as specified in a .netrc file. + */ + void ftpAutoLoginMacro (); + + /** + * Called by openConnection. It logs us in. + * m_initialPath is set to the current working directory + * if logging on was successful. + * + * @return true on success. + */ + bool ftpLogin(); + + /** + * ftpSendCmd - send a command (@p cmd) and read response + * + * @param maxretries number of time it should retry. Since it recursively + * calls itself if it can't read the answer (this happens especially after + * timeouts), we need to limit the recursiveness ;-) + * + * return true if any response received, false on error + */ + bool ftpSendCmd( const QCString& cmd, int maxretries = 1 ); + + /** + * Use the SIZE command to get the file size. + * @param mode the size depends on the transfer mode, hence this arg. + * @return true on success + * Gets the size into m_size. + */ + bool ftpSize( const QString & path, char mode ); + + /** + * Set the current working directory, but only if not yet current + */ + bool ftpFolder(const QString& path, bool bReportError); + + /** + * Runs a command on the ftp server like "list" or "retr". In contrast to + * ftpSendCmd a data connection is opened. The corresponding socket + * sData is available for reading/writing on success. + * The connection must be closed afterwards with ftpCloseCommand. + * + * @param mode is 'A' or 'I'. 'A' means ASCII transfer, 'I' means binary transfer. + * @param errorcode the command-dependent error code to emit on error + * + * @return true if the command was accepted by the server. + */ + bool ftpOpenCommand( const char *command, const QString & path, char mode, + int errorcode, KIO::fileoffset_t offset = 0 ); + + /** + * The counterpart to openCommand. + * Closes data sockets and then reads line sent by server at + * end of command. + * @return false on error (line doesn't start with '2') + */ + bool ftpCloseCommand(); + + /** + * Send "TYPE I" or "TYPE A" only if required, see m_cDataMode. + * + * Use 'A' to select ASCII and 'I' to select BINARY mode. If + * cMode is '?' the m_bTextMode flag is used to choose a mode. + */ + bool ftpDataMode(char cMode); + + //void ftpAbortTransfer(); + + /** + * Used by ftpOpenCommand, return 0 on success or an error code + */ + int ftpOpenDataConnection(); + + /** + * closes a data connection, see ftpOpenDataConnection() + */ + void ftpCloseDataConnection(); + + /** + * Helper for ftpOpenDataConnection + */ + int ftpOpenPASVDataConnection(); + /** + * Helper for ftpOpenDataConnection + */ + int ftpOpenEPSVDataConnection(); + /** + * Helper for ftpOpenDataConnection + */ + int ftpOpenEPRTDataConnection(); + /** + * Helper for ftpOpenDataConnection + */ + int ftpOpenPortDataConnection(); + + /** + * ftpAcceptConnect - wait for incoming connection + * + * return -2 on error or timeout + * otherwise returns socket descriptor + */ + int ftpAcceptConnect(); + + bool ftpChmod( const QString & path, int permissions ); + + // used by listDir + bool ftpOpenDir( const QString & path ); + /** + * Called to parse directory listings, call this until it returns false + */ + bool ftpReadDir(FtpEntry& ftpEnt); + + /** + * Helper to fill an UDSEntry + */ + void ftpCreateUDSEntry( const QString & filename, FtpEntry& ftpEnt, KIO::UDSEntry& entry, bool isDir ); + + void ftpShortStatAnswer( const QString& filename, bool isDir ); + + void ftpStatAnswerNotFound( const QString & path, const QString & filename ); + + /** + * This is the internal implementation of rename() - set put(). + * + * @return true on success. + */ + bool ftpRename( const QString & src, const QString & dst, bool overwrite ); + + /** + * Called by openConnection. It opens the control connection to the ftp server. + * + * @return true on success. + */ + bool ftpOpenControlConnection( const QString & host, unsigned short int port ); + + /** + * closes the socket holding the control connection (see ftpOpenControlConnection) + */ + void ftpCloseControlConnection(); + + /** + * read a response from the server (a trailing CR gets stripped) + * @param iOffset -1 to read a new line from the server<br> + * 0 to return the whole response string + * >0 to return the response with iOffset chars skipped + * @return the reponse message with iOffset chars skipped (or "" if iOffset points + * behind the available data) + */ + const char* ftpResponse(int iOffset); + + /** + * This is the internal implementation of get() - see copy(). + * + * IMPORTANT: the caller should call ftpCloseCommand() on return. + * The function does not call error(), the caller should do this. + * + * @param iError set to an ERR_xxxx code on error + * @param iCopyFile -1 -or- handle of a local destination file + * @param hCopyOffset local file only: non-zero for resume + * @return 0 for success, -1 for server error, -2 for client error + */ + StatusCode ftpGet(int& iError, int iCopyFile, const KURL& url, KIO::fileoffset_t hCopyOffset); + + /** + * This is the internal implementation of put() - see copy(). + * + * IMPORTANT: the caller should call ftpCloseCommand() on return. + * The function does not call error(), the caller should do this. + * + * @param iError set to an ERR_xxxx code on error + * @param iCopyFile -1 -or- handle of a local source file + * @return 0 for success, -1 for server error, -2 for client error + */ + StatusCode ftpPut(int& iError, int iCopyFile, const KURL& url, int permissions, bool overwrite, bool resume); + + /** + * helper called from copy() to implement FILE -> FTP transfers + * + * @param iError set to an ERR_xxxx code on error + * @param iCopyFile [out] handle of a local source file + * @param sCopyFile path of the local source file + * @return 0 for success, -1 for server error, -2 for client error + */ + StatusCode ftpCopyPut(int& iError, int& iCopyFile, QString sCopyFile, const KURL& url, int permissions, bool overwrite); + + /** + * helper called from copy() to implement FTP -> FILE transfers + * + * @param iError set to an ERR_xxxx code on error + * @param iCopyFile [out] handle of a local source file + * @param sCopyFile path of the local destination file + * @return 0 for success, -1 for server error, -2 for client error + */ + StatusCode ftpCopyGet(int& iError, int& iCopyFile, QString sCopyFile, const KURL& url, int permissions, bool overwrite); + +private: // data members + + QString m_host; + unsigned short int m_port; + QString m_user; + QString m_pass; + /** + * Where we end up after connecting + */ + QString m_initialPath; + KURL m_proxyURL; + + /** + * the current working directory - see ftpFolder + */ + QString m_currentPath; + + /** + * the status returned by the FTP protocol, set in ftpResponse() + */ + int m_iRespCode; + + /** + * the status/100 returned by the FTP protocol, set in ftpResponse() + */ + int m_iRespType; + + /** + * This flag is maintained by ftpDataMode() and contains I or A after + * ftpDataMode() has successfully set the mode. + */ + char m_cDataMode; + + /** + * true if logged on (m_control should also be non-NULL) + */ + bool m_bLoggedOn; + + /** + * true if a "textmode" metadata key was found by ftpLogin(). This + * switches the ftp data transfer mode from binary to ASCII. + */ + bool m_bTextMode; + + /** + * true if a data stream is open, used in closeConnection(). + * + * When the user cancels a get or put command the Ftp dtor will be called, + * which in turn calls closeConnection(). The later would try to send QUIT + * which won't work until timeout. ftpOpenCommand sets the m_bBusy flag so + * that the sockets will be closed immedeately - the server should be + * capable of handling this and return an error code on thru the control + * connection. The m_bBusy gets cleared by the ftpCloseCommand() routine. + */ + bool m_bBusy; + + bool m_bPasv; + bool m_bUseProxy; + + KIO::filesize_t m_size; + static KIO::filesize_t UnknownSize; + + enum + { + epsvUnknown = 0x01, + epsvAllUnknown = 0x02, + eprtUnknown = 0x04, + epsvAllSent = 0x10, + pasvUnknown = 0x20, + chmodUnknown = 0x100 + }; + int m_extControl; + + /** + * control connection socket, only set if openControl() succeeded + */ + FtpSocket *m_control; + + /** + * data connection socket + */ + FtpSocket *m_data; +}; + +#endif diff --git a/kioslave/ftp/ftp.protocol b/kioslave/ftp/ftp.protocol new file mode 100644 index 000000000..3af1e33ec --- /dev/null +++ b/kioslave/ftp/ftp.protocol @@ -0,0 +1,18 @@ +[Protocol] +exec=kio_ftp +protocol=ftp +input=none +output=filesystem +copyToFile=true +copyFromFile=true +listing=Name,Type,Size,Date,Access,Owner,Group,Link, +reading=true +writing=true +makedir=true +deleting=true +moving=true +ProxiedBy=http +Icon=ftp +maxInstances=2 +DocPath=kioslave/ftp.html +Class=:internet diff --git a/kioslave/gzip/Makefile.am b/kioslave/gzip/Makefile.am new file mode 100644 index 000000000..2b290c206 --- /dev/null +++ b/kioslave/gzip/Makefile.am @@ -0,0 +1,12 @@ +INCLUDES = -I$(top_srcdir)/kio $(all_includes) +AM_LDFLAGS = $(all_libraries) $(KDE_RPATH) +METASOURCES = AUTO + +kde_module_LTLIBRARIES = kgzipfilter.la + +kgzipfilter_la_SOURCES = kgzipfilter.cpp +kgzipfilter_la_LIBADD = $(LIB_KIO) $(LIBZ) +kgzipfilter_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) + +kde_services_DATA = kgzipfilter.desktop + diff --git a/kioslave/gzip/kgzipfilter.cpp b/kioslave/gzip/kgzipfilter.cpp new file mode 100644 index 000000000..21380c6c1 --- /dev/null +++ b/kioslave/gzip/kgzipfilter.cpp @@ -0,0 +1,336 @@ +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@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 version 2 as published by the Free Software Foundation. + + 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 "kgzipfilter.h" +#include <time.h> +#include <zlib.h> +#include <kdebug.h> +#include <klibloader.h> + +/* gzip flag byte */ +#define ASCII_FLAG 0x01 /* bit 0 set: file probably ascii text */ +#define HEAD_CRC 0x02 /* bit 1 set: header CRC present */ +#define EXTRA_FIELD 0x04 /* bit 2 set: extra field present */ +#define ORIG_NAME 0x08 /* bit 3 set: original file name present */ +#define COMMENT 0x10 /* bit 4 set: file comment present */ +#define RESERVED 0xE0 /* bits 5..7: reserved */ + + +// #define DEBUG_GZIP + +class KGzipFilterFactory : public KLibFactory +{ +public: + KGzipFilterFactory() : KLibFactory() {} + ~KGzipFilterFactory(){} + QObject *createObject( QObject *parent, const char *name, const char*className, const QStringList & args ) + { + Q_UNUSED(parent); + Q_UNUSED(name); + Q_UNUSED(className); + Q_UNUSED(args); + return new KGzipFilter; + } +}; + +K_EXPORT_COMPONENT_FACTORY( kgzipfilter, KGzipFilterFactory ) + +// Not really necessary anymore, now that this is a dynamically-loaded lib. +class KGzipFilter::KGzipFilterPrivate +{ +public: + z_stream zStream; + bool bCompressed; +}; + +KGzipFilter::KGzipFilter() +{ + d = new KGzipFilterPrivate; + d->zStream.zalloc = (alloc_func)0; + d->zStream.zfree = (free_func)0; + d->zStream.opaque = (voidpf)0; +} + + +KGzipFilter::~KGzipFilter() +{ + delete d; +} + +void KGzipFilter::init( int mode ) +{ + d->zStream.next_in = Z_NULL; + d->zStream.avail_in = 0; + if ( mode == IO_ReadOnly ) + { + int result = inflateInit2(&d->zStream, -MAX_WBITS); // windowBits is passed < 0 to suppress zlib header + if ( result != Z_OK ) + kdDebug(7005) << "inflateInit returned " << result << endl; + // No idea what to do with result :) + } else if ( mode == IO_WriteOnly ) + { + int result = deflateInit2(&d->zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); // same here + if ( result != Z_OK ) + kdDebug(7005) << "deflateInit returned " << result << endl; + } else { + kdWarning(7005) << "KGzipFilter: Unsupported mode " << mode << ". Only IO_ReadOnly and IO_WriteOnly supported" << endl; + } + m_mode = mode; + d->bCompressed = true; + m_headerWritten = false; +} + +void KGzipFilter::terminate() +{ + if ( m_mode == IO_ReadOnly ) + { + int result = inflateEnd(&d->zStream); + if ( result != Z_OK ) + kdDebug(7005) << "inflateEnd returned " << result << endl; + } else if ( m_mode == IO_WriteOnly ) + { + int result = deflateEnd(&d->zStream); + if ( result != Z_OK ) + kdDebug(7005) << "deflateEnd returned " << result << endl; + } +} + + +void KGzipFilter::reset() +{ + if ( m_mode == IO_ReadOnly ) + { + int result = inflateReset(&d->zStream); + if ( result != Z_OK ) + kdDebug(7005) << "inflateReset returned " << result << endl; + } else if ( m_mode == IO_WriteOnly ) { + int result = deflateReset(&d->zStream); + if ( result != Z_OK ) + kdDebug(7005) << "deflateReset returned " << result << endl; + m_headerWritten = false; + } +} + +bool KGzipFilter::readHeader() +{ +#ifdef DEBUG_GZIP + kdDebug(7005) << "KGzipFilter::readHeader avail=" << d->zStream.avail_in << endl; +#endif + // Assume not compressed until we successfully decode the header + d->bCompressed = false; + // Assume the first block of data contains the whole header. + // The right way is to build this as a big state machine which + // is a pain in the ass. + // With 8K-blocks, we don't risk much anyway. + Bytef *p = d->zStream.next_in; + int i = d->zStream.avail_in; + if ((i -= 10) < 0) return false; // Need at least 10 bytes +#ifdef DEBUG_GZIP + kdDebug(7005) << "KGzipFilter::readHeader first byte is " << QString::number(*p,16) << endl; +#endif + if (*p++ != 0x1f) return false; // GZip magic +#ifdef DEBUG_GZIP + kdDebug(7005) << "KGzipFilter::readHeader second byte is " << QString::number(*p,16) << endl; +#endif + if (*p++ != 0x8b) return false; + int method = *p++; + int flags = *p++; + if ((method != Z_DEFLATED) || (flags & RESERVED) != 0) return false; + p += 6; + if ((flags & EXTRA_FIELD) != 0) // skip extra field + { + if ((i -= 2) < 0) return false; // Need at least 2 bytes + int len = *p++; + len += (*p++) << 8; + if ((i -= len) < 0) return false; // Need at least len bytes + p += len; + } + if ((flags & ORIG_NAME) != 0) // skip original file name + { +#ifdef DEBUG_GZIP + kdDebug(7005) << "ORIG_NAME=" << p << endl; +#endif + while( (i > 0) && (*p)) + { + i--; p++; + } + if (--i <= 0) return false; + p++; + } + if ((flags & COMMENT) != 0) // skip comment + { + while( (i > 0) && (*p)) + { + i--; p++; + } + if (--i <= 0) return false; + p++; + } + if ((flags & HEAD_CRC) != 0) // skip the header crc + { + if ((i-=2) < 0) return false; + p += 2; + } + + d->zStream.avail_in = i; + d->zStream.next_in = p; + d->bCompressed = true; +#ifdef DEBUG_GZIP + kdDebug(7005) << "header OK" << endl; +#endif + return true; +} + +/* Output a 16 bit value, lsb first */ +#define put_short(w) \ + *p++ = (uchar) ((w) & 0xff); \ + *p++ = (uchar) ((ushort)(w) >> 8); + +/* Output a 32 bit value to the bit stream, lsb first */ +#define put_long(n) \ + put_short((n) & 0xffff); \ + put_short(((ulong)(n)) >> 16); + +bool KGzipFilter::writeHeader( const QCString & fileName ) +{ + Bytef *p = d->zStream.next_out; + int i = d->zStream.avail_out; + *p++ = 0x1f; + *p++ = 0x8b; + *p++ = Z_DEFLATED; + *p++ = ORIG_NAME; + put_long( time( 0L ) ); // Modification time (in unix format) + *p++ = 0; // Extra flags (2=max compress, 4=fastest compress) + *p++ = 3; // Unix + + uint len = fileName.length(); + for ( uint j = 0 ; j < len ; ++j ) + *p++ = fileName[j]; + *p++ = 0; + int headerSize = p - d->zStream.next_out; + i -= headerSize; + Q_ASSERT(i>0); + m_crc = crc32(0L, Z_NULL, 0); + d->zStream.next_out = p; + d->zStream.avail_out = i; + m_headerWritten = true; + return true; +} + +void KGzipFilter::writeFooter() +{ + Q_ASSERT( m_headerWritten ); + if (!m_headerWritten) kdDebug() << kdBacktrace(); + Bytef *p = d->zStream.next_out; + int i = d->zStream.avail_out; + //kdDebug(7005) << "KGzipFilter::writeFooter writing CRC= " << QString::number( m_crc, 16 ) << endl; + put_long( m_crc ); + //kdDebug(7005) << "KGzipFilter::writing writing totalin= " << d->zStream.total_in << endl; + put_long( d->zStream.total_in ); + i -= p - d->zStream.next_out; + d->zStream.next_out = p; + d->zStream.avail_out = i; +} + +void KGzipFilter::setOutBuffer( char * data, uint maxlen ) +{ + d->zStream.avail_out = maxlen; + d->zStream.next_out = (Bytef *) data; +} +void KGzipFilter::setInBuffer( const char * data, uint size ) +{ +#ifdef DEBUG_GZIP + kdDebug(7005) << "KGzipFilter::setInBuffer avail_in=" << size << endl; +#endif + d->zStream.avail_in = size; + d->zStream.next_in = (Bytef*) data; +} +int KGzipFilter::inBufferAvailable() const +{ + return d->zStream.avail_in; +} +int KGzipFilter::outBufferAvailable() const +{ + return d->zStream.avail_out; +} + +KGzipFilter::Result KGzipFilter::uncompress_noop() +{ + // I'm not sure we really need support for that (uncompressed streams), + // but why not, it can't hurt to have it. One case I can think of is someone + // naming a tar file "blah.tar.gz" :-) + if ( d->zStream.avail_in > 0 ) + { + int n = (d->zStream.avail_in < d->zStream.avail_out) ? d->zStream.avail_in : d->zStream.avail_out; + memcpy( d->zStream.next_out, d->zStream.next_in, n ); + d->zStream.avail_out -= n; + d->zStream.next_in += n; + d->zStream.avail_in -= n; + return OK; + } else + return END; +} + +KGzipFilter::Result KGzipFilter::uncompress() +{ + Q_ASSERT ( m_mode == IO_ReadOnly ); + if ( d->bCompressed ) + { +#ifdef DEBUG_GZIP + kdDebug(7005) << "Calling inflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable() << endl; + kdDebug(7005) << " next_in=" << d->zStream.next_in << endl; +#endif + int result = inflate(&d->zStream, Z_SYNC_FLUSH); +#ifdef DEBUG_GZIP + kdDebug(7005) << " -> inflate returned " << result << endl; + kdDebug(7005) << "Now avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable() << endl; + kdDebug(7005) << " next_in=" << d->zStream.next_in << endl; +#else + if ( result != Z_OK && result != Z_STREAM_END ) + kdDebug(7005) << "Warning: inflate() returned " << result << endl; +#endif + return ( result == Z_OK ? OK : ( result == Z_STREAM_END ? END : ERROR ) ); + } else + return uncompress_noop(); +} + +KGzipFilter::Result KGzipFilter::compress( bool finish ) +{ + Q_ASSERT ( d->bCompressed ); + Q_ASSERT ( m_mode == IO_WriteOnly ); + + Bytef* p = d->zStream.next_in; + ulong len = d->zStream.avail_in; +#ifdef DEBUG_GZIP + kdDebug(7005) << " calling deflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable() << endl; +#endif + int result = deflate(&d->zStream, finish ? Z_FINISH : Z_NO_FLUSH); + if ( result != Z_OK && result != Z_STREAM_END ) + kdDebug(7005) << " deflate returned " << result << endl; + if ( m_headerWritten ) + { + //kdDebug(7005) << "Computing CRC for the next " << len - d->zStream.avail_in << " bytes" << endl; + m_crc = crc32(m_crc, p, len - d->zStream.avail_in); + } + if ( result == Z_STREAM_END && m_headerWritten ) + { + //kdDebug(7005) << "KGzipFilter::compress finished, write footer" << endl; + writeFooter(); + } + return ( result == Z_OK ? OK : ( result == Z_STREAM_END ? END : ERROR ) ); +} diff --git a/kioslave/gzip/kgzipfilter.desktop b/kioslave/gzip/kgzipfilter.desktop new file mode 100644 index 000000000..e9f3c119c --- /dev/null +++ b/kioslave/gzip/kgzipfilter.desktop @@ -0,0 +1,86 @@ +[Desktop Entry] +Type=Service +Name=GZip Filter +Name[af]=Gzip Filter +Name[ar]=فلتر GZip +Name[az]=GZip Filtri +Name[be]=Фільтр GZip +Name[bg]=Филтър GZip +Name[bn]=জি-জিপ (Gzip) ফিল্টার +Name[br]=Sil GZip +Name[ca]=Filtre GZip +Name[cs]=Filtr GZip2 +Name[csb]=Filter GZipa +Name[cy]=Hidl GZip +Name[da]=GZip-filter +Name[de]=GZip-Filter +Name[el]=Φίλτρο GZip +Name[eo]=GZip-filtrilo +Name[es]=Filtro GZip +Name[et]=GZip filter +Name[eu]=GZip iragazkia +Name[fa]=پالایۀ GZip +Name[fi]=GZip-suodin +Name[fr]=Filtre Gzip +Name[fy]=GZip-filter +Name[ga]=Scagaire gzip +Name[gl]=Filtro GZip +Name[he]=מסנן GZip +Name[hi]=GZip फ़िल्टर +Name[hr]=GZip filtar +Name[hu]=GZip szűrő +Name[id]=Filter Gzip +Name[is]=GZip sía +Name[it]=Filtro Gzip +Name[ja]=GZip フィルタ +Name[ka]=GZip ფილტრი +Name[kk]=GZip сүзгісі +Name[km]=តម្រង GZip +Name[ko]=GZip 거르개 +Name[lb]=GZip-Filter +Name[lt]=GZip filtras +Name[lv]=GZip Filtrs +Name[mk]=GZip филтер +Name[mn]=GZip-Filter +Name[ms]=Penapis GZip +Name[mt]=Filtru GZip +Name[nb]=GZip-filter +Name[nds]=GZip-Filter +Name[ne]=GZip फिल्टर +Name[nl]=GZip-filter +Name[nn]=GZip-filter +Name[nso]=Sesekodi sa GZip +Name[pa]=GZip ਫਿਲਟਰ +Name[pl]=Filtr GZipa +Name[pt]=Filtro GZip +Name[pt_BR]=Filtro GZip +Name[ro]=Filtru GZip +Name[ru]=Фильтр gzip +Name[rw]=Muyunguruzi GZipu +Name[se]=GZip-filter +Name[sk]=GZip filter +Name[sl]=Filter za gzip +Name[sq]=Filteri GZip +Name[sr]=GZip филтер +Name[sr@Latn]=GZip filter +Name[ss]=Sisefo se GZip +Name[sv]=Gzip-filter +Name[ta]=GZip வடிகட்டி +Name[te]=జిజిప్ గలని +Name[tg]=Таровиши GZip +Name[th]=ตัวกรอง GZip +Name[tr]=GZip Filtresi +Name[tt]=GZip Sözgeçe +Name[uk]=Фільтр GZip +Name[uz]=GZip-filter +Name[uz@cyrillic]=GZip-филтер +Name[ven]=Filithara ya GZip +Name[vi]=Bộ lọc GZip +Name[wa]=Passete GZip +Name[xh]=Isihluzi se GZip +Name[zh_CN]=GZip 过滤程序 +Name[zh_HK]=GZip 過濾器 +Name[zh_TW]=GZip 過濾器 +Name[zu]=Ihluzo le-GZip +X-KDE-Library=kgzipfilter +ServiceTypes=KDECompressionFilter,application/x-gzip,application/x-tgz diff --git a/kioslave/gzip/kgzipfilter.h b/kioslave/gzip/kgzipfilter.h new file mode 100644 index 000000000..c103d774d --- /dev/null +++ b/kioslave/gzip/kgzipfilter.h @@ -0,0 +1,52 @@ +/* This file is part of the KDE libraries + Copyright (C) 2000 David Faure <faure@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 version 2 as published by the Free Software Foundation. + + 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 __kgzipfilter__h +#define __kgzipfilter__h + +#include "kfilterbase.h" + +class KGzipFilter : public KFilterBase +{ +public: + KGzipFilter(); + virtual ~KGzipFilter(); + + virtual void init( int mode ); + virtual int mode() const { return m_mode; } + virtual void terminate(); + virtual void reset(); + virtual bool readHeader(); + virtual bool writeHeader( const QCString & fileName ); + void writeFooter(); + virtual void setOutBuffer( char * data, uint maxlen ); + virtual void setInBuffer( const char * data, uint size ); + virtual int inBufferAvailable() const; + virtual int outBufferAvailable() const; + virtual Result uncompress(); + virtual Result compress( bool finish ); +private: + Result uncompress_noop(); + int m_mode; + ulong m_crc; + bool m_headerWritten; + class KGzipFilterPrivate; + KGzipFilterPrivate *d; +}; + +#endif 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 diff --git a/kioslave/metainfo/Makefile.am b/kioslave/metainfo/Makefile.am new file mode 100644 index 000000000..6807019f4 --- /dev/null +++ b/kioslave/metainfo/Makefile.am @@ -0,0 +1,24 @@ +## $Id$ +## Makefile.am of kdebase/kioslave/metainfo + +INCLUDES = $(all_includes) +AM_LDFLAGS = $(all_libraries) $(KDE_RPATH) +METASOURCES = AUTO + +kde_module_LTLIBRARIES = kio_metainfo.la + +kio_metainfo_la_SOURCES = metainfo.cpp +kio_metainfo_la_LIBADD = $(LIB_KIO) +kio_metainfo_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) + +noinst_HEADERS = metainfo.h + +kdelnk_DATA = metainfo.protocol +kdelnkdir = $(kde_servicesdir) + +#servicetypes_DATA = thumbcreator.desktop +#servicetypesdir = $(kde_servicetypesdir) + +#services_DATA = imagethumbnail.desktop textthumbnail.desktop +# htmlthumbnail.desktop gsthumbnail.desktop +#servicesdir = $(kde_servicesdir) diff --git a/kioslave/metainfo/metainfo.cpp b/kioslave/metainfo/metainfo.cpp new file mode 100644 index 000000000..0e4814b33 --- /dev/null +++ b/kioslave/metainfo/metainfo.cpp @@ -0,0 +1,103 @@ +/* This file is part of the KDE libraries + Copyright (C) 2002 Rolf Magnus <ramagnus@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 version 2.0 + + 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. +*/ + +// $Id$ + +#include <kdatastream.h> // Do not remove, needed for correct bool serialization +#include <kurl.h> +#include <kapplication.h> +#include <kmimetype.h> +#include <kdebug.h> +#include <kfilemetainfo.h> +#include <klocale.h> +#include <stdlib.h> + +#include "metainfo.h" + +// Recognized metadata entries: +// mimeType - the mime type of the file, so we need not extra determine it +// what - what to load + +using namespace KIO; + +extern "C" +{ + KDE_EXPORT int kdemain(int argc, char **argv); +} + +int kdemain(int argc, char **argv) +{ + KApplication app(argc, argv, "kio_metainfo", false, true); + + if (argc != 4) + { + kdError() << "Usage: kio_metainfo protocol domain-socket1 domain-socket2" << endl; + exit(-1); + } + + MetaInfoProtocol slave(argv[2], argv[3]); + slave.dispatchLoop(); + + return 0; +} + +MetaInfoProtocol::MetaInfoProtocol(const QCString &pool, const QCString &app) + : SlaveBase("metainfo", pool, app) +{ +} + +MetaInfoProtocol::~MetaInfoProtocol() +{ +} + +void MetaInfoProtocol::get(const KURL &url) +{ + QString mimeType = metaData("mimeType"); + KFileMetaInfo info(url.path(), mimeType); + + QByteArray arr; + QDataStream stream(arr, IO_WriteOnly); + + stream << info; + + data(arr); + finished(); +} + +void MetaInfoProtocol::put(const KURL& url, int, bool, bool) +{ + QString mimeType = metaData("mimeType"); + KFileMetaInfo info; + + QByteArray arr; + readData(arr); + QDataStream stream(arr, IO_ReadOnly); + + stream >> info; + + if (info.isValid()) + { + info.applyChanges(); + } + else + { + error(ERR_NO_CONTENT, i18n("No metainfo for %1").arg(url.path())); + return; + } + finished(); +} diff --git a/kioslave/metainfo/metainfo.h b/kioslave/metainfo/metainfo.h new file mode 100644 index 000000000..de2a6b055 --- /dev/null +++ b/kioslave/metainfo/metainfo.h @@ -0,0 +1,38 @@ +/* This file is part of the KDE libraries + Copyright (C) 2002 Rolf Magnus <ramagnus@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 version 2.0 + + 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. +*/ + +// $Id$ + +#ifndef _METAINFO_H_ +#define _METAINFO_H_ + +#include <kio/slavebase.h> + +class MetaInfoProtocol : public KIO::SlaveBase +{ +public: + MetaInfoProtocol(const QCString &pool, const QCString &app); + virtual ~MetaInfoProtocol(); + + virtual void get(const KURL &url); + virtual void put(const KURL& url, int permissions, + bool overwrite, bool resume); + +}; + +#endif diff --git a/kioslave/metainfo/metainfo.protocol b/kioslave/metainfo/metainfo.protocol new file mode 100644 index 000000000..f1fa9adac --- /dev/null +++ b/kioslave/metainfo/metainfo.protocol @@ -0,0 +1,9 @@ +[Protocol] +exec=kio_metainfo +protocol=metainfo +input=stream +output=stream +reading=true +writing=true +source=false +Icon=help |