From ce4a32fe52ef09d8f5ff1dd22c001110902b60a2 Mon Sep 17 00:00:00 2001 From: toma Date: Wed, 25 Nov 2009 17:56:58 +0000 Subject: Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features. BUG:215923 git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdelibs@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da --- kioslave/http/http.cc | 6095 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 6095 insertions(+) create mode 100644 kioslave/http/http.cc (limited to 'kioslave/http/http.cc') 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 + Copyright (C) 2000-2002 George Staikos + Copyright (C) 2000-2002 Dawit Alemayehu + Copyright (C) 2001,2002 Hamish Rodda + + 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 + +#include +#include +#include +#include +#include +#include +#include +#include // Required for AIX +#include +#include // must be explicitly included for MacOSX + +/* +#include +#include +#include +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kio/ioslave_defaults.h" +#include "kio/http_slave_defaults.h" + +#include "httpfilter.h" +#include "http.h" + +#ifdef HAVE_LIBGSSAPI +#ifdef GSSAPI_MIT +#include +#else +#include +#endif /* GSSAPI_MIT */ + +// Catch uncompatible crap (BR86019) +#if defined(GSS_RFC_COMPLIANT_OIDS) && (GSS_RFC_COMPLIANT_OIDS == 0) +#include +#define GSS_C_NT_HOSTBASED_SERVICE gss_nt_service_name +#endif + +#endif /* HAVE_LIBGSSAPI */ + +#include + +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 << ")" <\r\n"; + request.append( "\r\n" ); + request.append( query.utf8() ); + request.append( "\r\n" ); + + davSetRequest( request ); + } else { + // We are only after certain features... + QCString request; + request = "" + ""; + + // insert additional XML request from the davRequestResponse metadata + if ( hasMetaData( "davRequestResponse" ) ) + request += metaData( "davRequestResponse" ).utf8(); + else { + // No special request, ask for default properties + request += "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + } + request += ""; + + 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 = "" + "" + "" + "" + "" + "" + ""; + + 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.
    ").arg( action ).arg( url ); + + for ( QStringList::Iterator it = errors.begin(); it != errors.end(); ++it ) + errorString += "
  • " + *it + "
  • "; + + errorString += "
"; + } + 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 " << 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(_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: ; 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(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) << "("<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 + // 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("%1 at %2").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("%1 at %2").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; icount && !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" -- cgit v1.2.1