• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdelibs-4.9.4 API Reference
  • KDE Home
  • Contact Us
 

KIOSlave

  • kioslave
  • http
http.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2000-2003 Waldo Bastian <bastian@kde.org>
3  Copyright (C) 2000-2002 George Staikos <staikos@kde.org>
4  Copyright (C) 2000-2002 Dawit Alemayehu <adawit@kde.org>
5  Copyright (C) 2001,2002 Hamish Rodda <rodda@kde.org>
6  Copyright (C) 2007 Nick Shaforostoff <shafff@ukr.net>
7  Copyright (C) 2007 Daniel Nicoletti <mirttex@users.sourceforge.net>
8  Copyright (C) 2008,2009 Andreas Hartmetz <ahartmetz@gmail.com>
9 
10  This library is free software; you can redistribute it and/or
11  modify it under the terms of the GNU Library General Public
12  License (LGPL) as published by the Free Software Foundation;
13  either version 2 of the License, or (at your option) any later
14  version.
15 
16  This library is distributed in the hope that it will be useful,
17  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19  Library General Public License for more details.
20 
21  You should have received a copy of the GNU Library General Public License
22  along with this library; see the file COPYING.LIB. If not, write to
23  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24  Boston, MA 02110-1301, USA.
25 */
26 
27 // TODO delete / do not save very big files; "very big" to be defined
28 
29 #define QT_NO_CAST_FROM_ASCII
30 
31 #include "http.h"
32 
33 #include <config.h>
34 
35 #include <fcntl.h>
36 #include <utime.h>
37 #include <stdlib.h>
38 #include <stdio.h>
39 #include <sys/stat.h>
40 #include <sys/time.h>
41 #include <unistd.h> // must be explicitly included for MacOSX
42 
43 #include <QtXml/qdom.h>
44 #include <QtCore/QFile>
45 #include <QtCore/QRegExp>
46 #include <QtCore/QDate>
47 #include <QtCore/QBuffer>
48 #include <QtCore/QIODevice>
49 #include <QtDBus/QtDBus>
50 #include <QtNetwork/QAuthenticator>
51 #include <QtNetwork/QNetworkProxy>
52 #include <QtNetwork/QTcpSocket>
53 
54 #include <kurl.h>
55 #include <kdebug.h>
56 #include <klocale.h>
57 #include <kconfig.h>
58 #include <kconfiggroup.h>
59 #include <kservice.h>
60 #include <kdatetime.h>
61 #include <kcomponentdata.h>
62 #include <kmimetype.h>
63 #include <ktoolinvocation.h>
64 #include <kstandarddirs.h>
65 #include <kremoteencoding.h>
66 #include <ktcpsocket.h>
67 #include <kmessagebox.h>
68 
69 #include <kio/ioslave_defaults.h>
70 #include <kio/http_slave_defaults.h>
71 
72 #include <httpfilter.h>
73 
74 #include <solid/networking.h>
75 
76 #include <kapplication.h>
77 #include <kaboutdata.h>
78 #include <kcmdlineargs.h>
79 #include <kde_file.h>
80 #include <ktemporaryfile.h>
81 
82 #include "httpauthentication.h"
83 
84 // HeaderTokenizer declarations
85 #include "parsinghelpers.h"
86 //string parsing helpers and HeaderTokenizer implementation
87 #include "parsinghelpers.cpp"
88 
89 // KDE5 TODO (QT5) : use QString::htmlEscape or whatever https://qt.gitorious.org/qt/qtbase/merge_requests/56
90 // ends up with.
91 static QString htmlEscape(const QString &plain)
92 {
93  QString rich;
94  rich.reserve(int(plain.length() * 1.1));
95  for (int i = 0; i < plain.length(); ++i) {
96  if (plain.at(i) == QLatin1Char('<'))
97  rich += QLatin1String("&lt;");
98  else if (plain.at(i) == QLatin1Char('>'))
99  rich += QLatin1String("&gt;");
100  else if (plain.at(i) == QLatin1Char('&'))
101  rich += QLatin1String("&amp;");
102  else if (plain.at(i) == QLatin1Char('"'))
103  rich += QLatin1String("&quot;");
104  else
105  rich += plain.at(i);
106  }
107  rich.squeeze();
108  return rich;
109 }
110 
111 static bool supportedProxyScheme(const QString& scheme)
112 {
113  return (scheme.startsWith(QLatin1String("http"), Qt::CaseInsensitive)
114  || scheme == QLatin1String("socks"));
115 }
116 
117 // see filenameFromUrl(): a sha1 hash is 160 bits
118 static const int s_hashedUrlBits = 160; // this number should always be divisible by eight
119 static const int s_hashedUrlNibbles = s_hashedUrlBits / 4;
120 static const int s_hashedUrlBytes = s_hashedUrlBits / 8;
121 static const int s_MaxInMemPostBufSize = 256 * 1024; // Write anyting over 256 KB to file...
122 
123 using namespace KIO;
124 
125 extern "C" int KDE_EXPORT kdemain( int argc, char **argv )
126 {
127  QCoreApplication app( argc, argv ); // needed for QSocketNotifier
128  KComponentData componentData( "kio_http", "kdelibs4" );
129  (void) KGlobal::locale();
130 
131  if (argc != 4)
132  {
133  fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n");
134  exit(-1);
135  }
136 
137  HTTPProtocol slave(argv[1], argv[2], argv[3]);
138  slave.dispatchLoop();
139  return 0;
140 }
141 
142 /*********************************** Generic utility functions ********************/
143 
144 static QString toQString(const QByteArray& value)
145 {
146  return QString::fromLatin1(value.constData(), value.size());
147 }
148 
149 static bool isCrossDomainRequest( const QString& fqdn, const QString& originURL )
150 {
151  //TODO read the RFC
152  if (originURL == QLatin1String("true")) // Backwards compatibility
153  return true;
154 
155  KUrl url ( originURL );
156 
157  // Document Origin domain
158  QString a = url.host();
159  // Current request domain
160  QString b = fqdn;
161 
162  if (a == b)
163  return false;
164 
165  QStringList la = a.split(QLatin1Char('.'), QString::SkipEmptyParts);
166  QStringList lb = b.split(QLatin1Char('.'), QString::SkipEmptyParts);
167 
168  if (qMin(la.count(), lb.count()) < 2) {
169  return true; // better safe than sorry...
170  }
171 
172  while(la.count() > 2)
173  la.pop_front();
174  while(lb.count() > 2)
175  lb.pop_front();
176 
177  return la != lb;
178 }
179 
180 /*
181  Eliminates any custom header that could potentially alter the request
182 */
183 static QString sanitizeCustomHTTPHeader(const QString& _header)
184 {
185  QString sanitizedHeaders;
186  const QStringList headers = _header.split(QRegExp(QLatin1String("[\r\n]")));
187 
188  for(QStringList::ConstIterator it = headers.begin(); it != headers.end(); ++it)
189  {
190  // Do not allow Request line to be specified and ignore
191  // the other HTTP headers.
192  if (!(*it).contains(QLatin1Char(':')) ||
193  (*it).startsWith(QLatin1String("host"), Qt::CaseInsensitive) ||
194  (*it).startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) ||
195  (*it).startsWith(QLatin1String("via"), Qt::CaseInsensitive))
196  continue;
197 
198  sanitizedHeaders += (*it);
199  sanitizedHeaders += QLatin1String("\r\n");
200  }
201  sanitizedHeaders.chop(2);
202 
203  return sanitizedHeaders;
204 }
205 
206 static bool isPotentialSpoofingAttack(const HTTPProtocol::HTTPRequest& request, const KConfigGroup* config)
207 {
208  // kDebug(7113) << request.url << "response code: " << request.responseCode << "previous response code:" << request.prevResponseCode;
209  if (config->readEntry("no-spoof-check", false)) {
210  return false;
211  }
212 
213  if (request.url.user().isEmpty()) {
214  return false;
215  }
216 
217  // NOTE: Workaround for brain dead clients that include "undefined" as
218  // username and password in the request URL (BR# 275033).
219  if (request.url.user() == QLatin1String("undefined") && request.url.pass() == QLatin1String("undefined")) {
220  return false;
221  }
222 
223  // We already have cached authentication.
224  if (config->readEntry(QLatin1String("cached-www-auth"), false)) {
225  return false;
226  }
227 
228  const QString userName = config->readEntry(QLatin1String("LastSpoofedUserName"), QString());
229  return ((userName.isEmpty() || userName != request.url.user()) && request.responseCode != 401 && request.prevResponseCode != 401);
230 }
231 
232 // for a given response code, conclude if the response is going to/likely to have a response body
233 static bool canHaveResponseBody(int responseCode, KIO::HTTP_METHOD method)
234 {
235 /* RFC 2616 says...
236  1xx: false
237  200: method HEAD: false, otherwise:true
238  201: true
239  202: true
240  203: see 200
241  204: false
242  205: false
243  206: true
244  300: see 200
245  301: see 200
246  302: see 200
247  303: see 200
248  304: false
249  305: probably like 300, RFC seems to expect disconnection afterwards...
250  306: (reserved), for simplicity do it just like 200
251  307: see 200
252  4xx: see 200
253  5xx :see 200
254 */
255  if (responseCode >= 100 && responseCode < 200) {
256  return false;
257  }
258  switch (responseCode) {
259  case 201:
260  case 202:
261  case 206:
262  // RFC 2616 does not mention HEAD in the description of the above. if the assert turns out
263  // to be a problem the response code should probably be treated just like 200 and friends.
264  Q_ASSERT(method != HTTP_HEAD);
265  return true;
266  case 204:
267  case 205:
268  case 304:
269  return false;
270  default:
271  break;
272  }
273  // safe (and for most remaining response codes exactly correct) default
274  return method != HTTP_HEAD;
275 }
276 
277 static bool isEncryptedHttpVariety(const QByteArray &p)
278 {
279  return p == "https" || p == "webdavs";
280 }
281 
282 static bool isValidProxy(const KUrl &u)
283 {
284  return u.isValid() && u.hasHost();
285 }
286 
287 static bool isHttpProxy(const KUrl &u)
288 {
289  return isValidProxy(u) && u.protocol() == QLatin1String("http");
290 }
291 
292 static QIODevice* createPostBufferDeviceFor (KIO::filesize_t size)
293 {
294  QIODevice* device;
295  if (size > static_cast<KIO::filesize_t>(s_MaxInMemPostBufSize))
296  device = new KTemporaryFile;
297  else
298  device = new QBuffer;
299 
300  if (!device->open(QIODevice::ReadWrite))
301  return 0;
302 
303  return device;
304 }
305 
306 QByteArray HTTPProtocol::HTTPRequest::methodString() const
307 {
308  if (!methodStringOverride.isEmpty())
309  return (methodStringOverride + QLatin1Char(' ')).toLatin1();
310 
311  switch(method) {
312  case HTTP_GET:
313  return "GET ";
314  case HTTP_PUT:
315  return "PUT ";
316  case HTTP_POST:
317  return "POST ";
318  case HTTP_HEAD:
319  return "HEAD ";
320  case HTTP_DELETE:
321  return "DELETE ";
322  case HTTP_OPTIONS:
323  return "OPTIONS ";
324  case DAV_PROPFIND:
325  return "PROPFIND ";
326  case DAV_PROPPATCH:
327  return "PROPPATCH ";
328  case DAV_MKCOL:
329  return "MKCOL ";
330  case DAV_COPY:
331  return "COPY ";
332  case DAV_MOVE:
333  return "MOVE ";
334  case DAV_LOCK:
335  return "LOCK ";
336  case DAV_UNLOCK:
337  return "UNLOCK ";
338  case DAV_SEARCH:
339  return "SEARCH ";
340  case DAV_SUBSCRIBE:
341  return "SUBSCRIBE ";
342  case DAV_UNSUBSCRIBE:
343  return "UNSUBSCRIBE ";
344  case DAV_POLL:
345  return "POLL ";
346  case DAV_NOTIFY:
347  return "NOTIFY ";
348  case DAV_REPORT:
349  return "REPORT ";
350  default:
351  Q_ASSERT(false);
352  return QByteArray();
353  }
354 }
355 
356 static QString formatHttpDate(qint64 date)
357 {
358  KDateTime dt;
359  dt.setTime_t(date);
360  QString ret = dt.toString(KDateTime::RFCDateDay);
361  ret.chop(6); // remove " +0000"
362  // RFCDate[Day] omits the second if zero, but HTTP requires it; see bug 240585.
363  if (!dt.time().second()) {
364  ret.append(QLatin1String(":00"));
365  }
366  ret.append(QLatin1String(" GMT"));
367  return ret;
368 }
369 
370 static bool isAuthenticationRequired(int responseCode)
371 {
372  return (responseCode == 401) || (responseCode == 407);
373 }
374 
375 #define NO_SIZE ((KIO::filesize_t) -1)
376 
377 #ifdef HAVE_STRTOLL
378 #define STRTOLL strtoll
379 #else
380 #define STRTOLL strtol
381 #endif
382 
383 
384 /************************************** HTTPProtocol **********************************************/
385 
386 
387 HTTPProtocol::HTTPProtocol( const QByteArray &protocol, const QByteArray &pool,
388  const QByteArray &app )
389  : TCPSlaveBase(protocol, pool, app, isEncryptedHttpVariety(protocol))
390  , m_iSize(NO_SIZE)
391  , m_iPostDataSize(NO_SIZE)
392  , m_isBusy(false)
393  , m_POSTbuf(0)
394  , m_maxCacheAge(DEFAULT_MAX_CACHE_AGE)
395  , m_maxCacheSize(DEFAULT_MAX_CACHE_SIZE)
396  , m_protocol(protocol)
397  , m_wwwAuth(0)
398  , m_proxyAuth(0)
399  , m_socketProxyAuth(0)
400  , m_iError(0)
401  , m_isLoadingErrorPage(false)
402  , m_remoteRespTimeout(DEFAULT_RESPONSE_TIMEOUT)
403 {
404  reparseConfiguration();
405  setBlocking(true);
406  connect(socket(), SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
407  this, SLOT(proxyAuthenticationForSocket(QNetworkProxy,QAuthenticator*)));
408 }
409 
410 HTTPProtocol::~HTTPProtocol()
411 {
412  httpClose(false);
413 }
414 
415 void HTTPProtocol::reparseConfiguration()
416 {
417  kDebug(7113);
418 
419  delete m_proxyAuth;
420  delete m_wwwAuth;
421  m_proxyAuth = 0;
422  m_wwwAuth = 0;
423  m_request.proxyUrl.clear(); //TODO revisit
424  m_request.proxyUrls.clear();
425 
426  TCPSlaveBase::reparseConfiguration();
427 }
428 
429 void HTTPProtocol::resetConnectionSettings()
430 {
431  m_isEOF = false;
432  m_iError = 0;
433  m_isLoadingErrorPage = false;
434 }
435 
436 quint16 HTTPProtocol::defaultPort() const
437 {
438  return isEncryptedHttpVariety(m_protocol) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
439 }
440 
441 void HTTPProtocol::resetResponseParsing()
442 {
443  m_isRedirection = false;
444  m_isChunked = false;
445  m_iSize = NO_SIZE;
446  clearUnreadBuffer();
447 
448  m_responseHeaders.clear();
449  m_contentEncodings.clear();
450  m_transferEncodings.clear();
451  m_contentMD5.clear();
452  m_mimeType.clear();
453 
454  setMetaData(QLatin1String("request-id"), m_request.id);
455 }
456 
457 void HTTPProtocol::resetSessionSettings()
458 {
459  // Follow HTTP/1.1 spec and enable keep-alive by default
460  // unless the remote side tells us otherwise or we determine
461  // the persistent link has been terminated by the remote end.
462  m_request.isKeepAlive = true;
463  m_request.keepAliveTimeout = 0;
464 
465  m_request.redirectUrl = KUrl();
466  m_request.useCookieJar = config()->readEntry("Cookies", false);
467  m_request.cacheTag.useCache = config()->readEntry("UseCache", true);
468  m_request.preferErrorPage = config()->readEntry("errorPage", true);
469  const bool noAuth = config()->readEntry("no-auth", false);
470  m_request.doNotWWWAuthenticate = config()->readEntry("no-www-auth", noAuth);
471  m_request.doNotProxyAuthenticate = config()->readEntry("no-proxy-auth", noAuth);
472  m_strCacheDir = config()->readPathEntry("CacheDir", QString());
473  m_maxCacheAge = config()->readEntry("MaxCacheAge", DEFAULT_MAX_CACHE_AGE);
474  m_request.windowId = config()->readEntry("window-id");
475 
476  m_request.methodStringOverride = metaData(QLatin1String("CustomHTTPMethod"));
477 
478  kDebug(7113) << "Window Id =" << m_request.windowId;
479  kDebug(7113) << "ssl_was_in_use =" << metaData(QLatin1String("ssl_was_in_use"));
480 
481  m_request.referrer.clear();
482  // RFC 2616: do not send the referrer if the referrer page was served using SSL and
483  // the current page does not use SSL.
484  if ( config()->readEntry("SendReferrer", true) &&
485  (isEncryptedHttpVariety(m_protocol) || metaData(QLatin1String("ssl_was_in_use")) != QLatin1String("TRUE") ) )
486  {
487  KUrl refUrl(metaData(QLatin1String("referrer")));
488  if (refUrl.isValid()) {
489  // Sanitize
490  QString protocol = refUrl.protocol();
491  if (protocol.startsWith(QLatin1String("webdav"))) {
492  protocol.replace(0, 6, QLatin1String("http"));
493  refUrl.setProtocol(protocol);
494  }
495 
496  if (protocol.startsWith(QLatin1String("http"))) {
497  m_request.referrer = toQString(refUrl.toEncoded(QUrl::RemoveUserInfo | QUrl::RemoveFragment));
498  }
499  }
500  }
501 
502  if (config()->readEntry("SendLanguageSettings", true)) {
503  m_request.charsets = config()->readEntry("Charsets", DEFAULT_PARTIAL_CHARSET_HEADER);
504  if (!m_request.charsets.contains(QLatin1String("*;"), Qt::CaseInsensitive)) {
505  m_request.charsets += QLatin1String(",*;q=0.5");
506  }
507  m_request.languages = config()->readEntry("Languages", DEFAULT_LANGUAGE_HEADER);
508  } else {
509  m_request.charsets.clear();
510  m_request.languages.clear();
511  }
512 
513  // Adjust the offset value based on the "resume" meta-data.
514  QString resumeOffset = metaData(QLatin1String("resume"));
515  if (!resumeOffset.isEmpty()) {
516  m_request.offset = resumeOffset.toULongLong();
517  } else {
518  m_request.offset = 0;
519  }
520  // Same procedure for endoffset.
521  QString resumeEndOffset = metaData(QLatin1String("resume_until"));
522  if (!resumeEndOffset.isEmpty()) {
523  m_request.endoffset = resumeEndOffset.toULongLong();
524  } else {
525  m_request.endoffset = 0;
526  }
527 
528  m_request.disablePassDialog = config()->readEntry("DisablePassDlg", false);
529  m_request.allowTransferCompression = config()->readEntry("AllowCompressedPage", true);
530  m_request.id = metaData(QLatin1String("request-id"));
531 
532  // Store user agent for this host.
533  if (config()->readEntry("SendUserAgent", true)) {
534  m_request.userAgent = metaData(QLatin1String("UserAgent"));
535  } else {
536  m_request.userAgent.clear();
537  }
538 
539  m_request.cacheTag.etag.clear();
540  // -1 is also the value returned by KDateTime::toTime_t() from an invalid instance.
541  m_request.cacheTag.servedDate = -1;
542  m_request.cacheTag.lastModifiedDate = -1;
543  m_request.cacheTag.expireDate = -1;
544 
545  m_request.responseCode = 0;
546  m_request.prevResponseCode = 0;
547 
548  delete m_wwwAuth;
549  m_wwwAuth = 0;
550  delete m_socketProxyAuth;
551  m_socketProxyAuth = 0;
552 
553  // Obtain timeout values
554  m_remoteRespTimeout = responseTimeout();
555 
556  // Bounce back the actual referrer sent
557  setMetaData(QLatin1String("referrer"), m_request.referrer);
558 
559  // Reset the post data size
560  m_iPostDataSize = NO_SIZE;
561 }
562 
563 void HTTPProtocol::setHost( const QString& host, quint16 port,
564  const QString& user, const QString& pass )
565 {
566  // Reset the webdav-capable flags for this host
567  if ( m_request.url.host() != host )
568  m_davHostOk = m_davHostUnsupported = false;
569 
570  m_request.url.setHost(host);
571 
572  // is it an IPv6 address?
573  if (host.indexOf(QLatin1Char(':')) == -1) {
574  m_request.encoded_hostname = toQString(QUrl::toAce(host));
575  } else {
576  int pos = host.indexOf(QLatin1Char('%'));
577  if (pos == -1)
578  m_request.encoded_hostname = QLatin1Char('[') + host + QLatin1Char(']');
579  else
580  // don't send the scope-id in IPv6 addresses to the server
581  m_request.encoded_hostname = QLatin1Char('[') + host.left(pos) + QLatin1Char(']');
582  }
583  m_request.url.setPort((port > 0 && port != defaultPort()) ? port : -1);
584  m_request.url.setUser(user);
585  m_request.url.setPass(pass);
586 
587  // On new connection always clear previous proxy information...
588  m_request.proxyUrl.clear();
589  m_request.proxyUrls.clear();
590 
591  kDebug(7113) << "Hostname is now:" << m_request.url.host()
592  << "(" << m_request.encoded_hostname << ")";
593 }
594 
595 bool HTTPProtocol::maybeSetRequestUrl(const KUrl &u)
596 {
597  kDebug(7113) << u;
598 
599  m_request.url = u;
600  m_request.url.setPort(u.port(defaultPort()) != defaultPort() ? u.port() : -1);
601 
602  if (u.host().isEmpty()) {
603  error( KIO::ERR_UNKNOWN_HOST, i18n("No host specified."));
604  return false;
605  }
606 
607  if (u.path().isEmpty()) {
608  KUrl newUrl(u);
609  newUrl.setPath(QLatin1String("/"));
610  redirection(newUrl);
611  finished();
612  return false;
613  }
614 
615  return true;
616 }
617 
618 void HTTPProtocol::proceedUntilResponseContent( bool dataInternal /* = false */ )
619 {
620  kDebug (7113);
621 
622  const bool status = (proceedUntilResponseHeader() && readBody(dataInternal));
623 
624  // If not an error condition or internal request, close
625  // the connection based on the keep alive settings...
626  if (!m_iError && !dataInternal) {
627  httpClose(m_request.isKeepAlive);
628  }
629 
630  // if data is required internally or we got error, don't finish,
631  // it is processed before we finish()
632  if (dataInternal || !status) {
633  return;
634  }
635 
636  if (!sendHttpError()) {
637  finished();
638  }
639 }
640 
641 bool HTTPProtocol::proceedUntilResponseHeader()
642 {
643  kDebug (7113);
644 
645  // Retry the request until it succeeds or an unrecoverable error occurs.
646  // Recoverable errors are, for example:
647  // - Proxy or server authentication required: Ask for credentials and try again,
648  // this time with an authorization header in the request.
649  // - Server-initiated timeout on keep-alive connection: Reconnect and try again
650 
651  while (true) {
652  if (!sendQuery()) {
653  return false;
654  }
655  if (readResponseHeader()) {
656  // Success, finish the request.
657  break;
658  }
659 
660  // If not loading error page and the response code requires us to resend the query,
661  // then throw away any error message that might have been sent by the server.
662  if (!m_isLoadingErrorPage && isAuthenticationRequired(m_request.responseCode)) {
663  // This gets rid of any error page sent with 401 or 407 authentication required response...
664  readBody(true);
665  }
666 
667  // no success, close the cache file so the cache state is reset - that way most other code
668  // doesn't have to deal with the cache being in various states.
669  cacheFileClose();
670  if (m_iError || m_isLoadingErrorPage) {
671  // Unrecoverable error, abort everything.
672  // Also, if we've just loaded an error page there is nothing more to do.
673  // In that case we abort to avoid loops; some webservers manage to send 401 and
674  // no authentication request. Or an auth request we don't understand.
675  return false;
676  }
677 
678  if (!m_request.isKeepAlive) {
679  httpCloseConnection();
680  m_request.isKeepAlive = true;
681  m_request.keepAliveTimeout = 0;
682  }
683  }
684 
685  // Do not save authorization if the current response code is
686  // 4xx (client error) or 5xx (server error).
687  kDebug(7113) << "Previous Response:" << m_request.prevResponseCode;
688  kDebug(7113) << "Current Response:" << m_request.responseCode;
689 
690  setMetaData(QLatin1String("responsecode"), QString::number(m_request.responseCode));
691  setMetaData(QLatin1String("content-type"), m_mimeType);
692 
693  // At this point sendBody() should have delivered any POST data.
694  clearPostDataBuffer();
695 
696  return true;
697 }
698 
699 void HTTPProtocol::stat(const KUrl& url)
700 {
701  kDebug(7113) << url;
702 
703  if (!maybeSetRequestUrl(url))
704  return;
705  resetSessionSettings();
706 
707  if ( m_protocol != "webdav" && m_protocol != "webdavs" )
708  {
709  QString statSide = metaData(QLatin1String("statSide"));
710  if (statSide != QLatin1String("source"))
711  {
712  // When uploading we assume the file doesn't exit
713  error( ERR_DOES_NOT_EXIST, url.prettyUrl() );
714  return;
715  }
716 
717  // When downloading we assume it exists
718  UDSEntry entry;
719  entry.insert( KIO::UDSEntry::UDS_NAME, url.fileName() );
720  entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG ); // a file
721  entry.insert( KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH ); // readable by everybody
722 
723  statEntry( entry );
724  finished();
725  return;
726  }
727 
728  davStatList( url );
729 }
730 
731 void HTTPProtocol::listDir( const KUrl& url )
732 {
733  kDebug(7113) << url;
734 
735  if (!maybeSetRequestUrl(url))
736  return;
737  resetSessionSettings();
738 
739  davStatList( url, false );
740 }
741 
742 void HTTPProtocol::davSetRequest( const QByteArray& requestXML )
743 {
744  // insert the document into the POST buffer, kill trailing zero byte
745  cachePostData(requestXML);
746 }
747 
748 void HTTPProtocol::davStatList( const KUrl& url, bool stat )
749 {
750  UDSEntry entry;
751 
752  // check to make sure this host supports WebDAV
753  if ( !davHostOk() )
754  return;
755 
756  // Maybe it's a disguised SEARCH...
757  QString query = metaData(QLatin1String("davSearchQuery"));
758  if ( !query.isEmpty() )
759  {
760  QByteArray request = "<?xml version=\"1.0\"?>\r\n";
761  request.append( "<D:searchrequest xmlns:D=\"DAV:\">\r\n" );
762  request.append( query.toUtf8() );
763  request.append( "</D:searchrequest>\r\n" );
764 
765  davSetRequest( request );
766  } else {
767  // We are only after certain features...
768  QByteArray request;
769  request = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
770  "<D:propfind xmlns:D=\"DAV:\">";
771 
772  // insert additional XML request from the davRequestResponse metadata
773  if ( hasMetaData(QLatin1String("davRequestResponse")) )
774  request += metaData(QLatin1String("davRequestResponse")).toUtf8();
775  else {
776  // No special request, ask for default properties
777  request += "<D:prop>"
778  "<D:creationdate/>"
779  "<D:getcontentlength/>"
780  "<D:displayname/>"
781  "<D:source/>"
782  "<D:getcontentlanguage/>"
783  "<D:getcontenttype/>"
784  "<D:getlastmodified/>"
785  "<D:getetag/>"
786  "<D:supportedlock/>"
787  "<D:lockdiscovery/>"
788  "<D:resourcetype/>"
789  "</D:prop>";
790  }
791  request += "</D:propfind>";
792 
793  davSetRequest( request );
794  }
795 
796  // WebDAV Stat or List...
797  m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH;
798  m_request.url.setQuery(QString());
799  m_request.cacheTag.policy = CC_Reload;
800  m_request.davData.depth = stat ? 0 : 1;
801  if (!stat)
802  m_request.url.adjustPath(KUrl::AddTrailingSlash);
803 
804  proceedUntilResponseContent( true );
805  infoMessage(QLatin1String(""));
806 
807  // Has a redirection already been called? If so, we're done.
808  if (m_isRedirection || m_iError) {
809  if (m_isRedirection) {
810  davFinished();
811  }
812  return;
813  }
814 
815  QDomDocument multiResponse;
816  multiResponse.setContent( m_webDavDataBuf, true );
817 
818  bool hasResponse = false;
819 
820  // kDebug(7113) << endl << multiResponse.toString(2);
821 
822  for ( QDomNode n = multiResponse.documentElement().firstChild();
823  !n.isNull(); n = n.nextSibling()) {
824  QDomElement thisResponse = n.toElement();
825  if (thisResponse.isNull())
826  continue;
827 
828  hasResponse = true;
829 
830  QDomElement href = thisResponse.namedItem(QLatin1String("href")).toElement();
831  if ( !href.isNull() ) {
832  entry.clear();
833 
834  QString urlStr = QUrl::fromPercentEncoding(href.text().toUtf8());
835 #if 0 // qt4/kde4 say: it's all utf8...
836  int encoding = remoteEncoding()->encodingMib();
837  if ((encoding == 106) && (!KStringHandler::isUtf8(KUrl::decode_string(urlStr, 4).toLatin1())))
838  encoding = 4; // Use latin1 if the file is not actually utf-8
839 
840  KUrl thisURL ( urlStr, encoding );
841 #else
842  KUrl thisURL( urlStr );
843 #endif
844 
845  if ( thisURL.isValid() ) {
846  QString name = thisURL.fileName();
847 
848  // base dir of a listDir(): name should be "."
849  if ( !stat && thisURL.path(KUrl::AddTrailingSlash).length() == url.path(KUrl::AddTrailingSlash).length() )
850  name = QLatin1Char('.');
851 
852  entry.insert( KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name );
853  }
854 
855  QDomNodeList propstats = thisResponse.elementsByTagName(QLatin1String("propstat"));
856 
857  davParsePropstats( propstats, entry );
858 
859  // Since a lot of webdav servers seem not to send the content-type information
860  // for the requested directory listings, we attempt to guess the mime-type from
861  // the resource name so long as the resource is not a directory.
862  if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() &&
863  entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) {
864  int accuracy = 0;
865  KMimeType::Ptr mime = KMimeType::findByUrl(thisURL.fileName(), 0, false, true, &accuracy);
866  if (mime && !mime->isDefault() && accuracy == 100) {
867  kDebug(7113) << "Setting" << mime->name() << "as guessed mime type for" << thisURL.fileName();
868  entry.insert( KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime->name());
869  }
870  }
871 
872  if ( stat ) {
873  // return an item
874  statEntry( entry );
875  davFinished();
876  return;
877  }
878 
879  listEntry( entry, false );
880  } else {
881  kDebug(7113) << "Error: no URL contained in response to PROPFIND on" << url;
882  }
883  }
884 
885  if ( stat || !hasResponse ) {
886  error( ERR_DOES_NOT_EXIST, url.prettyUrl() );
887  return;
888  }
889 
890  listEntry( entry, true );
891  davFinished();
892 }
893 
894 void HTTPProtocol::davGeneric( const KUrl& url, KIO::HTTP_METHOD method, qint64 size )
895 {
896  kDebug(7113) << url;
897 
898  if (!maybeSetRequestUrl(url))
899  return;
900  resetSessionSettings();
901 
902  // check to make sure this host supports WebDAV
903  if ( !davHostOk() )
904  return;
905 
906  // WebDAV method
907  m_request.method = method;
908  m_request.url.setQuery(QString());
909  m_request.cacheTag.policy = CC_Reload;
910 
911  m_iPostDataSize = (size > -1 ? static_cast<KIO::filesize_t>(size) : NO_SIZE);
912  proceedUntilResponseContent();
913 }
914 
915 int HTTPProtocol::codeFromResponse( const QString& response )
916 {
917  const int firstSpace = response.indexOf( QLatin1Char(' ') );
918  const int secondSpace = response.indexOf( QLatin1Char(' '), firstSpace + 1 );
919  return response.mid( firstSpace + 1, secondSpace - firstSpace - 1 ).toInt();
920 }
921 
922 void HTTPProtocol::davParsePropstats( const QDomNodeList& propstats, UDSEntry& entry )
923 {
924  QString mimeType;
925  bool foundExecutable = false;
926  bool isDirectory = false;
927  uint lockCount = 0;
928  uint supportedLockCount = 0;
929 
930  for ( int i = 0; i < propstats.count(); i++)
931  {
932  QDomElement propstat = propstats.item(i).toElement();
933 
934  QDomElement status = propstat.namedItem(QLatin1String("status")).toElement();
935  if ( status.isNull() )
936  {
937  // error, no status code in this propstat
938  kDebug(7113) << "Error, no status code in this propstat";
939  return;
940  }
941 
942  int code = codeFromResponse( status.text() );
943 
944  if ( code != 200 )
945  {
946  kDebug(7113) << "Got status code" << code << "(this may mean that some properties are unavailable)";
947  continue;
948  }
949 
950  QDomElement prop = propstat.namedItem( QLatin1String("prop") ).toElement();
951  if ( prop.isNull() )
952  {
953  kDebug(7113) << "Error: no prop segment in this propstat.";
954  return;
955  }
956 
957  if ( hasMetaData( QLatin1String("davRequestResponse") ) )
958  {
959  QDomDocument doc;
960  doc.appendChild(prop);
961  entry.insert( KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString() );
962  }
963 
964  for ( QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling() )
965  {
966  QDomElement property = n.toElement();
967  if (property.isNull())
968  continue;
969 
970  if ( property.namespaceURI() != QLatin1String("DAV:") )
971  {
972  // break out - we're only interested in properties from the DAV namespace
973  continue;
974  }
975 
976  if ( property.tagName() == QLatin1String("creationdate") )
977  {
978  // Resource creation date. Should be is ISO 8601 format.
979  entry.insert( KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime( property.text(), property.attribute(QLatin1String("dt")) ) );
980  }
981  else if ( property.tagName() == QLatin1String("getcontentlength") )
982  {
983  // Content length (file size)
984  entry.insert( KIO::UDSEntry::UDS_SIZE, property.text().toULong() );
985  }
986  else if ( property.tagName() == QLatin1String("displayname") )
987  {
988  // Name suitable for presentation to the user
989  setMetaData( QLatin1String("davDisplayName"), property.text() );
990  }
991  else if ( property.tagName() == QLatin1String("source") )
992  {
993  // Source template location
994  QDomElement source = property.namedItem( QLatin1String("link") ).toElement()
995  .namedItem( QLatin1String("dst") ).toElement();
996  if ( !source.isNull() )
997  setMetaData( QLatin1String("davSource"), source.text() );
998  }
999  else if ( property.tagName() == QLatin1String("getcontentlanguage") )
1000  {
1001  // equiv. to Content-Language header on a GET
1002  setMetaData( QLatin1String("davContentLanguage"), property.text() );
1003  }
1004  else if ( property.tagName() == QLatin1String("getcontenttype") )
1005  {
1006  // Content type (mime type)
1007  // This may require adjustments for other server-side webdav implementations
1008  // (tested with Apache + mod_dav 1.0.3)
1009  if ( property.text() == QLatin1String("httpd/unix-directory") )
1010  {
1011  isDirectory = true;
1012  }
1013  else
1014  {
1015  mimeType = property.text();
1016  }
1017  }
1018  else if ( property.tagName() == QLatin1String("executable") )
1019  {
1020  // File executable status
1021  if ( property.text() == QLatin1String("T") )
1022  foundExecutable = true;
1023 
1024  }
1025  else if ( property.tagName() == QLatin1String("getlastmodified") )
1026  {
1027  // Last modification date
1028  entry.insert( KIO::UDSEntry::UDS_MODIFICATION_TIME, parseDateTime( property.text(), property.attribute(QLatin1String("dt")) ) );
1029  }
1030  else if ( property.tagName() == QLatin1String("getetag") )
1031  {
1032  // Entity tag
1033  setMetaData( QLatin1String("davEntityTag"), property.text() );
1034  }
1035  else if ( property.tagName() == QLatin1String("supportedlock") )
1036  {
1037  // Supported locking specifications
1038  for ( QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling() )
1039  {
1040  QDomElement lockEntry = n2.toElement();
1041  if ( lockEntry.tagName() == QLatin1String("lockentry") )
1042  {
1043  QDomElement lockScope = lockEntry.namedItem( QLatin1String("lockscope") ).toElement();
1044  QDomElement lockType = lockEntry.namedItem( QLatin1String("locktype") ).toElement();
1045  if ( !lockScope.isNull() && !lockType.isNull() )
1046  {
1047  // Lock type was properly specified
1048  supportedLockCount++;
1049  const QString lockCountStr = QString::number(supportedLockCount);
1050  const QString scope = lockScope.firstChild().toElement().tagName();
1051  const QString type = lockType.firstChild().toElement().tagName();
1052 
1053  setMetaData( QLatin1String("davSupportedLockScope") + lockCountStr, scope );
1054  setMetaData( QLatin1String("davSupportedLockType") + lockCountStr, type );
1055  }
1056  }
1057  }
1058  }
1059  else if ( property.tagName() == QLatin1String("lockdiscovery") )
1060  {
1061  // Lists the available locks
1062  davParseActiveLocks( property.elementsByTagName( QLatin1String("activelock") ), lockCount );
1063  }
1064  else if ( property.tagName() == QLatin1String("resourcetype") )
1065  {
1066  // Resource type. "Specifies the nature of the resource."
1067  if ( !property.namedItem( QLatin1String("collection") ).toElement().isNull() )
1068  {
1069  // This is a collection (directory)
1070  isDirectory = true;
1071  }
1072  }
1073  else
1074  {
1075  kDebug(7113) << "Found unknown webdav property:" << property.tagName();
1076  }
1077  }
1078  }
1079 
1080  setMetaData( QLatin1String("davLockCount"), QString::number(lockCount) );
1081  setMetaData( QLatin1String("davSupportedLockCount"), QString::number(supportedLockCount) );
1082 
1083  entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG );
1084 
1085  if ( foundExecutable || isDirectory )
1086  {
1087  // File was executable, or is a directory.
1088  entry.insert( KIO::UDSEntry::UDS_ACCESS, 0700 );
1089  }
1090  else
1091  {
1092  entry.insert( KIO::UDSEntry::UDS_ACCESS, 0600 );
1093  }
1094 
1095  if ( !isDirectory && !mimeType.isEmpty() )
1096  {
1097  entry.insert( KIO::UDSEntry::UDS_MIME_TYPE, mimeType );
1098  }
1099 }
1100 
1101 void HTTPProtocol::davParseActiveLocks( const QDomNodeList& activeLocks,
1102  uint& lockCount )
1103 {
1104  for ( int i = 0; i < activeLocks.count(); i++ )
1105  {
1106  const QDomElement activeLock = activeLocks.item(i).toElement();
1107 
1108  lockCount++;
1109  // required
1110  const QDomElement lockScope = activeLock.namedItem( QLatin1String("lockscope") ).toElement();
1111  const QDomElement lockType = activeLock.namedItem( QLatin1String("locktype") ).toElement();
1112  const QDomElement lockDepth = activeLock.namedItem( QLatin1String("depth") ).toElement();
1113  // optional
1114  const QDomElement lockOwner = activeLock.namedItem( QLatin1String("owner") ).toElement();
1115  const QDomElement lockTimeout = activeLock.namedItem( QLatin1String("timeout") ).toElement();
1116  const QDomElement lockToken = activeLock.namedItem( QLatin1String("locktoken") ).toElement();
1117 
1118  if ( !lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull() )
1119  {
1120  // lock was properly specified
1121  lockCount++;
1122  const QString lockCountStr = QString::number(lockCount);
1123  const QString scope = lockScope.firstChild().toElement().tagName();
1124  const QString type = lockType.firstChild().toElement().tagName();
1125  const QString depth = lockDepth.text();
1126 
1127  setMetaData( QLatin1String("davLockScope") + lockCountStr, scope );
1128  setMetaData( QLatin1String("davLockType") + lockCountStr, type );
1129  setMetaData( QLatin1String("davLockDepth") + lockCountStr, depth );
1130 
1131  if ( !lockOwner.isNull() )
1132  setMetaData( QLatin1String("davLockOwner") + lockCountStr, lockOwner.text() );
1133 
1134  if ( !lockTimeout.isNull() )
1135  setMetaData( QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text() );
1136 
1137  if ( !lockToken.isNull() )
1138  {
1139  QDomElement tokenVal = lockScope.namedItem( QLatin1String("href") ).toElement();
1140  if ( !tokenVal.isNull() )
1141  setMetaData( QLatin1String("davLockToken") + lockCountStr, tokenVal.text() );
1142  }
1143  }
1144  }
1145 }
1146 
1147 long HTTPProtocol::parseDateTime( const QString& input, const QString& type )
1148 {
1149  if ( type == QLatin1String("dateTime.tz") )
1150  {
1151  return KDateTime::fromString( input, KDateTime::ISODate ).toTime_t();
1152  }
1153  else if ( type == QLatin1String("dateTime.rfc1123") )
1154  {
1155  return KDateTime::fromString( input, KDateTime::RFCDate ).toTime_t();
1156  }
1157 
1158  // format not advertised... try to parse anyway
1159  time_t time = KDateTime::fromString( input, KDateTime::RFCDate ).toTime_t();
1160  if ( time != 0 )
1161  return time;
1162 
1163  return KDateTime::fromString( input, KDateTime::ISODate ).toTime_t();
1164 }
1165 
1166 QString HTTPProtocol::davProcessLocks()
1167 {
1168  if ( hasMetaData( QLatin1String("davLockCount") ) )
1169  {
1170  QString response = QLatin1String("If:");
1171  int numLocks = metaData( QLatin1String("davLockCount") ).toInt();
1172  bool bracketsOpen = false;
1173  for ( int i = 0; i < numLocks; i++ )
1174  {
1175  const QString countStr = QString::number(i);
1176  if ( hasMetaData( QLatin1String("davLockToken") + countStr ) )
1177  {
1178  if ( hasMetaData( QLatin1String("davLockURL") + countStr ) )
1179  {
1180  if ( bracketsOpen )
1181  {
1182  response += QLatin1Char(')');
1183  bracketsOpen = false;
1184  }
1185  response += QLatin1String(" <") + metaData( QLatin1String("davLockURL") + countStr ) + QLatin1Char('>');
1186  }
1187 
1188  if ( !bracketsOpen )
1189  {
1190  response += QLatin1String(" (");
1191  bracketsOpen = true;
1192  }
1193  else
1194  {
1195  response += QLatin1Char(' ');
1196  }
1197 
1198  if ( hasMetaData( QLatin1String("davLockNot") + countStr ) )
1199  response += QLatin1String("Not ");
1200 
1201  response += QLatin1Char('<') + metaData( QLatin1String("davLockToken") + countStr ) + QLatin1Char('>');
1202  }
1203  }
1204 
1205  if ( bracketsOpen )
1206  response += QLatin1Char(')');
1207 
1208  response += QLatin1String("\r\n");
1209  return response;
1210  }
1211 
1212  return QString();
1213 }
1214 
1215 bool HTTPProtocol::davHostOk()
1216 {
1217  // FIXME needs to be reworked. Switched off for now.
1218  return true;
1219 
1220  // cached?
1221  if ( m_davHostOk )
1222  {
1223  kDebug(7113) << "true";
1224  return true;
1225  }
1226  else if ( m_davHostUnsupported )
1227  {
1228  kDebug(7113) << " false";
1229  davError( -2 );
1230  return false;
1231  }
1232 
1233  m_request.method = HTTP_OPTIONS;
1234 
1235  // query the server's capabilities generally, not for a specific URL
1236  m_request.url.setPath(QLatin1String("*"));
1237  m_request.url.setQuery(QString());
1238  m_request.cacheTag.policy = CC_Reload;
1239 
1240  // clear davVersions variable, which holds the response to the DAV: header
1241  m_davCapabilities.clear();
1242 
1243  proceedUntilResponseHeader();
1244 
1245  if (m_davCapabilities.count())
1246  {
1247  for (int i = 0; i < m_davCapabilities.count(); i++)
1248  {
1249  bool ok;
1250  uint verNo = m_davCapabilities[i].toUInt(&ok);
1251  if (ok && verNo > 0 && verNo < 3)
1252  {
1253  m_davHostOk = true;
1254  kDebug(7113) << "Server supports DAV version" << verNo;
1255  }
1256  }
1257 
1258  if ( m_davHostOk )
1259  return true;
1260  }
1261 
1262  m_davHostUnsupported = true;
1263  davError( -2 );
1264  return false;
1265 }
1266 
1267 // This function is for closing proceedUntilResponseHeader(); requests
1268 // Required because there may or may not be further info expected
1269 void HTTPProtocol::davFinished()
1270 {
1271  // TODO: Check with the DAV extension developers
1272  httpClose(m_request.isKeepAlive);
1273  finished();
1274 }
1275 
1276 void HTTPProtocol::mkdir( const KUrl& url, int )
1277 {
1278  kDebug(7113) << url;
1279 
1280  if (!maybeSetRequestUrl(url))
1281  return;
1282  resetSessionSettings();
1283 
1284  m_request.method = DAV_MKCOL;
1285  m_request.url.setQuery(QString());
1286  m_request.cacheTag.policy = CC_Reload;
1287 
1288  proceedUntilResponseHeader();
1289 
1290  if ( m_request.responseCode == 201 )
1291  davFinished();
1292  else
1293  davError();
1294 }
1295 
1296 void HTTPProtocol::get( const KUrl& url )
1297 {
1298  kDebug(7113) << url;
1299 
1300  if (!maybeSetRequestUrl(url))
1301  return;
1302  resetSessionSettings();
1303 
1304  m_request.method = HTTP_GET;
1305 
1306  QString tmp(metaData(QLatin1String("cache")));
1307  if (!tmp.isEmpty())
1308  m_request.cacheTag.policy = parseCacheControl(tmp);
1309  else
1310  m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL;
1311 
1312  proceedUntilResponseContent();
1313 }
1314 
1315 void HTTPProtocol::put( const KUrl &url, int, KIO::JobFlags flags )
1316 {
1317  kDebug(7113) << url;
1318 
1319  if (!maybeSetRequestUrl(url))
1320  return;
1321 
1322  resetSessionSettings();
1323 
1324  // Webdav hosts are capable of observing overwrite == false
1325  if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings
1326  if (!(flags & KIO::Overwrite)) {
1327  // check to make sure this host supports WebDAV
1328  if (!davHostOk())
1329  return;
1330 
1331  const QByteArray request ("<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
1332  "<D:propfind xmlns:D=\"DAV:\"><D:prop>"
1333  "<D:creationdate/>"
1334  "<D:getcontentlength/>"
1335  "<D:displayname/>"
1336  "<D:resourcetype/>"
1337  "</D:prop></D:propfind>");
1338 
1339  davSetRequest( request );
1340 
1341  // WebDAV Stat or List...
1342  m_request.method = DAV_PROPFIND;
1343  m_request.url.setQuery(QString());
1344  m_request.cacheTag.policy = CC_Reload;
1345  m_request.davData.depth = 0;
1346 
1347  proceedUntilResponseContent(true);
1348 
1349  if (!m_request.isKeepAlive) {
1350  httpCloseConnection(); // close connection if server requested it.
1351  m_request.isKeepAlive = true; // reset the keep alive flag.
1352  }
1353 
1354  if (m_request.responseCode == 207) {
1355  error(ERR_FILE_ALREADY_EXIST, QString());
1356  return;
1357  }
1358 
1359  // force re-authentication...
1360  delete m_wwwAuth;
1361  m_wwwAuth = 0;
1362  }
1363  }
1364 
1365  m_request.method = HTTP_PUT;
1366  m_request.cacheTag.policy = CC_Reload;
1367 
1368  proceedUntilResponseContent();
1369 }
1370 
1371 void HTTPProtocol::copy( const KUrl& src, const KUrl& dest, int, KIO::JobFlags flags )
1372 {
1373  kDebug(7113) << src << "->" << dest;
1374 
1375  if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src))
1376  return;
1377  resetSessionSettings();
1378 
1379  // destination has to be "http(s)://..."
1380  KUrl newDest = dest;
1381  if (newDest.protocol() == QLatin1String("webdavs"))
1382  newDest.setProtocol(QLatin1String("https"));
1383  else if (newDest.protocol() == QLatin1String("webdav"))
1384  newDest.setProtocol(QLatin1String("http"));
1385 
1386  m_request.method = DAV_COPY;
1387  m_request.davData.desturl = newDest.url();
1388  m_request.davData.overwrite = (flags & KIO::Overwrite);
1389  m_request.url.setQuery(QString());
1390  m_request.cacheTag.policy = CC_Reload;
1391 
1392  proceedUntilResponseHeader();
1393 
1394  // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion
1395  if ( m_request.responseCode == 201 || m_request.responseCode == 204 )
1396  davFinished();
1397  else
1398  davError();
1399 }
1400 
1401 void HTTPProtocol::rename( const KUrl& src, const KUrl& dest, KIO::JobFlags flags )
1402 {
1403  kDebug(7113) << src << "->" << dest;
1404 
1405  if (!maybeSetRequestUrl(dest) || !maybeSetRequestUrl(src))
1406  return;
1407  resetSessionSettings();
1408 
1409  // destination has to be "http://..."
1410  KUrl newDest = dest;
1411  if (newDest.protocol() == QLatin1String("webdavs"))
1412  newDest.setProtocol(QLatin1String("https"));
1413  else if (newDest.protocol() == QLatin1String("webdav"))
1414  newDest.setProtocol(QLatin1String("http"));
1415 
1416  m_request.method = DAV_MOVE;
1417  m_request.davData.desturl = newDest.url();
1418  m_request.davData.overwrite = (flags & KIO::Overwrite);
1419  m_request.url.setQuery(QString());
1420  m_request.cacheTag.policy = CC_Reload;
1421 
1422  proceedUntilResponseHeader();
1423 
1424  // Work around strict Apache-2 WebDAV implementation which refuses to cooperate
1425  // with webdav://host/directory, instead requiring webdav://host/directory/
1426  // (strangely enough it accepts Destination: without a trailing slash)
1427  // See BR# 209508 and BR#187970
1428  if ( m_request.responseCode == 301) {
1429  m_request.url = m_request.redirectUrl;
1430  m_request.method = DAV_MOVE;
1431  m_request.davData.desturl = newDest.url();
1432  m_request.davData.overwrite = (flags & KIO::Overwrite);
1433  m_request.url.setQuery(QString());
1434  m_request.cacheTag.policy = CC_Reload;
1435  // force re-authentication...
1436  delete m_wwwAuth;
1437  m_wwwAuth = 0;
1438  proceedUntilResponseHeader();
1439  }
1440 
1441  if ( m_request.responseCode == 201 )
1442  davFinished();
1443  else
1444  davError();
1445 }
1446 
1447 void HTTPProtocol::del( const KUrl& url, bool )
1448 {
1449  kDebug(7113) << url;
1450 
1451  if (!maybeSetRequestUrl(url))
1452  return;
1453 
1454  resetSessionSettings();
1455 
1456  m_request.method = HTTP_DELETE;
1457  m_request.cacheTag.policy = CC_Reload;
1458 
1459  if (m_protocol.startsWith("webdav")) {
1460  m_request.url.setQuery(QString());
1461  if (!proceedUntilResponseHeader()) {
1462  return;
1463  }
1464 
1465  // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content
1466  // on successful completion.
1467  if ( m_request.responseCode == 200 || m_request.responseCode == 204 || m_isRedirection)
1468  davFinished();
1469  else
1470  davError();
1471 
1472  return;
1473  }
1474 
1475  proceedUntilResponseContent();
1476 }
1477 
1478 void HTTPProtocol::post( const KUrl& url, qint64 size )
1479 {
1480  kDebug(7113) << url;
1481 
1482  if (!maybeSetRequestUrl(url))
1483  return;
1484  resetSessionSettings();
1485 
1486  m_request.method = HTTP_POST;
1487  m_request.cacheTag.policy= CC_Reload;
1488 
1489  m_iPostDataSize = (size > -1 ? static_cast<KIO::filesize_t>(size) : NO_SIZE);
1490  proceedUntilResponseContent();
1491 }
1492 
1493 void HTTPProtocol::davLock( const KUrl& url, const QString& scope,
1494  const QString& type, const QString& owner )
1495 {
1496  kDebug(7113) << url;
1497 
1498  if (!maybeSetRequestUrl(url))
1499  return;
1500  resetSessionSettings();
1501 
1502  m_request.method = DAV_LOCK;
1503  m_request.url.setQuery(QString());
1504  m_request.cacheTag.policy= CC_Reload;
1505 
1506  /* Create appropriate lock XML request. */
1507  QDomDocument lockReq;
1508 
1509  QDomElement lockInfo = lockReq.createElementNS( QLatin1String("DAV:"), QLatin1String("lockinfo") );
1510  lockReq.appendChild( lockInfo );
1511 
1512  QDomElement lockScope = lockReq.createElement( QLatin1String("lockscope") );
1513  lockInfo.appendChild( lockScope );
1514 
1515  lockScope.appendChild( lockReq.createElement( scope ) );
1516 
1517  QDomElement lockType = lockReq.createElement( QLatin1String("locktype") );
1518  lockInfo.appendChild( lockType );
1519 
1520  lockType.appendChild( lockReq.createElement( type ) );
1521 
1522  if ( !owner.isNull() ) {
1523  QDomElement ownerElement = lockReq.createElement( QLatin1String("owner") );
1524  lockReq.appendChild( ownerElement );
1525 
1526  QDomElement ownerHref = lockReq.createElement( QLatin1String("href") );
1527  ownerElement.appendChild( ownerHref );
1528 
1529  ownerHref.appendChild( lockReq.createTextNode( owner ) );
1530  }
1531 
1532  // insert the document into the POST buffer
1533  cachePostData(lockReq.toByteArray());
1534 
1535  proceedUntilResponseContent( true );
1536 
1537  if ( m_request.responseCode == 200 ) {
1538  // success
1539  QDomDocument multiResponse;
1540  multiResponse.setContent( m_webDavDataBuf, true );
1541 
1542  QDomElement prop = multiResponse.documentElement().namedItem( QLatin1String("prop") ).toElement();
1543 
1544  QDomElement lockdiscovery = prop.namedItem( QLatin1String("lockdiscovery") ).toElement();
1545 
1546  uint lockCount = 0;
1547  davParseActiveLocks( lockdiscovery.elementsByTagName( QLatin1String("activelock") ), lockCount );
1548 
1549  setMetaData( QLatin1String("davLockCount"), QString::number( lockCount ) );
1550 
1551  finished();
1552 
1553  } else
1554  davError();
1555 }
1556 
1557 void HTTPProtocol::davUnlock( const KUrl& url )
1558 {
1559  kDebug(7113) << url;
1560 
1561  if (!maybeSetRequestUrl(url))
1562  return;
1563  resetSessionSettings();
1564 
1565  m_request.method = DAV_UNLOCK;
1566  m_request.url.setQuery(QString());
1567  m_request.cacheTag.policy= CC_Reload;
1568 
1569  proceedUntilResponseContent( true );
1570 
1571  if ( m_request.responseCode == 200 )
1572  finished();
1573  else
1574  davError();
1575 }
1576 
1577 QString HTTPProtocol::davError( int code /* = -1 */, const QString &_url )
1578 {
1579  bool callError = false;
1580  if ( code == -1 ) {
1581  code = m_request.responseCode;
1582  callError = true;
1583  }
1584  if ( code == -2 ) {
1585  callError = true;
1586  }
1587 
1588  QString url = _url;
1589  if ( !url.isNull() )
1590  url = m_request.url.url();
1591 
1592  QString action, errorString;
1593  int errorCode = ERR_SLAVE_DEFINED;
1594 
1595  // for 412 Precondition Failed
1596  QString ow = i18n( "Otherwise, the request would have succeeded." );
1597 
1598  switch ( m_request.method ) {
1599  case DAV_PROPFIND:
1600  action = i18nc( "request type", "retrieve property values" );
1601  break;
1602  case DAV_PROPPATCH:
1603  action = i18nc( "request type", "set property values" );
1604  break;
1605  case DAV_MKCOL:
1606  action = i18nc( "request type", "create the requested folder" );
1607  break;
1608  case DAV_COPY:
1609  action = i18nc( "request type", "copy the specified file or folder" );
1610  break;
1611  case DAV_MOVE:
1612  action = i18nc( "request type", "move the specified file or folder" );
1613  break;
1614  case DAV_SEARCH:
1615  action = i18nc( "request type", "search in the specified folder" );
1616  break;
1617  case DAV_LOCK:
1618  action = i18nc( "request type", "lock the specified file or folder" );
1619  break;
1620  case DAV_UNLOCK:
1621  action = i18nc( "request type", "unlock the specified file or folder" );
1622  break;
1623  case HTTP_DELETE:
1624  action = i18nc( "request type", "delete the specified file or folder" );
1625  break;
1626  case HTTP_OPTIONS:
1627  action = i18nc( "request type", "query the server's capabilities" );
1628  break;
1629  case HTTP_GET:
1630  action = i18nc( "request type", "retrieve the contents of the specified file or folder" );
1631  break;
1632  case DAV_REPORT:
1633  action = i18nc( "request type", "run a report in the specified folder" );
1634  break;
1635  case HTTP_PUT:
1636  case HTTP_POST:
1637  case HTTP_HEAD:
1638  default:
1639  // this should not happen, this function is for webdav errors only
1640  Q_ASSERT(0);
1641  }
1642 
1643  // default error message if the following code fails
1644  errorString = i18nc("%1: code, %2: request type", "An unexpected error (%1) occurred "
1645  "while attempting to %2.", code, action);
1646 
1647  switch ( code )
1648  {
1649  case -2:
1650  // internal error: OPTIONS request did not specify DAV compliance
1651  // ERR_UNSUPPORTED_PROTOCOL
1652  errorString = i18n("The server does not support the WebDAV protocol.");
1653  break;
1654  case 207:
1655  // 207 Multi-status
1656  {
1657  // our error info is in the returned XML document.
1658  // retrieve the XML document
1659 
1660  // there was an error retrieving the XML document.
1661  // ironic, eh?
1662  if ( !readBody( true ) && m_iError )
1663  return QString();
1664 
1665  QStringList errors;
1666  QDomDocument multiResponse;
1667 
1668  multiResponse.setContent( m_webDavDataBuf, true );
1669 
1670  QDomElement multistatus = multiResponse.documentElement().namedItem( QLatin1String("multistatus") ).toElement();
1671 
1672  QDomNodeList responses = multistatus.elementsByTagName( QLatin1String("response") );
1673 
1674  for (int i = 0; i < responses.count(); i++)
1675  {
1676  int errCode;
1677  QString errUrl;
1678 
1679  QDomElement response = responses.item(i).toElement();
1680  QDomElement code = response.namedItem( QLatin1String("status") ).toElement();
1681 
1682  if ( !code.isNull() )
1683  {
1684  errCode = codeFromResponse( code.text() );
1685  QDomElement href = response.namedItem( QLatin1String("href") ).toElement();
1686  if ( !href.isNull() )
1687  errUrl = href.text();
1688  errors << davError( errCode, errUrl );
1689  }
1690  }
1691 
1692  //kError = ERR_SLAVE_DEFINED;
1693  errorString = i18nc( "%1: request type, %2: url",
1694  "An error occurred while attempting to %1, %2. A "
1695  "summary of the reasons is below.", action, url );
1696 
1697  errorString += QLatin1String("<ul>");
1698 
1699  Q_FOREACH(const QString& error, errors)
1700  errorString += QLatin1String("<li>") + error + QLatin1String("</li>");
1701 
1702  errorString += QLatin1String("</ul>");
1703  }
1704  case 403:
1705  case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1706  // 403 Forbidden
1707  // ERR_ACCESS_DENIED
1708  errorString = i18nc( "%1: request type", "Access was denied while attempting to %1.", action );
1709  break;
1710  case 405:
1711  // 405 Method Not Allowed
1712  if ( m_request.method == DAV_MKCOL ) {
1713  // ERR_DIR_ALREADY_EXIST
1714  errorString = url;
1715  errorCode = ERR_DIR_ALREADY_EXIST;
1716  }
1717  break;
1718  case 409:
1719  // 409 Conflict
1720  // ERR_ACCESS_DENIED
1721  errorString = i18n("A resource cannot be created at the destination "
1722  "until one or more intermediate collections (folders) "
1723  "have been created.");
1724  break;
1725  case 412:
1726  // 412 Precondition failed
1727  if ( m_request.method == DAV_COPY || m_request.method == DAV_MOVE ) {
1728  // ERR_ACCESS_DENIED
1729  errorString = i18n("The server was unable to maintain the liveness of "
1730  "the properties listed in the propertybehavior XML "
1731  "element or you attempted to overwrite a file while "
1732  "requesting that files are not overwritten. %1",
1733  ow );
1734 
1735  } else if ( m_request.method == DAV_LOCK ) {
1736  // ERR_ACCESS_DENIED
1737  errorString = i18n("The requested lock could not be granted. %1", ow );
1738  }
1739  break;
1740  case 415:
1741  // 415 Unsupported Media Type
1742  // ERR_ACCESS_DENIED
1743  errorString = i18n("The server does not support the request type of the body.");
1744  break;
1745  case 423:
1746  // 423 Locked
1747  // ERR_ACCESS_DENIED
1748  errorString = i18nc( "%1: request type", "Unable to %1 because the resource is locked.", action );
1749  break;
1750  case 425:
1751  // 424 Failed Dependency
1752  errorString = i18n("This action was prevented by another error.");
1753  break;
1754  case 502:
1755  // 502 Bad Gateway
1756  if ( m_request.method == DAV_COPY || m_request.method == DAV_MOVE ) {
1757  // ERR_WRITE_ACCESS_DENIED
1758  errorString = i18nc( "%1: request type", "Unable to %1 because the destination server refuses "
1759  "to accept the file or folder.", action );
1760  }
1761  break;
1762  case 507:
1763  // 507 Insufficient Storage
1764  // ERR_DISK_FULL
1765  errorString = i18n("The destination resource does not have sufficient space "
1766  "to record the state of the resource after the execution "
1767  "of this method.");
1768  break;
1769  default:
1770  break;
1771  }
1772 
1773  // if ( kError != ERR_SLAVE_DEFINED )
1774  //errorString += " (" + url + ')';
1775 
1776  if ( callError )
1777  error( errorCode, errorString );
1778 
1779  return errorString;
1780 }
1781 
1782 // HTTP generic error
1783 static int httpGenericError(const HTTPProtocol::HTTPRequest& request, QString* errorString)
1784 {
1785  Q_ASSERT(errorString);
1786 
1787  int errorCode = 0;
1788  errorString->clear();
1789 
1790  if (request.responseCode == 204) {
1791  errorCode = ERR_NO_CONTENT;
1792  }
1793 
1794  return errorCode;
1795 }
1796 
1797 // HTTP DELETE specific errors
1798 static int httpDelError(const HTTPProtocol::HTTPRequest& request, QString* errorString)
1799 {
1800  Q_ASSERT(errorString);
1801 
1802  int errorCode = 0;
1803  const int responseCode = request.responseCode;
1804  errorString->clear();
1805 
1806  switch (responseCode) {
1807  case 204:
1808  errorCode = ERR_NO_CONTENT;
1809  break;
1810  default:
1811  break;
1812  }
1813 
1814  if (!errorCode
1815  && (responseCode < 200 || responseCode > 400)
1816  && responseCode != 404) {
1817  errorCode = ERR_SLAVE_DEFINED;
1818  *errorString = i18n( "The resource cannot be deleted." );
1819  }
1820 
1821  return errorCode;
1822 }
1823 
1824 // HTTP PUT specific errors
1825 static int httpPutError(const HTTPProtocol::HTTPRequest& request, QString* errorString)
1826 {
1827  Q_ASSERT(errorString);
1828 
1829  int errorCode = 0;
1830  const int responseCode = request.responseCode;
1831  const QString action (i18nc("request type", "upload %1", request.url.prettyUrl()));
1832 
1833  switch (responseCode) {
1834  case 403:
1835  case 405:
1836  case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1837  // 403 Forbidden
1838  // 405 Method Not Allowed
1839  // ERR_ACCESS_DENIED
1840  *errorString = i18nc( "%1: request type", "Access was denied while attempting to %1.", action );
1841  errorCode = ERR_SLAVE_DEFINED;
1842  break;
1843  case 409:
1844  // 409 Conflict
1845  // ERR_ACCESS_DENIED
1846  *errorString = i18n("A resource cannot be created at the destination "
1847  "until one or more intermediate collections (folders) "
1848  "have been created.");
1849  errorCode = ERR_SLAVE_DEFINED;
1850  break;
1851  case 423:
1852  // 423 Locked
1853  // ERR_ACCESS_DENIED
1854  *errorString = i18nc( "%1: request type", "Unable to %1 because the resource is locked.", action );
1855  errorCode = ERR_SLAVE_DEFINED;
1856  break;
1857  case 502:
1858  // 502 Bad Gateway
1859  // ERR_WRITE_ACCESS_DENIED;
1860  *errorString = i18nc( "%1: request type", "Unable to %1 because the destination server refuses "
1861  "to accept the file or folder.", action );
1862  errorCode = ERR_SLAVE_DEFINED;
1863  break;
1864  case 507:
1865  // 507 Insufficient Storage
1866  // ERR_DISK_FULL
1867  *errorString = i18n("The destination resource does not have sufficient space "
1868  "to record the state of the resource after the execution "
1869  "of this method.");
1870  errorCode = ERR_SLAVE_DEFINED;
1871  break;
1872  default:
1873  break;
1874  }
1875 
1876  if (!errorCode
1877  && (responseCode < 200 || responseCode > 400)
1878  && responseCode != 404) {
1879  errorCode = ERR_SLAVE_DEFINED;
1880  *errorString = i18nc("%1: response code, %2: request type",
1881  "An unexpected error (%1) occurred while attempting to %2.",
1882  responseCode, action);
1883  }
1884 
1885  return errorCode;
1886 }
1887 
1888 bool HTTPProtocol::sendHttpError()
1889 {
1890  QString errorString;
1891  int errorCode = 0;
1892 
1893  switch (m_request.method) {
1894  case HTTP_GET:
1895  case HTTP_POST:
1896  errorCode = httpGenericError(m_request, &errorString);
1897  break;
1898  case HTTP_PUT:
1899  errorCode = httpPutError(m_request, &errorString);
1900  break;
1901  case HTTP_DELETE:
1902  errorCode = httpDelError(m_request, &errorString);
1903  break;
1904  default:
1905  break;
1906  }
1907 
1908  // Force any message previously shown by the client to be cleared.
1909  infoMessage(QLatin1String(""));
1910 
1911  if (errorCode) {
1912  error( errorCode, errorString );
1913  return true;
1914  }
1915 
1916  return false;
1917 }
1918 
1919 bool HTTPProtocol::sendErrorPageNotification()
1920 {
1921  if (!m_request.preferErrorPage)
1922  return false;
1923 
1924  if (m_isLoadingErrorPage)
1925  kWarning(7113) << "called twice during one request, something is probably wrong.";
1926 
1927  m_isLoadingErrorPage = true;
1928  SlaveBase::errorPage();
1929  return true;
1930 }
1931 
1932 bool HTTPProtocol::isOffline()
1933 {
1934  // ### TEMPORARY WORKAROUND (While investigating why solid may
1935  // produce false positives)
1936  return false;
1937 
1938  Solid::Networking::Status status = Solid::Networking::status();
1939 
1940  kDebug(7113) << "networkstatus:" << status;
1941 
1942  // on error or unknown, we assume online
1943  return status == Solid::Networking::Unconnected;
1944 }
1945 
1946 void HTTPProtocol::multiGet(const QByteArray &data)
1947 {
1948  QDataStream stream(data);
1949  quint32 n;
1950  stream >> n;
1951 
1952  kDebug(7113) << n;
1953 
1954  HTTPRequest saveRequest;
1955  if (m_isBusy)
1956  saveRequest = m_request;
1957 
1958  resetSessionSettings();
1959 
1960  for (unsigned i = 0; i < n; ++i) {
1961  KUrl url;
1962  stream >> url >> mIncomingMetaData;
1963 
1964  if (!maybeSetRequestUrl(url))
1965  continue;
1966 
1967  //### should maybe call resetSessionSettings() if the server/domain is
1968  // different from the last request!
1969 
1970  kDebug(7113) << url;
1971 
1972  m_request.method = HTTP_GET;
1973  m_request.isKeepAlive = true; //readResponseHeader clears it if necessary
1974 
1975  QString tmp = metaData(QLatin1String("cache"));
1976  if (!tmp.isEmpty())
1977  m_request.cacheTag.policy= parseCacheControl(tmp);
1978  else
1979  m_request.cacheTag.policy= DEFAULT_CACHE_CONTROL;
1980 
1981  m_requestQueue.append(m_request);
1982  }
1983 
1984  if (m_isBusy)
1985  m_request = saveRequest;
1986 #if 0
1987  if (!m_isBusy) {
1988  m_isBusy = true;
1989  QMutableListIterator<HTTPRequest> it(m_requestQueue);
1990  while (it.hasNext()) {
1991  m_request = it.next();
1992  it.remove();
1993  proceedUntilResponseContent();
1994  }
1995  m_isBusy = false;
1996  }
1997 #endif
1998  if (!m_isBusy) {
1999  m_isBusy = true;
2000  QMutableListIterator<HTTPRequest> it(m_requestQueue);
2001  // send the requests
2002  while (it.hasNext()) {
2003  m_request = it.next();
2004  sendQuery();
2005  // save the request state so we can pick it up again in the collection phase
2006  it.setValue(m_request);
2007  kDebug(7113) << "check one: isKeepAlive =" << m_request.isKeepAlive;
2008  if (m_request.cacheTag.ioMode != ReadFromCache) {
2009  m_server.initFrom(m_request);
2010  }
2011  }
2012  // collect the responses
2013  //### for the moment we use a hack: instead of saving and restoring request-id
2014  // we just count up like ParallelGetJobs does.
2015  int requestId = 0;
2016  Q_FOREACH (const HTTPRequest &r, m_requestQueue) {
2017  m_request = r;
2018  kDebug(7113) << "check two: isKeepAlive =" << m_request.isKeepAlive;
2019  setMetaData(QLatin1String("request-id"), QString::number(requestId++));
2020  sendAndKeepMetaData();
2021  if (!(readResponseHeader() && readBody())) {
2022  return;
2023  }
2024  // the "next job" signal for ParallelGetJob is data of size zero which
2025  // readBody() sends without our intervention.
2026  kDebug(7113) << "check three: isKeepAlive =" << m_request.isKeepAlive;
2027  httpClose(m_request.isKeepAlive); //actually keep-alive is mandatory for pipelining
2028  }
2029 
2030  finished();
2031  m_requestQueue.clear();
2032  m_isBusy = false;
2033  }
2034 }
2035 
2036 ssize_t HTTPProtocol::write (const void *_buf, size_t nbytes)
2037 {
2038  size_t sent = 0;
2039  const char* buf = static_cast<const char*>(_buf);
2040  while (sent < nbytes)
2041  {
2042  int n = TCPSlaveBase::write(buf + sent, nbytes - sent);
2043 
2044  if (n < 0) {
2045  // some error occurred
2046  return -1;
2047  }
2048 
2049  sent += n;
2050  }
2051 
2052  return sent;
2053 }
2054 
2055 void HTTPProtocol::clearUnreadBuffer()
2056 {
2057  m_unreadBuf.clear();
2058 }
2059 
2060 // Note: the implementation of unread/readBuffered assumes that unread will only
2061 // be used when there is extra data we don't want to handle, and not to wait for more data.
2062 void HTTPProtocol::unread(char *buf, size_t size)
2063 {
2064  // implement LIFO (stack) semantics
2065  const int newSize = m_unreadBuf.size() + size;
2066  m_unreadBuf.resize(newSize);
2067  for (size_t i = 0; i < size; i++) {
2068  m_unreadBuf.data()[newSize - i - 1] = buf[i];
2069  }
2070  if (size) {
2071  //hey, we still have data, closed connection or not!
2072  m_isEOF = false;
2073  }
2074 }
2075 
2076 size_t HTTPProtocol::readBuffered(char *buf, size_t size, bool unlimited)
2077 {
2078  size_t bytesRead = 0;
2079  if (!m_unreadBuf.isEmpty()) {
2080  const int bufSize = m_unreadBuf.size();
2081  bytesRead = qMin((int)size, bufSize);
2082 
2083  for (size_t i = 0; i < bytesRead; i++) {
2084  buf[i] = m_unreadBuf.constData()[bufSize - i - 1];
2085  }
2086  m_unreadBuf.truncate(bufSize - bytesRead);
2087 
2088  // If we have an unread buffer and the size of the content returned by the
2089  // server is unknown, e.g. chuncked transfer, return the bytes read here since
2090  // we may already have enough data to complete the response and don't want to
2091  // wait for more. See BR# 180631.
2092  if (unlimited)
2093  return bytesRead;
2094  }
2095  if (bytesRead < size) {
2096  int rawRead = TCPSlaveBase::read(buf + bytesRead, size - bytesRead);
2097  if (rawRead < 1) {
2098  m_isEOF = true;
2099  return bytesRead;
2100  }
2101  bytesRead += rawRead;
2102  }
2103  return bytesRead;
2104 }
2105 
2106 //### this method will detect an n*(\r\n) sequence if it crosses invocations.
2107 // it will look (n*2 - 1) bytes before start at most and never before buf, naturally.
2108 // supported number of newlines are one and two, in line with HTTP syntax.
2109 // return true if numNewlines newlines were found.
2110 bool HTTPProtocol::readDelimitedText(char *buf, int *idx, int end, int numNewlines)
2111 {
2112  Q_ASSERT(numNewlines >=1 && numNewlines <= 2);
2113  char mybuf[64]; //somewhere close to the usual line length to avoid unread()ing too much
2114  int pos = *idx;
2115  while (pos < end && !m_isEOF) {
2116  int step = qMin((int)sizeof(mybuf), end - pos);
2117  if (m_isChunked) {
2118  //we might be reading the end of the very last chunk after which there is no data.
2119  //don't try to read any more bytes than there are because it causes stalls
2120  //(yes, it shouldn't stall but it does)
2121  step = 1;
2122  }
2123  size_t bufferFill = readBuffered(mybuf, step);
2124 
2125  for (size_t i = 0; i < bufferFill ; ++i, ++pos) {
2126  // we copy the data from mybuf to buf immediately and look for the newlines in buf.
2127  // that way we don't miss newlines split over several invocations of this method.
2128  buf[pos] = mybuf[i];
2129 
2130  // did we just copy one or two times the (usually) \r\n delimiter?
2131  // until we find even more broken webservers in the wild let's assume that they either
2132  // send \r\n (RFC compliant) or \n (broken) as delimiter...
2133  if (buf[pos] == '\n') {
2134  bool found = numNewlines == 1;
2135  if (!found) { // looking for two newlines
2136  // Detect \n\n and \n\r\n. The other cases (\r\n\n, \r\n\r\n) are covered by the first two.
2137  found = ((pos >= 1 && buf[pos - 1] == '\n') ||
2138  (pos >= 2 && buf[pos - 2] == '\n' && buf[pos - 1] == '\r'));
2139  }
2140  if (found) {
2141  i++; // unread bytes *after* CRLF
2142  unread(&mybuf[i], bufferFill - i);
2143  *idx = pos + 1;
2144  return true;
2145  }
2146  }
2147  }
2148  }
2149  *idx = pos;
2150  return false;
2151 }
2152 
2153 static bool isCompatibleNextUrl(const KUrl &previous, const KUrl &now)
2154 {
2155  if (previous.host() != now.host() || previous.port() != now.port()) {
2156  return false;
2157  }
2158  if (previous.user().isEmpty() && previous.pass().isEmpty()) {
2159  return true;
2160  }
2161  return previous.user() == now.user() && previous.pass() == now.pass();
2162 }
2163 
2164 bool HTTPProtocol::httpShouldCloseConnection()
2165 {
2166  kDebug(7113);
2167 
2168  if (!isConnected()) {
2169  return false;
2170  }
2171 
2172  if (!m_request.proxyUrls.isEmpty() && !isAutoSsl()) {
2173  Q_FOREACH(const QString& url, m_request.proxyUrls) {
2174  if (url != QLatin1String("DIRECT")) {
2175  if (isCompatibleNextUrl(m_server.proxyUrl, KUrl(url))) {
2176  return false;
2177  }
2178  }
2179  }
2180  return true;
2181  }
2182 
2183  return !isCompatibleNextUrl(m_server.url, m_request.url);
2184 }
2185 
2186 bool HTTPProtocol::httpOpenConnection()
2187 {
2188  kDebug(7113);
2189  m_server.clear();
2190 
2191  // Only save proxy auth information after proxy authentication has
2192  // actually taken place, which will set up exactly this connection.
2193  disconnect(socket(), SIGNAL(connected()),
2194  this, SLOT(saveProxyAuthenticationForSocket()));
2195 
2196  clearUnreadBuffer();
2197 
2198  int connectError = 0;
2199  QString errorString;
2200 
2201  // Get proxy information...
2202  if (m_request.proxyUrls.isEmpty()) {
2203  m_request.proxyUrls = config()->readEntry("ProxyUrls", QStringList());
2204  kDebug(7113) << "Proxy URLs:" << m_request.proxyUrls;
2205  }
2206 
2207  if (m_request.proxyUrls.isEmpty()) {
2208  connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString);
2209  } else {
2210  KUrl::List badProxyUrls;
2211  Q_FOREACH(const QString& proxyUrl, m_request.proxyUrls) {
2212  const KUrl url (proxyUrl);
2213  const QString scheme (url.protocol());
2214 
2215  if (!supportedProxyScheme(scheme)) {
2216  connectError = ERR_COULD_NOT_CONNECT;
2217  errorString = url.url();
2218  continue;
2219  }
2220 
2221  const bool isDirectConnect = (proxyUrl == QLatin1String("DIRECT"));
2222  QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy;
2223  if (url.protocol() == QLatin1String("socks")) {
2224  proxyType = QNetworkProxy::Socks5Proxy;
2225  } else if (!isDirectConnect && isAutoSsl()) {
2226  proxyType = QNetworkProxy::HttpProxy;
2227  }
2228 
2229  kDebug(7113) << "Connecting to proxy: address=" << proxyUrl << "type=" << proxyType;
2230 
2231  if (proxyType == QNetworkProxy::NoProxy) {
2232  // Only way proxy url and request url are the same is when the
2233  // proxy URL list contains a "DIRECT" entry. See resetSessionSettings().
2234  if (isDirectConnect) {
2235  connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString);
2236  kDebug(7113) << "Connected DIRECT: host=" << m_request.url.host() << "post=" << m_request.url.port(defaultPort());
2237  } else {
2238  connectError = connectToHost(url.host(), url.port(), &errorString);
2239  if (connectError == 0) {
2240  m_request.proxyUrl = url;
2241  kDebug(7113) << "Connected to proxy: host=" << url.host() << "port=" << url.port();
2242  } else {
2243  if (connectError == ERR_UNKNOWN_HOST)
2244  connectError = ERR_UNKNOWN_PROXY_HOST;
2245  kDebug(7113) << "Failed to connect to proxy:" << proxyUrl;
2246  badProxyUrls << url;
2247  }
2248  }
2249  if (connectError == 0) {
2250  break;
2251  }
2252  } else {
2253  QNetworkProxy proxy (proxyType, url.host(), url.port(), url.user(), url.pass());
2254  QNetworkProxy::setApplicationProxy(proxy);
2255  connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString);
2256  if (connectError == 0) {
2257  kDebug(7113) << "Tunneling thru proxy: host=" << url.host() << "port=" << url.port();
2258  break;
2259  } else {
2260  if (connectError == ERR_UNKNOWN_HOST)
2261  connectError = ERR_UNKNOWN_PROXY_HOST;
2262  kDebug(7113) << "Failed to connect to proxy:" << proxyUrl;
2263  badProxyUrls << url;
2264  QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
2265  }
2266  }
2267  }
2268 
2269  if (!badProxyUrls.isEmpty()) {
2270  //TODO: Notify the client of BAD proxy addresses (needed for PAC setups).
2271  }
2272  }
2273 
2274  if (connectError != 0) {
2275  error (connectError, errorString);
2276  return false;
2277  }
2278 
2279  // Disable Nagle's algorithm, i.e turn on TCP_NODELAY.
2280  KTcpSocket *sock = qobject_cast<KTcpSocket*>(socket());
2281  if (sock) {
2282  // kDebug(7113) << "TCP_NODELAY:" << sock->socketOption(QAbstractSocket::LowDelayOption);
2283  sock->setSocketOption(QAbstractSocket::LowDelayOption, 1);
2284  }
2285 
2286  m_server.initFrom(m_request);
2287  connected();
2288  return true;
2289 }
2290 
2291 bool HTTPProtocol::satisfyRequestFromCache(bool *cacheHasPage)
2292 {
2293  kDebug(7113);
2294 
2295  if (m_request.cacheTag.useCache) {
2296  const bool offline = isOffline();
2297 
2298  if (offline && m_request.cacheTag.policy != KIO::CC_Reload) {
2299  m_request.cacheTag.policy= KIO::CC_CacheOnly;
2300  }
2301 
2302  const bool isCacheOnly = m_request.cacheTag.policy == KIO::CC_CacheOnly;
2303  const CacheTag::CachePlan plan = m_request.cacheTag.plan(m_maxCacheAge);
2304 
2305  bool openForReading = false;
2306  if (plan == CacheTag::UseCached || plan == CacheTag::ValidateCached) {
2307  openForReading = cacheFileOpenRead();
2308 
2309  if (!openForReading && (isCacheOnly || offline)) {
2310  // cache-only or offline -> we give a definite answer and it is "no"
2311  *cacheHasPage = false;
2312  if (isCacheOnly) {
2313  error(ERR_DOES_NOT_EXIST, m_request.url.url());
2314  } else if (offline) {
2315  error(ERR_COULD_NOT_CONNECT, m_request.url.url());
2316  }
2317  return true;
2318  }
2319  }
2320 
2321  if (openForReading) {
2322  m_request.cacheTag.ioMode = ReadFromCache;
2323  *cacheHasPage = true;
2324  // return false if validation is required, so a network request will be sent
2325  return m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached;
2326  }
2327  }
2328  *cacheHasPage = false;
2329  return false;
2330 }
2331 
2332 QString HTTPProtocol::formatRequestUri() const
2333 {
2334  // Only specify protocol, host and port when they are not already clear, i.e. when
2335  // we handle HTTP proxying ourself and the proxy server needs to know them.
2336  // Sending protocol/host/port in other cases confuses some servers, and it's not their fault.
2337  if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) {
2338  KUrl u;
2339 
2340  QString protocol = m_request.url.protocol();
2341  if (protocol.startsWith(QLatin1String("webdav"))) {
2342  protocol.replace(0, qstrlen("webdav"), QLatin1String("http"));
2343  }
2344  u.setProtocol(protocol);
2345 
2346  u.setHost(m_request.url.host());
2347  // if the URL contained the default port it should have been stripped earlier
2348  Q_ASSERT(m_request.url.port() != defaultPort());
2349  u.setPort(m_request.url.port());
2350  u.setEncodedPathAndQuery(m_request.url.encodedPathAndQuery(
2351  KUrl::LeaveTrailingSlash, KUrl::AvoidEmptyPath));
2352  return u.url();
2353  } else {
2354  return m_request.url.encodedPathAndQuery(KUrl::LeaveTrailingSlash, KUrl::AvoidEmptyPath);
2355  }
2356 }
2357 
2373 bool HTTPProtocol::sendQuery()
2374 {
2375  kDebug(7113);
2376 
2377  // Cannot have an https request without autoSsl! This can
2378  // only happen if the current installation does not support SSL...
2379  if (isEncryptedHttpVariety(m_protocol) && !isAutoSsl()) {
2380  error(ERR_UNSUPPORTED_PROTOCOL, toQString(m_protocol));
2381  return false;
2382  }
2383 
2384  // Check the reusability of the current connection.
2385  if (httpShouldCloseConnection()) {
2386  httpCloseConnection();
2387  }
2388 
2389  // Create a new connection to the remote machine if we do
2390  // not already have one...
2391  // NB: the !m_socketProxyAuth condition is a workaround for a proxied Qt socket sometimes
2392  // looking disconnected after receiving the initial 407 response.
2393  // I guess the Qt socket fails to hide the effect of proxy-connection: close after receiving
2394  // the 407 header.
2395  if ((!isConnected() && !m_socketProxyAuth))
2396  {
2397  if (!httpOpenConnection())
2398  {
2399  kDebug(7113) << "Couldn't connect, oopsie!";
2400  return false;
2401  }
2402  }
2403 
2404  m_request.cacheTag.ioMode = NoCache;
2405  m_request.cacheTag.servedDate = -1;
2406  m_request.cacheTag.lastModifiedDate = -1;
2407  m_request.cacheTag.expireDate = -1;
2408 
2409  QString header;
2410 
2411  bool hasBodyData = false;
2412  bool hasDavData = false;
2413 
2414  {
2415  header = toQString(m_request.methodString());
2416  QString davHeader;
2417 
2418  // Fill in some values depending on the HTTP method to guide further processing
2419  switch (m_request.method)
2420  {
2421  case HTTP_GET: {
2422  bool cacheHasPage = false;
2423  if (satisfyRequestFromCache(&cacheHasPage)) {
2424  kDebug(7113) << "cacheHasPage =" << cacheHasPage;
2425  return cacheHasPage;
2426  }
2427  if (!cacheHasPage) {
2428  // start a new cache file later if appropriate
2429  m_request.cacheTag.ioMode = WriteToCache;
2430  }
2431  break;
2432  }
2433  case HTTP_HEAD:
2434  break;
2435  case HTTP_PUT:
2436  case HTTP_POST:
2437  hasBodyData = true;
2438  break;
2439  case HTTP_DELETE:
2440  case HTTP_OPTIONS:
2441  break;
2442  case DAV_PROPFIND:
2443  hasDavData = true;
2444  davHeader = QLatin1String("Depth: ");
2445  if ( hasMetaData( QLatin1String("davDepth") ) )
2446  {
2447  kDebug(7113) << "Reading DAV depth from metadata:" << metaData( QLatin1String("davDepth") );
2448  davHeader += metaData( QLatin1String("davDepth") );
2449  }
2450  else
2451  {
2452  if ( m_request.davData.depth == 2 )
2453  davHeader += QLatin1String("infinity");
2454  else
2455  davHeader += QString::number( m_request.davData.depth );
2456  }
2457  davHeader += QLatin1String("\r\n");
2458  break;
2459  case DAV_PROPPATCH:
2460  hasDavData = true;
2461  break;
2462  case DAV_MKCOL:
2463  break;
2464  case DAV_COPY:
2465  case DAV_MOVE:
2466  davHeader = QLatin1String("Destination: ") + m_request.davData.desturl;
2467  // infinity depth means copy recursively
2468  // (optional for copy -> but is the desired action)
2469  davHeader += QLatin1String("\r\nDepth: infinity\r\nOverwrite: ");
2470  davHeader += QLatin1Char(m_request.davData.overwrite ? 'T' : 'F');
2471  davHeader += QLatin1String("\r\n");
2472  break;
2473  case DAV_LOCK:
2474  davHeader = QLatin1String("Timeout: ");
2475  {
2476  uint timeout = 0;
2477  if ( hasMetaData( QLatin1String("davTimeout") ) )
2478  timeout = metaData( QLatin1String("davTimeout") ).toUInt();
2479  if ( timeout == 0 )
2480  davHeader += QLatin1String("Infinite");
2481  else
2482  davHeader += QLatin1String("Seconds-") + QString::number(timeout);
2483  }
2484  davHeader += QLatin1String("\r\n");
2485  hasDavData = true;
2486  break;
2487  case DAV_UNLOCK:
2488  davHeader = QLatin1String("Lock-token: ") + metaData(QLatin1String("davLockToken")) + QLatin1String("\r\n");
2489  break;
2490  case DAV_SEARCH:
2491  case DAV_REPORT:
2492  hasDavData = true;
2493  /* fall through */
2494  case DAV_SUBSCRIBE:
2495  case DAV_UNSUBSCRIBE:
2496  case DAV_POLL:
2497  break;
2498  default:
2499  error (ERR_UNSUPPORTED_ACTION, QString());
2500  return false;
2501  }
2502  // DAV_POLL; DAV_NOTIFY
2503 
2504  header += formatRequestUri() + QLatin1String(" HTTP/1.1\r\n"); /* start header */
2505 
2506  /* support for virtual hosts and required by HTTP 1.1 */
2507  header += QLatin1String("Host: ") + m_request.encoded_hostname;
2508  if (m_request.url.port(defaultPort()) != defaultPort()) {
2509  header += QLatin1Char(':') + QString::number(m_request.url.port());
2510  }
2511  header += QLatin1String("\r\n");
2512 
2513  // Support old HTTP/1.0 style keep-alive header for compatibility
2514  // purposes as well as performance improvements while giving end
2515  // users the ability to disable this feature for proxy servers that
2516  // don't support it, e.g. junkbuster proxy server.
2517  if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) {
2518  header += QLatin1String("Proxy-Connection: ");
2519  } else {
2520  header += QLatin1String("Connection: ");
2521  }
2522  if (m_request.isKeepAlive) {
2523  header += QLatin1String("keep-alive\r\n");
2524  } else {
2525  header += QLatin1String("close\r\n");
2526  }
2527 
2528  if (!m_request.userAgent.isEmpty())
2529  {
2530  header += QLatin1String("User-Agent: ");
2531  header += m_request.userAgent;
2532  header += QLatin1String("\r\n");
2533  }
2534 
2535  if (!m_request.referrer.isEmpty())
2536  {
2537  header += QLatin1String("Referer: "); //Don't try to correct spelling!
2538  header += m_request.referrer;
2539  header += QLatin1String("\r\n");
2540  }
2541 
2542  if ( m_request.endoffset > m_request.offset )
2543  {
2544  header += QLatin1String("Range: bytes=");
2545  header += KIO::number(m_request.offset);
2546  header += QLatin1Char('-');
2547  header += KIO::number(m_request.endoffset);
2548  header += QLatin1String("\r\n");
2549  kDebug(7103) << "kio_http : Range =" << KIO::number(m_request.offset)
2550  << "-" << KIO::number(m_request.endoffset);
2551  }
2552  else if ( m_request.offset > 0 && m_request.endoffset == 0 )
2553  {
2554  header += QLatin1String("Range: bytes=");
2555  header += KIO::number(m_request.offset);
2556  header += QLatin1String("-\r\n");
2557  kDebug(7103) << "kio_http: Range =" << KIO::number(m_request.offset);
2558  }
2559 
2560  if ( !m_request.cacheTag.useCache || m_request.cacheTag.policy==CC_Reload )
2561  {
2562  /* No caching for reload */
2563  header += QLatin1String("Pragma: no-cache\r\n"); /* for HTTP/1.0 caches */
2564  header += QLatin1String("Cache-control: no-cache\r\n"); /* for HTTP >=1.1 caches */
2565  }
2566  else if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached)
2567  {
2568  kDebug(7113) << "needs validation, performing conditional get.";
2569  /* conditional get */
2570  if (!m_request.cacheTag.etag.isEmpty())
2571  header += QLatin1String("If-None-Match: ") + m_request.cacheTag.etag + QLatin1String("\r\n");
2572 
2573  if (m_request.cacheTag.lastModifiedDate != -1) {
2574  const QString httpDate = formatHttpDate(m_request.cacheTag.lastModifiedDate);
2575  header += QLatin1String("If-Modified-Since: ") + httpDate + QLatin1String("\r\n");
2576  setMetaData(QLatin1String("modified"), httpDate);
2577  }
2578  }
2579 
2580  header += QLatin1String("Accept: ");
2581  const QString acceptHeader = metaData(QLatin1String("accept"));
2582  if (!acceptHeader.isEmpty())
2583  header += acceptHeader;
2584  else
2585  header += QLatin1String(DEFAULT_ACCEPT_HEADER);
2586  header += QLatin1String("\r\n");
2587 
2588  if (m_request.allowTransferCompression)
2589  header += QLatin1String("Accept-Encoding: gzip, deflate, x-gzip, x-deflate\r\n");
2590 
2591  if (!m_request.charsets.isEmpty())
2592  header += QLatin1String("Accept-Charset: ") + m_request.charsets + QLatin1String("\r\n");
2593 
2594  if (!m_request.languages.isEmpty())
2595  header += QLatin1String("Accept-Language: ") + m_request.languages + QLatin1String("\r\n");
2596 
2597  QString cookieStr;
2598  const QString cookieMode = metaData(QLatin1String("cookies")).toLower();
2599 
2600  if (cookieMode == QLatin1String("none"))
2601  {
2602  m_request.cookieMode = HTTPRequest::CookiesNone;
2603  }
2604  else if (cookieMode == QLatin1String("manual"))
2605  {
2606  m_request.cookieMode = HTTPRequest::CookiesManual;
2607  cookieStr = metaData(QLatin1String("setcookies"));
2608  }
2609  else
2610  {
2611  m_request.cookieMode = HTTPRequest::CookiesAuto;
2612  if (m_request.useCookieJar)
2613  cookieStr = findCookies(m_request.url.url());
2614  }
2615 
2616  if (!cookieStr.isEmpty())
2617  header += cookieStr + QLatin1String("\r\n");
2618 
2619  const QString customHeader = metaData( QLatin1String("customHTTPHeader") );
2620  if (!customHeader.isEmpty())
2621  {
2622  header += sanitizeCustomHTTPHeader(customHeader);
2623  header += QLatin1String("\r\n");
2624  }
2625 
2626  const QString contentType = metaData(QLatin1String("content-type"));
2627  if (!contentType.isEmpty())
2628  {
2629  if (!contentType.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive))
2630  header += QLatin1String("Content-Type: ");
2631  header += contentType;
2632  header += QLatin1String("\r\n");
2633  }
2634 
2635  // DoNotTrack feature...
2636  if (config()->readEntry("DoNotTrack", false))
2637  header += QLatin1String("DNT: 1\r\n");
2638 
2639  // Remember that at least one failed (with 401 or 407) request/response
2640  // roundtrip is necessary for the server to tell us that it requires
2641  // authentication. However, we proactively add authentication headers if when
2642  // we have cached credentials to avoid the extra roundtrip where possible.
2643  header += authenticationHeader();
2644 
2645  if ( m_protocol == "webdav" || m_protocol == "webdavs" )
2646  {
2647  header += davProcessLocks();
2648 
2649  // add extra webdav headers, if supplied
2650  davHeader += metaData(QLatin1String("davHeader"));
2651 
2652  // Set content type of webdav data
2653  if (hasDavData)
2654  davHeader += QLatin1String("Content-Type: text/xml; charset=utf-8\r\n");
2655 
2656  // add extra header elements for WebDAV
2657  header += davHeader;
2658  }
2659  }
2660 
2661  kDebug(7103) << "============ Sending Header:";
2662  Q_FOREACH (const QString &s, header.split(QLatin1String("\r\n"), QString::SkipEmptyParts)) {
2663  kDebug(7103) << s;
2664  }
2665 
2666  // End the header iff there is no payload data. If we do have payload data
2667  // sendBody() will add another field to the header, Content-Length.
2668  if (!hasBodyData && !hasDavData)
2669  header += QLatin1String("\r\n");
2670 
2671 
2672  // Now that we have our formatted header, let's send it!
2673 
2674  // Clear out per-connection settings...
2675  resetConnectionSettings();
2676 
2677  // Send the data to the remote machine...
2678  ssize_t written = write(header.toLatin1(), header.length());
2679  bool sendOk = (written == (ssize_t) header.length());
2680  if (!sendOk)
2681  {
2682  kDebug(7113) << "Connection broken! (" << m_request.url.host() << ")"
2683  << " -- intended to write" << header.length()
2684  << "bytes but wrote" << (int)written << ".";
2685 
2686  // The server might have closed the connection due to a timeout, or maybe
2687  // some transport problem arose while the connection was idle.
2688  if (m_request.isKeepAlive)
2689  {
2690  httpCloseConnection();
2691  return true; // Try again
2692  }
2693 
2694  kDebug(7113) << "sendOk == false. Connection broken !"
2695  << " -- intended to write" << header.length()
2696  << "bytes but wrote" << (int)written << ".";
2697  error( ERR_CONNECTION_BROKEN, m_request.url.host() );
2698  return false;
2699  }
2700  else
2701  kDebug(7113) << "sent it!";
2702 
2703  bool res = true;
2704  if (hasBodyData || hasDavData)
2705  res = sendBody();
2706 
2707  infoMessage(i18n("%1 contacted. Waiting for reply...", m_request.url.host()));
2708 
2709  return res;
2710 }
2711 
2712 void HTTPProtocol::forwardHttpResponseHeader(bool forwardImmediately)
2713 {
2714  // Send the response header if it was requested...
2715  if (!config()->readEntry("PropagateHttpHeader", false))
2716  return;
2717 
2718  setMetaData(QLatin1String("HTTP-Headers"), m_responseHeaders.join(QString(QLatin1Char('\n'))));
2719 
2720  if (forwardImmediately)
2721  sendMetaData();
2722 }
2723 
2724 bool HTTPProtocol::parseHeaderFromCache()
2725 {
2726  kDebug(7113);
2727  if (!cacheFileReadTextHeader2()) {
2728  return false;
2729  }
2730 
2731  Q_FOREACH (const QString &str, m_responseHeaders) {
2732  const QString header = str.trimmed();
2733  if (header.startsWith(QLatin1String("content-type:")), Qt::CaseInsensitive) {
2734  int pos = header.indexOf(QLatin1String("charset="), Qt::CaseInsensitive);
2735  if (pos != -1) {
2736  const QString charset = header.mid(pos + 8).toLower();
2737  m_request.cacheTag.charset = charset;
2738  setMetaData(QLatin1String("charset"), charset);
2739  }
2740  } else if (header.startsWith(QLatin1String("content-language:")), Qt::CaseInsensitive) {
2741  const QString language = header.mid(17).trimmed().toLower();
2742  setMetaData(QLatin1String("content-language"), language);
2743  } else if (header.startsWith(QLatin1String("content-disposition:")), Qt::CaseInsensitive) {
2744  parseContentDisposition(header.mid(20).toLower());
2745  }
2746  }
2747 
2748  if (m_request.cacheTag.lastModifiedDate != -1) {
2749  setMetaData(QLatin1String("modified"), formatHttpDate(m_request.cacheTag.lastModifiedDate));
2750  }
2751 
2752  // this header comes from the cache, so the response must have been cacheable :)
2753  setCacheabilityMetadata(true);
2754  kDebug(7113) << "Emitting mimeType" << m_mimeType;
2755  forwardHttpResponseHeader(false);
2756  mimeType(m_mimeType);
2757  // IMPORTANT: Do not remove the call below or the http response headers will
2758  // not be available to the application if this slave is put on hold.
2759  forwardHttpResponseHeader();
2760  return true;
2761 }
2762 
2763 void HTTPProtocol::fixupResponseMimetype()
2764 {
2765  if (m_mimeType.isEmpty())
2766  return;
2767 
2768  kDebug(7113) << "before fixup" << m_mimeType;
2769  // Convert some common mimetypes to standard mimetypes
2770  if (m_mimeType == QLatin1String("application/x-targz"))
2771  m_mimeType = QLatin1String("application/x-compressed-tar");
2772  else if (m_mimeType == QLatin1String("image/x-png"))
2773  m_mimeType = QLatin1String("image/png");
2774  else if (m_mimeType == QLatin1String("audio/x-mp3") || m_mimeType == QLatin1String("audio/x-mpeg") || m_mimeType == QLatin1String("audio/mp3"))
2775  m_mimeType = QLatin1String("audio/mpeg");
2776  else if (m_mimeType == QLatin1String("audio/microsoft-wave"))
2777  m_mimeType = QLatin1String("audio/x-wav");
2778  else if (m_mimeType == QLatin1String("image/x-ms-bmp"))
2779  m_mimeType = QLatin1String("image/bmp");
2780 
2781  // Crypto ones....
2782  else if (m_mimeType == QLatin1String("application/pkix-cert") ||
2783  m_mimeType == QLatin1String("application/binary-certificate")) {
2784  m_mimeType = QLatin1String("application/x-x509-ca-cert");
2785  }
2786 
2787  // Prefer application/x-compressed-tar or x-gzpostscript over application/x-gzip.
2788  else if (m_mimeType == QLatin1String("application/x-gzip")) {
2789  if ((m_request.url.path().endsWith(QLatin1String(".tar.gz"))) ||
2790  (m_request.url.path().endsWith(QLatin1String(".tar"))))
2791  m_mimeType = QLatin1String("application/x-compressed-tar");
2792  if ((m_request.url.path().endsWith(QLatin1String(".ps.gz"))))
2793  m_mimeType = QLatin1String("application/x-gzpostscript");
2794  }
2795 
2796  // Prefer application/x-xz-compressed-tar over application/x-xz for LMZA compressed
2797  // tar files. Arch Linux AUR servers notoriously send the wrong mimetype for this.
2798  else if(m_mimeType == QLatin1String("application/x-xz")) {
2799  if (m_request.url.path().endsWith(QLatin1String(".tar.xz")) ||
2800  m_request.url.path().endsWith(QLatin1String(".txz"))) {
2801  m_mimeType = QLatin1String("application/x-xz-compressed-tar");
2802  }
2803  }
2804 
2805  // Some webservers say "text/plain" when they mean "application/x-bzip"
2806  else if ((m_mimeType == QLatin1String("text/plain")) || (m_mimeType == QLatin1String("application/octet-stream"))) {
2807  const QString ext = QFileInfo(m_request.url.path()).suffix().toUpper();
2808  if (ext == QLatin1String("BZ2"))
2809  m_mimeType = QLatin1String("application/x-bzip");
2810  else if (ext == QLatin1String("PEM"))
2811  m_mimeType = QLatin1String("application/x-x509-ca-cert");
2812  else if (ext == QLatin1String("SWF"))
2813  m_mimeType = QLatin1String("application/x-shockwave-flash");
2814  else if (ext == QLatin1String("PLS"))
2815  m_mimeType = QLatin1String("audio/x-scpls");
2816  else if (ext == QLatin1String("WMV"))
2817  m_mimeType = QLatin1String("video/x-ms-wmv");
2818  else if (ext == QLatin1String("WEBM"))
2819  m_mimeType = QLatin1String("video/webm");
2820  else if (ext == QLatin1String("DEB"))
2821  m_mimeType = QLatin1String("application/x-deb");
2822  }
2823  kDebug(7113) << "after fixup" << m_mimeType;
2824 }
2825 
2826 
2827 void HTTPProtocol::fixupResponseContentEncoding()
2828 {
2829  // WABA: Correct for tgz files with a gzip-encoding.
2830  // They really shouldn't put gzip in the Content-Encoding field!
2831  // Web-servers really shouldn't do this: They let Content-Size refer
2832  // to the size of the tgz file, not to the size of the tar file,
2833  // while the Content-Type refers to "tar" instead of "tgz".
2834  if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("gzip")) {
2835  if (m_mimeType == QLatin1String("application/x-tar")) {
2836  m_contentEncodings.removeLast();
2837  m_mimeType = QLatin1String("application/x-compressed-tar");
2838  } else if (m_mimeType == QLatin1String("application/postscript")) {
2839  // LEONB: Adding another exception for psgz files.
2840  // Could we use the mimelnk files instead of hardcoding all this?
2841  m_contentEncodings.removeLast();
2842  m_mimeType = QLatin1String("application/x-gzpostscript");
2843  } else if ((m_request.allowTransferCompression &&
2844  m_mimeType == QLatin1String("text/html"))
2845  ||
2846  (m_request.allowTransferCompression &&
2847  m_mimeType != QLatin1String("application/x-compressed-tar") &&
2848  m_mimeType != QLatin1String("application/x-tgz") && // deprecated name
2849  m_mimeType != QLatin1String("application/x-targz") && // deprecated name
2850  m_mimeType != QLatin1String("application/x-gzip"))) {
2851  // Unzip!
2852  } else {
2853  m_contentEncodings.removeLast();
2854  m_mimeType = QLatin1String("application/x-gzip");
2855  }
2856  }
2857 
2858  // We can't handle "bzip2" encoding (yet). So if we get something with
2859  // bzip2 encoding, we change the mimetype to "application/x-bzip".
2860  // Note for future changes: some web-servers send both "bzip2" as
2861  // encoding and "application/x-bzip[2]" as mimetype. That is wrong.
2862  // currently that doesn't bother us, because we remove the encoding
2863  // and set the mimetype to x-bzip anyway.
2864  if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("bzip2")) {
2865  m_contentEncodings.removeLast();
2866  m_mimeType = QLatin1String("application/x-bzip");
2867  }
2868 }
2869 
2870 //Return true if the term was found, false otherwise. Advance *pos.
2871 //If (*pos + strlen(term) >= end) just advance *pos to end and return false.
2872 //This means that users should always search for the shortest terms first.
2873 static bool consume(const char input[], int *pos, int end, const char *term)
2874 {
2875  // note: gcc/g++ is quite good at optimizing away redundant strlen()s
2876  int idx = *pos;
2877  if (idx + (int)strlen(term) >= end) {
2878  *pos = end;
2879  return false;
2880  }
2881  if (strncasecmp(&input[idx], term, strlen(term)) == 0) {
2882  *pos = idx + strlen(term);
2883  return true;
2884  }
2885  return false;
2886 }
2887 
2894 bool HTTPProtocol::readResponseHeader()
2895 {
2896  resetResponseParsing();
2897  if (m_request.cacheTag.ioMode == ReadFromCache &&
2898  m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached) {
2899  // parseHeaderFromCache replaces this method in case of cached content
2900  return parseHeaderFromCache();
2901  }
2902 
2903 try_again:
2904  kDebug(7113);
2905 
2906  bool upgradeRequired = false; // Server demands that we upgrade to something
2907  // This is also true if we ask to upgrade and
2908  // the server accepts, since we are now
2909  // committed to doing so
2910  bool noHeadersFound = false;
2911 
2912  m_request.cacheTag.charset.clear();
2913  m_responseHeaders.clear();
2914 
2915  static const int maxHeaderSize = 128 * 1024;
2916 
2917  char buffer[maxHeaderSize];
2918  bool cont = false;
2919  bool bCanResume = false;
2920 
2921  if (!isConnected()) {
2922  kDebug(7113) << "No connection.";
2923  return false; // Reestablish connection and try again
2924  }
2925 
2926 #if 0
2927  // NOTE: This is unnecessary since TCPSlaveBase::read does the same exact
2928  // thing. Plus, if we are unable to read from the socket we need to resend
2929  // the request as done below, not error out! Do not assume remote server
2930  // will honor persistent connections!!
2931  if (!waitForResponse(m_remoteRespTimeout)) {
2932  kDebug(7113) << "Got socket error:" << socket()->errorString();
2933  // No response error
2934  error(ERR_SERVER_TIMEOUT , m_request.url.host());
2935  return false;
2936  }
2937 #endif
2938 
2939  int bufPos = 0;
2940  bool foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 1);
2941  if (!foundDelimiter && bufPos < maxHeaderSize) {
2942  kDebug(7113) << "EOF while waiting for header start.";
2943  if (m_request.isKeepAlive) {
2944  // Try to reestablish connection.
2945  httpCloseConnection();
2946  return false; // Reestablish connection and try again.
2947  }
2948 
2949  if (m_request.method == HTTP_HEAD) {
2950  // HACK
2951  // Some web-servers fail to respond properly to a HEAD request.
2952  // We compensate for their failure to properly implement the HTTP standard
2953  // by assuming that they will be sending html.
2954  kDebug(7113) << "HEAD -> returned mimetype:" << DEFAULT_MIME_TYPE;
2955  mimeType(QLatin1String(DEFAULT_MIME_TYPE));
2956  return true;
2957  }
2958 
2959  kDebug(7113) << "Connection broken !";
2960  error( ERR_CONNECTION_BROKEN, m_request.url.host() );
2961  return false;
2962  }
2963  if (!foundDelimiter) {
2964  //### buffer too small for first line of header(!)
2965  Q_ASSERT(0);
2966  }
2967 
2968  kDebug(7103) << "============ Received Status Response:";
2969  kDebug(7103) << QByteArray(buffer, bufPos).trimmed();
2970 
2971  HTTP_REV httpRev = HTTP_None;
2972  int idx = 0;
2973 
2974  if (idx != bufPos && buffer[idx] == '<') {
2975  kDebug(7103) << "No valid HTTP header found! Document starts with XML/HTML tag";
2976  // document starts with a tag, assume HTML instead of text/plain
2977  m_mimeType = QLatin1String("text/html");
2978  m_request.responseCode = 200; // Fake it
2979  httpRev = HTTP_Unknown;
2980  m_request.isKeepAlive = false;
2981  noHeadersFound = true;
2982  // put string back
2983  unread(buffer, bufPos);
2984  goto endParsing;
2985  }
2986 
2987  // "HTTP/1.1" or similar
2988  if (consume(buffer, &idx, bufPos, "ICY ")) {
2989  httpRev = SHOUTCAST;
2990  m_request.isKeepAlive = false;
2991  } else if (consume(buffer, &idx, bufPos, "HTTP/")) {
2992  if (consume(buffer, &idx, bufPos, "1.0")) {
2993  httpRev = HTTP_10;
2994  m_request.isKeepAlive = false;
2995  } else if (consume(buffer, &idx, bufPos, "1.1")) {
2996  httpRev = HTTP_11;
2997  }
2998  }
2999 
3000  if (httpRev == HTTP_None && bufPos != 0) {
3001  // Remote server does not seem to speak HTTP at all
3002  // Put the crap back into the buffer and hope for the best
3003  kDebug(7113) << "DO NOT WANT." << bufPos;
3004  unread(buffer, bufPos);
3005  if (m_request.responseCode) {
3006  m_request.prevResponseCode = m_request.responseCode;
3007  }
3008  m_request.responseCode = 200; // Fake it
3009  httpRev = HTTP_Unknown;
3010  m_request.isKeepAlive = false;
3011  noHeadersFound = true;
3012  goto endParsing;
3013  }
3014 
3015  // response code //### maybe wrong if we need several iterations for this response...
3016  //### also, do multiple iterations (cf. try_again) to parse one header work w/ pipelining?
3017  if (m_request.responseCode) {
3018  m_request.prevResponseCode = m_request.responseCode;
3019  }
3020  skipSpace(buffer, &idx, bufPos);
3021  //TODO saner handling of invalid response code strings
3022  if (idx != bufPos) {
3023  m_request.responseCode = atoi(&buffer[idx]);
3024  } else {
3025  m_request.responseCode = 200;
3026  }
3027  // move idx to start of (yet to be fetched) next line, skipping the "OK"
3028  idx = bufPos;
3029  // (don't bother parsing the "OK", what do we do if it isn't there anyway?)
3030 
3031  // immediately act on most response codes...
3032 
3033  // Protect users against bogus username intended to fool them into visiting
3034  // sites they had no intention of visiting.
3035  if (isPotentialSpoofingAttack(m_request, config())) {
3036  // kDebug(7113) << "**** POTENTIAL ADDRESS SPOOFING:" << m_request.url;
3037  const int result = messageBox(WarningYesNo,
3038  i18nc("@warning: Security check on url "
3039  "being accessed", "You are about to "
3040  "log in to the site \"%1\" with the "
3041  "username \"%2\", but the website "
3042  "does not require authentication. "
3043  "This may be an attempt to trick you."
3044  "<p>Is \"%1\" the site you want to visit?",
3045  m_request.url.host(), m_request.url.user()),
3046  i18nc("@title:window", "Confirm Website Access"));
3047  if (result == KMessageBox::No) {
3048  error(ERR_USER_CANCELED, m_request.url.url());
3049  return false;
3050  }
3051  setMetaData(QLatin1String("{internal~currenthost}LastSpoofedUserName"), m_request.url.user());
3052  }
3053 
3054  if (m_request.responseCode != 200 && m_request.responseCode != 304) {
3055  m_request.cacheTag.ioMode = NoCache;
3056  }
3057 
3058  if (m_request.responseCode >= 500 && m_request.responseCode <= 599) {
3059  // Server side errors
3060 
3061  if (m_request.method == HTTP_HEAD) {
3062  ; // Ignore error
3063  } else {
3064  if (!sendErrorPageNotification()) {
3065  error(ERR_INTERNAL_SERVER, m_request.url.url());
3066  return false;
3067  }
3068  }
3069  } else if (m_request.responseCode == 416) {
3070  // Range not supported
3071  m_request.offset = 0;
3072  return false; // Try again.
3073  } else if (m_request.responseCode == 426) {
3074  // Upgrade Required
3075  upgradeRequired = true;
3076  } else if (!isAuthenticationRequired(m_request.responseCode) && m_request.responseCode >= 400 && m_request.responseCode <= 499) {
3077  // Any other client errors
3078  // Tell that we will only get an error page here.
3079  if (!sendErrorPageNotification()) {
3080  if (m_request.responseCode == 403)
3081  error(ERR_ACCESS_DENIED, m_request.url.url());
3082  else
3083  error(ERR_DOES_NOT_EXIST, m_request.url.url());
3084  return false;
3085  }
3086  } else if (m_request.responseCode >= 301 && m_request.responseCode<= 303) {
3087  // 301 Moved permanently
3088  if (m_request.responseCode == 301) {
3089  setMetaData(QLatin1String("permanent-redirect"), QLatin1String("true"));
3090  }
3091  // 302 Found (temporary location)
3092  // 303 See Other
3093  // NOTE: This is wrong according to RFC 2616 (section 10.3.[2-4,8]).
3094  // However, because almost all client implementations treat a 301/302
3095  // response as a 303 response in violation of the spec, many servers
3096  // have simply adapted to this way of doing things! Thus, we are
3097  // forced to do the same thing. Otherwise, we loose compatability and
3098  // might not be able to correctly retrieve sites that redirect.
3099  if (m_request.method != HTTP_HEAD) {
3100  m_request.method = HTTP_GET; // Force a GET
3101  }
3102  } else if (m_request.responseCode == 204) {
3103  // No content
3104 
3105  // error(ERR_NO_CONTENT, i18n("Data have been successfully sent."));
3106  // Short circuit and do nothing!
3107 
3108  // The original handling here was wrong, this is not an error: eg. in the
3109  // example of a 204 No Content response to a PUT completing.
3110  // m_iError = true;
3111  // return false;
3112  } else if (m_request.responseCode == 206) {
3113  if (m_request.offset) {
3114  bCanResume = true;
3115  }
3116  } else if (m_request.responseCode == 102) {
3117  // Processing (for WebDAV)
3118  /***
3119  * This status code is given when the server expects the
3120  * command to take significant time to complete. So, inform
3121  * the user.
3122  */
3123  infoMessage( i18n( "Server processing request, please wait..." ) );
3124  cont = true;
3125  } else if (m_request.responseCode == 100) {
3126  // We got 'Continue' - ignore it
3127  cont = true;
3128  }
3129 
3130 endParsing:
3131  bool authRequiresAnotherRoundtrip = false;
3132 
3133  // Skip the whole header parsing if we got no HTTP headers at all
3134  if (!noHeadersFound) {
3135  // Auth handling
3136  const bool wasAuthError = isAuthenticationRequired(m_request.prevResponseCode);
3137  const bool isAuthError = isAuthenticationRequired(m_request.responseCode);
3138  const bool sameAuthError = (m_request.responseCode == m_request.prevResponseCode);
3139  kDebug(7113) << "wasAuthError=" << wasAuthError << "isAuthError=" << isAuthError
3140  << "sameAuthError=" << sameAuthError;
3141  // Not the same authorization error as before and no generic error?
3142  // -> save the successful credentials.
3143  if (wasAuthError && (m_request.responseCode < 400 || (isAuthError && !sameAuthError))) {
3144  saveAuthenticationData();
3145  }
3146 
3147  // done with the first line; now tokenize the other lines
3148 
3149  // TODO review use of STRTOLL vs. QByteArray::toInt()
3150 
3151  foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 2);
3152  kDebug(7113) << " -- full response:" << endl << QByteArray(buffer, bufPos).trimmed();
3153  // Use this to see newlines:
3154  //kDebug(7113) << " -- full response:" << endl << QByteArray(buffer, bufPos).replace("\r", "\\r").replace("\n", "\\n\n");
3155  Q_ASSERT(foundDelimiter);
3156 
3157  //NOTE because tokenizer will overwrite newlines in case of line continuations in the header
3158  // unread(buffer, bufSize) will not generally work anymore. we don't need it either.
3159  // either we have a http response line -> try to parse the header, fail if it doesn't work
3160  // or we have garbage -> fail.
3161  HeaderTokenizer tokenizer(buffer);
3162  tokenizer.tokenize(idx, sizeof(buffer));
3163 
3164  // Note that not receiving "accept-ranges" means that all bets are off
3165  // wrt the server supporting ranges.
3166  TokenIterator tIt = tokenizer.iterator("accept-ranges");
3167  if (tIt.hasNext() && tIt.next().toLower().startsWith("none")) { // krazy:exclude=strings
3168  bCanResume = false;
3169  }
3170 
3171  tIt = tokenizer.iterator("keep-alive");
3172  while (tIt.hasNext()) {
3173  QByteArray ka = tIt.next().trimmed().toLower();
3174  if (ka.startsWith("timeout=")) { // krazy:exclude=strings
3175  int ka_timeout = ka.mid(qstrlen("timeout=")).trimmed().toInt();
3176  if (ka_timeout > 0)
3177  m_request.keepAliveTimeout = ka_timeout;
3178  if (httpRev == HTTP_10) {
3179  m_request.isKeepAlive = true;
3180  }
3181 
3182  break; // we want to fetch ka timeout only
3183  }
3184  }
3185 
3186  // get the size of our data
3187  tIt = tokenizer.iterator("content-length");
3188  if (tIt.hasNext()) {
3189  m_iSize = STRTOLL(tIt.next().constData(), 0, 10);
3190  }
3191 
3192  tIt = tokenizer.iterator("content-location");
3193  if (tIt.hasNext()) {
3194  setMetaData(QLatin1String("content-location"), toQString(tIt.next().trimmed()));
3195  }
3196 
3197  // which type of data do we have?
3198  QString mediaValue;
3199  QString mediaAttribute;
3200  tIt = tokenizer.iterator("content-type");
3201  if (tIt.hasNext()) {
3202  QList<QByteArray> l = tIt.next().split(';');
3203  if (!l.isEmpty()) {
3204  // Assign the mime-type.
3205  m_mimeType = toQString(l.first().trimmed().toLower());
3206  if (m_mimeType.startsWith(QLatin1Char('"'))) {
3207  m_mimeType.remove(0, 1);
3208  }
3209  if (m_mimeType.endsWith(QLatin1Char('"'))) {
3210  m_mimeType.chop(1);
3211  }
3212  kDebug(7113) << "Content-type:" << m_mimeType;
3213  l.removeFirst();
3214  }
3215 
3216  // If we still have text, then it means we have a mime-type with a
3217  // parameter (eg: charset=iso-8851) ; so let's get that...
3218  Q_FOREACH (const QByteArray &statement, l) {
3219  const int index = statement.indexOf('=');
3220  if (index <= 0) {
3221  mediaAttribute = toQString(statement.mid(0, index));
3222  } else {
3223  mediaAttribute = toQString(statement.mid(0, index));
3224  mediaValue = toQString(statement.mid(index+1));
3225  }
3226  mediaAttribute = mediaAttribute.trimmed();
3227  mediaValue = mediaValue.trimmed();
3228 
3229  bool quoted = false;
3230  if (mediaValue.startsWith(QLatin1Char('"'))) {
3231  quoted = true;
3232  mediaValue.remove(0, 1);
3233  }
3234 
3235  if (mediaValue.endsWith(QLatin1Char('"'))) {
3236  mediaValue.chop(1);
3237  }
3238 
3239  kDebug (7113) << "Encoding-type:" << mediaAttribute << "=" << mediaValue;
3240 
3241  if (mediaAttribute == QLatin1String("charset")) {
3242  mediaValue = mediaValue.toLower();
3243  m_request.cacheTag.charset = mediaValue;
3244  setMetaData(QLatin1String("charset"), mediaValue);
3245  } else {
3246  setMetaData(QLatin1String("media-") + mediaAttribute, mediaValue);
3247  if (quoted) {
3248  setMetaData(QLatin1String("media-") + mediaAttribute + QLatin1String("-kio-quoted"),
3249  QLatin1String("true"));
3250  }
3251  }
3252  }
3253  }
3254 
3255  // content?
3256  tIt = tokenizer.iterator("content-encoding");
3257  while (tIt.hasNext()) {
3258  // This is so wrong !! No wonder kio_http is stripping the
3259  // gzip encoding from downloaded files. This solves multiple
3260  // bug reports and caitoo's problem with downloads when such a
3261  // header is encountered...
3262 
3263  // A quote from RFC 2616:
3264  // " When present, its (Content-Encoding) value indicates what additional
3265  // content have been applied to the entity body, and thus what decoding
3266  // mechanism must be applied to obtain the media-type referenced by the
3267  // Content-Type header field. Content-Encoding is primarily used to allow
3268  // a document to be compressed without loosing the identity of its underlying
3269  // media type. Simply put if it is specified, this is the actual mime-type
3270  // we should use when we pull the resource !!!
3271  addEncoding(toQString(tIt.next()), m_contentEncodings);
3272  }
3273  // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183
3274  tIt = tokenizer.iterator("content-disposition");
3275  if (tIt.hasNext()) {
3276  parseContentDisposition(toQString(tIt.next()));
3277  }
3278  tIt = tokenizer.iterator("content-language");
3279  if (tIt.hasNext()) {
3280  QString language = toQString(tIt.next().trimmed());
3281  if (!language.isEmpty()) {
3282  setMetaData(QLatin1String("content-language"), language);
3283  }
3284  }
3285 
3286  tIt = tokenizer.iterator("proxy-connection");
3287  if (tIt.hasNext() && isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) {
3288  QByteArray pc = tIt.next().toLower();
3289  if (pc.startsWith("close")) { // krazy:exclude=strings
3290  m_request.isKeepAlive = false;
3291  } else if (pc.startsWith("keep-alive")) { // krazy:exclude=strings
3292  m_request.isKeepAlive = true;
3293  }
3294  }
3295 
3296  tIt = tokenizer.iterator("link");
3297  if (tIt.hasNext()) {
3298  // We only support Link: <url>; rel="type" so far
3299  QStringList link = toQString(tIt.next()).split(QLatin1Char(';'), QString::SkipEmptyParts);
3300  if (link.count() == 2) {
3301  QString rel = link[1].trimmed();
3302  if (rel.startsWith(QLatin1String("rel=\""))) {
3303  rel = rel.mid(5, rel.length() - 6);
3304  if (rel.toLower() == QLatin1String("pageservices")) {
3305  //### the remove() part looks fishy!
3306  QString url = link[0].remove(QRegExp(QLatin1String("[<>]"))).trimmed();
3307  setMetaData(QLatin1String("PageServices"), url);
3308  }
3309  }
3310  }
3311  }
3312 
3313  tIt = tokenizer.iterator("p3p");
3314  if (tIt.hasNext()) {
3315  // P3P privacy policy information
3316  QStringList policyrefs, compact;
3317  while (tIt.hasNext()) {
3318  QStringList policy = toQString(tIt.next().simplified())
3319  .split(QLatin1Char('='), QString::SkipEmptyParts);
3320  if (policy.count() == 2) {
3321  if (policy[0].toLower() == QLatin1String("policyref")) {
3322  policyrefs << policy[1].remove(QRegExp(QLatin1String("[\")\']"))).trimmed();
3323  } else if (policy[0].toLower() == QLatin1String("cp")) {
3324  // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with
3325  // other metadata sent in strings. This could be a bit more
3326  // efficient but I'm going for correctness right now.
3327  const QString s = policy[1].remove(QRegExp(QLatin1String("[\")\']")));
3328  const QStringList cps = s.split(QLatin1Char(' '), QString::SkipEmptyParts);
3329  compact << cps;
3330  }
3331  }
3332  }
3333  if (!policyrefs.isEmpty()) {
3334  setMetaData(QLatin1String("PrivacyPolicy"), policyrefs.join(QLatin1String("\n")));
3335  }
3336  if (!compact.isEmpty()) {
3337  setMetaData(QLatin1String("PrivacyCompactPolicy"), compact.join(QLatin1String("\n")));
3338  }
3339  }
3340 
3341  // continue only if we know that we're at least HTTP/1.0
3342  if (httpRev == HTTP_11 || httpRev == HTTP_10) {
3343  // let them tell us if we should stay alive or not
3344  tIt = tokenizer.iterator("connection");
3345  while (tIt.hasNext()) {
3346  QByteArray connection = tIt.next().toLower();
3347  if (!(isHttpProxy(m_request.proxyUrl) && !isAutoSsl())) {
3348  if (connection.startsWith("close")) { // krazy:exclude=strings
3349  m_request.isKeepAlive = false;
3350  } else if (connection.startsWith("keep-alive")) { // krazy:exclude=strings
3351  m_request.isKeepAlive = true;
3352  }
3353  }
3354  if (connection.startsWith("upgrade")) { // krazy:exclude=strings
3355  if (m_request.responseCode == 101) {
3356  // Ok, an upgrade was accepted, now we must do it
3357  upgradeRequired = true;
3358  } else if (upgradeRequired) { // 426
3359  // Nothing to do since we did it above already
3360  }
3361  }
3362  }
3363  // what kind of encoding do we have? transfer?
3364  tIt = tokenizer.iterator("transfer-encoding");
3365  while (tIt.hasNext()) {
3366  // If multiple encodings have been applied to an entity, the
3367  // transfer-codings MUST be listed in the order in which they
3368  // were applied.
3369  addEncoding(toQString(tIt.next().trimmed()), m_transferEncodings);
3370  }
3371 
3372  // md5 signature
3373  tIt = tokenizer.iterator("content-md5");
3374  if (tIt.hasNext()) {
3375  m_contentMD5 = toQString(tIt.next().trimmed());
3376  }
3377 
3378  // *** Responses to the HTTP OPTIONS method follow
3379  // WebDAV capabilities
3380  tIt = tokenizer.iterator("dav");
3381  while (tIt.hasNext()) {
3382  m_davCapabilities << toQString(tIt.next());
3383  }
3384  // *** Responses to the HTTP OPTIONS method finished
3385  }
3386 
3387 
3388  // Now process the HTTP/1.1 upgrade
3389  QStringList upgradeOffers;
3390  tIt = tokenizer.iterator("upgrade");
3391  if (tIt.hasNext()) {
3392  // Now we have to check to see what is offered for the upgrade
3393  QString offered = toQString(tIt.next());
3394  upgradeOffers = offered.split(QRegExp(QLatin1String("[ \n,\r\t]")), QString::SkipEmptyParts);
3395  }
3396  Q_FOREACH (const QString &opt, upgradeOffers) {
3397  if (opt == QLatin1String("TLS/1.0")) {
3398  if (!startSsl() && upgradeRequired) {
3399  error(ERR_UPGRADE_REQUIRED, opt);
3400  return false;
3401  }
3402  } else if (opt == QLatin1String("HTTP/1.1")) {
3403  httpRev = HTTP_11;
3404  } else if (upgradeRequired) {
3405  // we are told to do an upgrade we don't understand
3406  error(ERR_UPGRADE_REQUIRED, opt);
3407  return false;
3408  }
3409  }
3410 
3411  // Harvest cookies (mmm, cookie fields!)
3412  QByteArray cookieStr; // In case we get a cookie.
3413  tIt = tokenizer.iterator("set-cookie");
3414  while (tIt.hasNext()) {
3415  cookieStr += "Set-Cookie: ";
3416  cookieStr += tIt.next();
3417  cookieStr += '\n';
3418  }
3419  if (!cookieStr.isEmpty()) {
3420  if ((m_request.cookieMode == HTTPRequest::CookiesAuto) && m_request.useCookieJar) {
3421  // Give cookies to the cookiejar.
3422  const QString domain = config()->readEntry("cross-domain");
3423  if (!domain.isEmpty() && isCrossDomainRequest(m_request.url.host(), domain)) {
3424  cookieStr = "Cross-Domain\n" + cookieStr;
3425  }
3426  addCookies( m_request.url.url(), cookieStr );
3427  } else if (m_request.cookieMode == HTTPRequest::CookiesManual) {
3428  // Pass cookie to application
3429  setMetaData(QLatin1String("setcookies"), QString::fromUtf8(cookieStr)); // ## is encoding ok?
3430  }
3431  }
3432 
3433  // We need to reread the header if we got a '100 Continue' or '102 Processing'
3434  // This may be a non keepalive connection so we handle this kind of loop internally
3435  if ( cont )
3436  {
3437  kDebug(7113) << "cont; returning to mark try_again";
3438  goto try_again;
3439  }
3440 
3441  if (!m_isChunked && (m_iSize == NO_SIZE) && m_request.isKeepAlive &&
3442  canHaveResponseBody(m_request.responseCode, m_request.method)) {
3443  kDebug(7113) << "Ignoring keep-alive: otherwise unable to determine response body length.";
3444  m_request.isKeepAlive = false;
3445  }
3446 
3447  // TODO cache the proxy auth data (not doing this means a small performance regression for now)
3448 
3449  // we may need to send (Proxy or WWW) authorization data
3450  if ((!m_request.doNotWWWAuthenticate && m_request.responseCode == 401) ||
3451  (!m_request.doNotProxyAuthenticate && m_request.responseCode == 407)) {
3452  authRequiresAnotherRoundtrip = handleAuthenticationHeader(&tokenizer);
3453  if (m_iError) {
3454  // If error is set, then handleAuthenticationHeader failed.
3455  return false;
3456  }
3457  } else {
3458  authRequiresAnotherRoundtrip = false;
3459  }
3460 
3461  QString locationStr;
3462  // In fact we should do redirection only if we have a redirection response code (300 range)
3463  tIt = tokenizer.iterator("location");
3464  if (tIt.hasNext() && m_request.responseCode > 299 && m_request.responseCode < 400) {
3465  locationStr = QString::fromUtf8(tIt.next().trimmed());
3466  }
3467  // We need to do a redirect
3468  if (!locationStr.isEmpty())
3469  {
3470  KUrl u(m_request.url, locationStr);
3471  if(!u.isValid())
3472  {
3473  error(ERR_MALFORMED_URL, u.url());
3474  return false;
3475  }
3476 
3477  // preserve #ref: (bug 124654)
3478  // if we were at http://host/resource1#ref, we sent a GET for "/resource1"
3479  // if we got redirected to http://host/resource2, then we have to re-add
3480  // the fragment:
3481  if (m_request.url.hasRef() && !u.hasRef() &&
3482  (m_request.url.host() == u.host()) &&
3483  (m_request.url.protocol() == u.protocol()))
3484  u.setRef(m_request.url.ref());
3485 
3486  m_isRedirection = true;
3487 
3488  if (!m_request.id.isEmpty())
3489  {
3490  sendMetaData();
3491  }
3492 
3493  // If we're redirected to a http:// url, remember that we're doing webdav...
3494  if (m_protocol == "webdav" || m_protocol == "webdavs"){
3495  if(u.protocol() == QLatin1String("http")){
3496  u.setProtocol(QLatin1String("webdav"));
3497  }else if(u.protocol() == QLatin1String("https")){
3498  u.setProtocol(QLatin1String("webdavs"));
3499  }
3500 
3501  m_request.redirectUrl = u;
3502  }
3503 
3504  kDebug(7113) << "Re-directing from" << m_request.url
3505  << "to" << u;
3506 
3507  redirection(u);
3508 
3509  // It would be hard to cache the redirection response correctly. The possible benefit
3510  // is small (if at all, assuming fast disk and slow network), so don't do it.
3511  cacheFileClose();
3512  setCacheabilityMetadata(false);
3513  }
3514 
3515  // Inform the job that we can indeed resume...
3516  if (bCanResume && m_request.offset) {
3517  //TODO turn off caching???
3518  canResume();
3519  } else {
3520  m_request.offset = 0;
3521  }
3522 
3523  // Correct a few common wrong content encodings
3524  fixupResponseContentEncoding();
3525 
3526  // Correct some common incorrect pseudo-mimetypes
3527  fixupResponseMimetype();
3528 
3529  // parse everything related to expire and other dates, and cache directives; also switch
3530  // between cache reading and writing depending on cache validation result.
3531  cacheParseResponseHeader(tokenizer);
3532  }
3533 
3534  if (m_request.cacheTag.ioMode == ReadFromCache) {
3535  if (m_request.cacheTag.policy == CC_Verify &&
3536  m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) {
3537  kDebug(7113) << "Reading resource from cache even though the cache plan is not "
3538  "UseCached; the server is probably sending wrong expiry information.";
3539  }
3540  // parseHeaderFromCache replaces this method in case of cached content
3541  return parseHeaderFromCache();
3542  }
3543 
3544  if (config()->readEntry("PropagateHttpHeader", false) ||
3545  m_request.cacheTag.ioMode == WriteToCache) {
3546  // store header lines if they will be used; note that the tokenizer removing
3547  // line continuation special cases is probably more good than bad.
3548  int nextLinePos = 0;
3549  int prevLinePos = 0;
3550  bool haveMore = true;
3551  while (haveMore) {
3552  haveMore = nextLine(buffer, &nextLinePos, bufPos);
3553  int prevLineEnd = nextLinePos;
3554  while (buffer[prevLineEnd - 1] == '\r' || buffer[prevLineEnd - 1] == '\n') {
3555  prevLineEnd--;
3556  }
3557 
3558  m_responseHeaders.append(QString::fromLatin1(&buffer[prevLinePos],
3559  prevLineEnd - prevLinePos));
3560  prevLinePos = nextLinePos;
3561  }
3562 
3563  // IMPORTANT: Do not remove this line because forwardHttpResponseHeader
3564  // is called below. This line is here to ensure the response headers are
3565  // available to the client before it receives mimetype information.
3566  // The support for putting ioslaves on hold in the KIO-QNAM integration
3567  // will break if this line is removed.
3568  setMetaData(QLatin1String("HTTP-Headers"), m_responseHeaders.join(QString(QLatin1Char('\n'))));
3569  }
3570 
3571  // Let the app know about the mime-type iff this is not a redirection and
3572  // the mime-type string is not empty.
3573  if (!m_isRedirection && m_request.responseCode != 204 &&
3574  (!m_mimeType.isEmpty() || m_request.method == HTTP_HEAD) &&
3575  (m_isLoadingErrorPage || !authRequiresAnotherRoundtrip)) {
3576  kDebug(7113) << "Emitting mimetype " << m_mimeType;
3577  mimeType( m_mimeType );
3578  }
3579 
3580  // IMPORTANT: Do not move the function call below before doing any
3581  // redirection. Otherwise it might mess up some sites, see BR# 150904.
3582  forwardHttpResponseHeader();
3583 
3584  if (m_request.method == HTTP_HEAD)
3585  return true;
3586 
3587  return !authRequiresAnotherRoundtrip; // return true if no more credentials need to be sent
3588 }
3589 
3590 void HTTPProtocol::parseContentDisposition(const QString &disposition)
3591 {
3592  const QMap<QString, QString> parameters = contentDispositionParser(disposition);
3593 
3594  QMap<QString, QString>::const_iterator i = parameters.constBegin();
3595  while (i != parameters.constEnd()) {
3596  setMetaData(QLatin1String("content-disposition-") + i.key(), i.value());
3597  kDebug(7113) << "Content-Disposition:" << i.key() << "=" << i.value();
3598  ++i;
3599  }
3600 }
3601 
3602 void HTTPProtocol::addEncoding(const QString &_encoding, QStringList &encs)
3603 {
3604  QString encoding = _encoding.trimmed().toLower();
3605  // Identity is the same as no encoding
3606  if (encoding == QLatin1String("identity")) {
3607  return;
3608  } else if (encoding == QLatin1String("8bit")) {
3609  // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de
3610  return;
3611  } else if (encoding == QLatin1String("chunked")) {
3612  m_isChunked = true;
3613  // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints?
3614  //if ( m_cmd != CMD_COPY )
3615  m_iSize = NO_SIZE;
3616  } else if ((encoding == QLatin1String("x-gzip")) || (encoding == QLatin1String("gzip"))) {
3617  encs.append(QLatin1String("gzip"));
3618  } else if ((encoding == QLatin1String("x-bzip2")) || (encoding == QLatin1String("bzip2"))) {
3619  encs.append(QLatin1String("bzip2")); // Not yet supported!
3620  } else if ((encoding == QLatin1String("x-deflate")) || (encoding == QLatin1String("deflate"))) {
3621  encs.append(QLatin1String("deflate"));
3622  } else {
3623  kDebug(7113) << "Unknown encoding encountered. "
3624  << "Please write code. Encoding =" << encoding;
3625  }
3626 }
3627 
3628 void HTTPProtocol::cacheParseResponseHeader(const HeaderTokenizer &tokenizer)
3629 {
3630  if (!m_request.cacheTag.useCache)
3631  return;
3632 
3633  // might have to add more response codes
3634  if (m_request.responseCode != 200 && m_request.responseCode != 304) {
3635  return;
3636  }
3637 
3638  // -1 is also the value returned by KDateTime::toTime_t() from an invalid instance.
3639  m_request.cacheTag.servedDate = -1;
3640  m_request.cacheTag.lastModifiedDate = -1;
3641  m_request.cacheTag.expireDate = -1;
3642 
3643  const qint64 currentDate = time(0);
3644  bool mayCache = m_request.cacheTag.ioMode != NoCache;
3645 
3646  TokenIterator tIt = tokenizer.iterator("last-modified");
3647  if (tIt.hasNext()) {
3648  m_request.cacheTag.lastModifiedDate =
3649  KDateTime::fromString(toQString(tIt.next()), KDateTime::RFCDate).toTime_t();
3650 
3651  //### might be good to canonicalize the date by using KDateTime::toString()
3652  if (m_request.cacheTag.lastModifiedDate != -1) {
3653  setMetaData(QLatin1String("modified"), toQString(tIt.current()));
3654  }
3655  }
3656 
3657  // determine from available information when the response was served by the origin server
3658  {
3659  qint64 dateHeader = -1;
3660  tIt = tokenizer.iterator("date");
3661  if (tIt.hasNext()) {
3662  dateHeader = KDateTime::fromString(toQString(tIt.next()), KDateTime::RFCDate).toTime_t();
3663  // -1 on error
3664  }
3665 
3666  qint64 ageHeader = 0;
3667  tIt = tokenizer.iterator("age");
3668  if (tIt.hasNext()) {
3669  ageHeader = tIt.next().toLongLong();
3670  // 0 on error
3671  }
3672 
3673  if (dateHeader != -1) {
3674  m_request.cacheTag.servedDate = dateHeader;
3675  } else if (ageHeader) {
3676  m_request.cacheTag.servedDate = currentDate - ageHeader;
3677  } else {
3678  m_request.cacheTag.servedDate = currentDate;
3679  }
3680  }
3681 
3682  bool hasCacheDirective = false;
3683  // determine when the response "expires", i.e. becomes stale and needs revalidation
3684  {
3685  // (we also parse other cache directives here)
3686  qint64 maxAgeHeader = 0;
3687  tIt = tokenizer.iterator("cache-control");
3688  while (tIt.hasNext()) {
3689  QByteArray cacheStr = tIt.next().toLower();
3690  if (cacheStr.startsWith("no-cache") || cacheStr.startsWith("no-store")) { // krazy:exclude=strings
3691  // Don't put in cache
3692  mayCache = false;
3693  hasCacheDirective = true;
3694  } else if (cacheStr.startsWith("max-age=")) { // krazy:exclude=strings
3695  QByteArray ba = cacheStr.mid(qstrlen("max-age=")).trimmed();
3696  bool ok = false;
3697  maxAgeHeader = ba.toLongLong(&ok);
3698  if (ok) {
3699  hasCacheDirective = true;
3700  }
3701  }
3702  }
3703 
3704  qint64 expiresHeader = -1;
3705  tIt = tokenizer.iterator("expires");
3706  if (tIt.hasNext()) {
3707  expiresHeader = KDateTime::fromString(toQString(tIt.next()), KDateTime::RFCDate).toTime_t();
3708  kDebug(7113) << "parsed expire date from 'expires' header:" << tIt.current();
3709  }
3710 
3711  if (maxAgeHeader) {
3712  m_request.cacheTag.expireDate = m_request.cacheTag.servedDate + maxAgeHeader;
3713  } else if (expiresHeader != -1) {
3714  m_request.cacheTag.expireDate = expiresHeader;
3715  } else {
3716  // heuristic expiration date
3717  if (m_request.cacheTag.lastModifiedDate != -1) {
3718  // expAge is following the RFC 2616 suggestion for heuristic expiration
3719  qint64 expAge = (m_request.cacheTag.servedDate -
3720  m_request.cacheTag.lastModifiedDate) / 10;
3721  // not in the RFC: make sure not to have a huge heuristic cache lifetime
3722  expAge = qMin(expAge, qint64(3600 * 24));
3723  m_request.cacheTag.expireDate = m_request.cacheTag.servedDate + expAge;
3724  } else {
3725  m_request.cacheTag.expireDate = m_request.cacheTag.servedDate +
3726  DEFAULT_CACHE_EXPIRE;
3727  }
3728  }
3729  // make sure that no future clock monkey business causes the cache entry to un-expire
3730  if (m_request.cacheTag.expireDate < currentDate) {
3731  m_request.cacheTag.expireDate = 0; // January 1, 1970 :)
3732  }
3733  }
3734 
3735  tIt = tokenizer.iterator("etag");
3736  if (tIt.hasNext()) {
3737  QString prevEtag = m_request.cacheTag.etag;
3738  m_request.cacheTag.etag = toQString(tIt.next());
3739  if (m_request.cacheTag.etag != prevEtag && m_request.responseCode == 304) {
3740  kDebug(7103) << "304 Not Modified but new entity tag - I don't think this is legal HTTP.";
3741  }
3742  }
3743 
3744  // whoops.. we received a warning
3745  tIt = tokenizer.iterator("warning");
3746  if (tIt.hasNext()) {
3747  //Don't use warning() here, no need to bother the user.
3748  //Those warnings are mostly about caches.
3749  infoMessage(toQString(tIt.next()));
3750  }
3751 
3752  // Cache management (HTTP 1.0)
3753  tIt = tokenizer.iterator("pragma");
3754  while (tIt.hasNext()) {
3755  if (tIt.next().toLower().startsWith("no-cache")) { // krazy:exclude=strings
3756  mayCache = false;
3757  hasCacheDirective = true;
3758  }
3759  }
3760 
3761  // The deprecated Refresh Response
3762  tIt = tokenizer.iterator("refresh");
3763  if (tIt.hasNext()) {
3764  mayCache = false;
3765  setMetaData(QLatin1String("http-refresh"), toQString(tIt.next().trimmed()));
3766  }
3767 
3768  // We don't cache certain text objects
3769  if (m_mimeType.startsWith(QLatin1String("text/")) && (m_mimeType != QLatin1String("text/css")) &&
3770  (m_mimeType != QLatin1String("text/x-javascript")) && !hasCacheDirective) {
3771  // Do not cache secure pages or pages
3772  // originating from password protected sites
3773  // unless the webserver explicitly allows it.
3774  if (isUsingSsl() || m_wwwAuth) {
3775  mayCache = false;
3776  }
3777  }
3778 
3779  // note that we've updated cacheTag, so the plan() is with current data
3780  if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) {
3781  kDebug(7113) << "Cache needs validation";
3782  if (m_request.responseCode == 304) {
3783  kDebug(7113) << "...was revalidated by response code but not by updated expire times. "
3784  "We're going to set the expire date to 60 seconds in the future...";
3785  m_request.cacheTag.expireDate = currentDate + 60;
3786  if (m_request.cacheTag.policy == CC_Verify &&
3787  m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) {
3788  // "apparently" because we /could/ have made an error ourselves, but the errors I
3789  // witnessed were all the server's fault.
3790  kDebug(7113) << "this proxy or server apparently sends bogus expiry information.";
3791  }
3792  }
3793  }
3794 
3795  // validation handling
3796  if (mayCache && m_request.responseCode == 200 && !m_mimeType.isEmpty()) {
3797  kDebug(7113) << "Cache, adding" << m_request.url;
3798  // ioMode can still be ReadFromCache here if we're performing a conditional get
3799  // aka validation
3800  m_request.cacheTag.ioMode = WriteToCache;
3801  if (!cacheFileOpenWrite()) {
3802  kDebug(7113) << "Error creating cache entry for " << m_request.url << "!\n";
3803  }
3804  m_maxCacheSize = config()->readEntry("MaxCacheSize", DEFAULT_MAX_CACHE_SIZE);
3805  } else if (m_request.responseCode == 304 && m_request.cacheTag.file) {
3806  if (!mayCache) {
3807  kDebug(7113) << "This webserver is confused about the cacheability of the data it sends.";
3808  }
3809  // the cache file should still be open for reading, see satisfyRequestFromCache().
3810  Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly);
3811  Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache);
3812  } else {
3813  cacheFileClose();
3814  }
3815 
3816  setCacheabilityMetadata(mayCache);
3817 }
3818 
3819 void HTTPProtocol::setCacheabilityMetadata(bool cachingAllowed)
3820 {
3821  if (!cachingAllowed) {
3822  setMetaData(QLatin1String("no-cache"), QLatin1String("true"));
3823  setMetaData(QLatin1String("expire-date"), QLatin1String("1")); // Expired
3824  } else {
3825  QString tmp;
3826  tmp.setNum(m_request.cacheTag.expireDate);
3827  setMetaData(QLatin1String("expire-date"), tmp);
3828  // slightly changed semantics from old creationDate, probably more correct now
3829  tmp.setNum(m_request.cacheTag.servedDate);
3830  setMetaData(QLatin1String("cache-creation-date"), tmp);
3831  }
3832 }
3833 
3834 bool HTTPProtocol::sendCachedBody()
3835 {
3836  infoMessage(i18n("Sending data to %1" , m_request.url.host()));
3837 
3838  QByteArray cLength ("Content-Length: ");
3839  cLength += QByteArray::number(m_POSTbuf->size());
3840  cLength += "\r\n\r\n";
3841 
3842  kDebug(7113) << "sending cached data (size=" << m_POSTbuf->size() << ")";
3843 
3844  // Send the content length...
3845  bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size());
3846  if (!sendOk) {
3847  kDebug( 7113 ) << "Connection broken when sending "
3848  << "content length: (" << m_request.url.host() << ")";
3849  error( ERR_CONNECTION_BROKEN, m_request.url.host() );
3850  return false;
3851  }
3852 
3853  // Make sure the read head is at the beginning...
3854  m_POSTbuf->reset();
3855 
3856  // Send the data...
3857  while (!m_POSTbuf->atEnd()) {
3858  const QByteArray buffer = m_POSTbuf->read(s_MaxInMemPostBufSize);
3859  sendOk = (write(buffer.data(), buffer.size()) == (ssize_t) buffer.size());
3860  if (!sendOk) {
3861  kDebug(7113) << "Connection broken when sending message body: ("
3862  << m_request.url.host() << ")";
3863  error( ERR_CONNECTION_BROKEN, m_request.url.host() );
3864  return false;
3865  }
3866  }
3867 
3868  return true;
3869 }
3870 
3871 bool HTTPProtocol::sendBody()
3872 {
3873  // If we have cached data, the it is either a repost or a DAV request so send
3874  // the cached data...
3875  if (m_POSTbuf)
3876  return sendCachedBody();
3877 
3878  if (m_iPostDataSize == NO_SIZE) {
3879  // Try the old approach of retireving content data from the job
3880  // before giving up.
3881  if (retrieveAllData())
3882  return sendCachedBody();
3883 
3884  error(ERR_POST_NO_SIZE, m_request.url.host());
3885  return false;
3886  }
3887 
3888  kDebug(7113) << "sending data (size=" << m_iPostDataSize << ")";
3889 
3890  infoMessage(i18n("Sending data to %1", m_request.url.host()));
3891 
3892  QByteArray cLength ("Content-Length: ");
3893  cLength += QByteArray::number(m_iPostDataSize);
3894  cLength += "\r\n\r\n";
3895 
3896  kDebug(7113) << cLength.trimmed();
3897 
3898  // Send the content length...
3899  bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t) cLength.size());
3900  if (!sendOk) {
3901  // The server might have closed the connection due to a timeout, or maybe
3902  // some transport problem arose while the connection was idle.
3903  if (m_request.isKeepAlive)
3904  {
3905  httpCloseConnection();
3906  return true; // Try again
3907  }
3908 
3909  kDebug(7113) << "Connection broken while sending POST content size to" << m_request.url.host();
3910  error( ERR_CONNECTION_BROKEN, m_request.url.host() );
3911  return false;
3912  }
3913 
3914  // Send the amount
3915  totalSize(m_iPostDataSize);
3916 
3917  // If content-length is 0, then do nothing but simply return true.
3918  if (m_iPostDataSize == 0)
3919  return true;
3920 
3921  sendOk = true;
3922  KIO::filesize_t bytesSent = 0;
3923 
3924  while (true) {
3925  dataReq();
3926 
3927  QByteArray buffer;
3928  const int bytesRead = readData(buffer);
3929 
3930  // On done...
3931  if (bytesRead == 0) {
3932  sendOk = (bytesSent == m_iPostDataSize);
3933  break;
3934  }
3935 
3936  // On error return false...
3937  if (bytesRead < 0) {
3938  error(ERR_ABORTED, m_request.url.host());
3939  sendOk = false;
3940  break;
3941  }
3942 
3943  // Cache the POST data in case of a repost request.
3944  cachePostData(buffer);
3945 
3946  // This will only happen if transmitting the data fails, so we will simply
3947  // cache the content locally for the potential re-transmit...
3948  if (!sendOk)
3949  continue;
3950 
3951  if (write(buffer.data(), bytesRead) == static_cast<ssize_t>(bytesRead)) {
3952  bytesSent += bytesRead;
3953  processedSize(bytesSent); // Send update status...
3954  continue;
3955  }
3956 
3957  kDebug(7113) << "Connection broken while sending POST content to" << m_request.url.host();
3958  error(ERR_CONNECTION_BROKEN, m_request.url.host());
3959  sendOk = false;
3960  }
3961 
3962  return sendOk;
3963 }
3964 
3965 void HTTPProtocol::httpClose( bool keepAlive )
3966 {
3967  kDebug(7113) << "keepAlive =" << keepAlive;
3968 
3969  cacheFileClose();
3970 
3971  // Only allow persistent connections for GET requests.
3972  // NOTE: we might even want to narrow this down to non-form
3973  // based submit requests which will require a meta-data from
3974  // khtml.
3975  if (keepAlive) {
3976  if (!m_request.keepAliveTimeout)
3977  m_request.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
3978  else if (m_request.keepAliveTimeout > 2*DEFAULT_KEEP_ALIVE_TIMEOUT)
3979  m_request.keepAliveTimeout = 2*DEFAULT_KEEP_ALIVE_TIMEOUT;
3980 
3981  kDebug(7113) << "keep alive (" << m_request.keepAliveTimeout << ")";
3982  QByteArray data;
3983  QDataStream stream( &data, QIODevice::WriteOnly );
3984  stream << int(99); // special: Close connection
3985  setTimeoutSpecialCommand(m_request.keepAliveTimeout, data);
3986 
3987  return;
3988  }
3989 
3990  httpCloseConnection();
3991 }
3992 
3993 void HTTPProtocol::closeConnection()
3994 {
3995  kDebug(7113);
3996  httpCloseConnection();
3997 }
3998 
3999 void HTTPProtocol::httpCloseConnection()
4000 {
4001  kDebug(7113);
4002  m_server.clear();
4003  disconnectFromHost();
4004  clearUnreadBuffer();
4005  setTimeoutSpecialCommand(-1); // Cancel any connection timeout
4006 }
4007 
4008 void HTTPProtocol::slave_status()
4009 {
4010  kDebug(7113);
4011 
4012  if ( !isConnected() )
4013  httpCloseConnection();
4014 
4015  slaveStatus( m_server.url.host(), isConnected() );
4016 }
4017 
4018 void HTTPProtocol::mimetype( const KUrl& url )
4019 {
4020  kDebug(7113) << url;
4021 
4022  if (!maybeSetRequestUrl(url))
4023  return;
4024  resetSessionSettings();
4025 
4026  m_request.method = HTTP_HEAD;
4027  m_request.cacheTag.policy= CC_Cache;
4028 
4029  if (proceedUntilResponseHeader()) {
4030  httpClose(m_request.isKeepAlive);
4031  finished();
4032  }
4033 
4034  kDebug(7113) << m_mimeType;
4035 }
4036 
4037 void HTTPProtocol::special( const QByteArray &data )
4038 {
4039  kDebug(7113);
4040 
4041  int tmp;
4042  QDataStream stream(data);
4043 
4044  stream >> tmp;
4045  switch (tmp) {
4046  case 1: // HTTP POST
4047  {
4048  KUrl url;
4049  qint64 size;
4050  stream >> url >> size;
4051  post( url, size );
4052  break;
4053  }
4054  case 2: // cache_update
4055  {
4056  KUrl url;
4057  bool no_cache;
4058  qint64 expireDate;
4059  stream >> url >> no_cache >> expireDate;
4060  if (no_cache) {
4061  QString filename = cacheFilePathFromUrl(url);
4062  // there is a tiny risk of deleting the wrong file due to hash collisions here.
4063  // this is an unimportant performance issue.
4064  // FIXME on Windows we may be unable to delete the file if open
4065  QFile::remove(filename);
4066  finished();
4067  break;
4068  }
4069  // let's be paranoid and inefficient here...
4070  HTTPRequest savedRequest = m_request;
4071 
4072  m_request.url = url;
4073  if (cacheFileOpenRead()) {
4074  m_request.cacheTag.expireDate = expireDate;
4075  cacheFileClose(); // this sends an update command to the cache cleaner process
4076  }
4077 
4078  m_request = savedRequest;
4079  finished();
4080  break;
4081  }
4082  case 5: // WebDAV lock
4083  {
4084  KUrl url;
4085  QString scope, type, owner;
4086  stream >> url >> scope >> type >> owner;
4087  davLock( url, scope, type, owner );
4088  break;
4089  }
4090  case 6: // WebDAV unlock
4091  {
4092  KUrl url;
4093  stream >> url;
4094  davUnlock( url );
4095  break;
4096  }
4097  case 7: // Generic WebDAV
4098  {
4099  KUrl url;
4100  int method;
4101  qint64 size;
4102  stream >> url >> method >> size;
4103  davGeneric( url, (KIO::HTTP_METHOD) method, size );
4104  break;
4105  }
4106  case 99: // Close Connection
4107  {
4108  httpCloseConnection();
4109  break;
4110  }
4111  default:
4112  // Some command we don't understand.
4113  // Just ignore it, it may come from some future version of KDE.
4114  break;
4115  }
4116 }
4117 
4121 int HTTPProtocol::readChunked()
4122 {
4123  if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE))
4124  {
4125  // discard CRLF from previous chunk, if any, and read size of next chunk
4126 
4127  int bufPos = 0;
4128  m_receiveBuf.resize(4096);
4129 
4130  bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1);
4131 
4132  if (foundCrLf && bufPos == 2) {
4133  // The previous read gave us the CRLF from the previous chunk. As bufPos includes
4134  // the trailing CRLF it has to be > 2 to possibly include the next chunksize.
4135  bufPos = 0;
4136  foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1);
4137  }
4138  if (!foundCrLf) {
4139  kDebug(7113) << "Failed to read chunk header.";
4140  return -1;
4141  }
4142  Q_ASSERT(bufPos > 2);
4143 
4144  long long nextChunkSize = STRTOLL(m_receiveBuf.data(), 0, 16);
4145  if (nextChunkSize < 0)
4146  {
4147  kDebug(7113) << "Negative chunk size";
4148  return -1;
4149  }
4150  m_iBytesLeft = nextChunkSize;
4151 
4152  kDebug(7113) << "Chunk size =" << m_iBytesLeft << "bytes";
4153 
4154  if (m_iBytesLeft == 0)
4155  {
4156  // Last chunk; read and discard chunk trailer.
4157  // The last trailer line ends with CRLF and is followed by another CRLF
4158  // so we have CRLFCRLF like at the end of a standard HTTP header.
4159  // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes.
4160  //NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over.
4161  char trash[4096];
4162  trash[0] = m_receiveBuf.constData()[bufPos - 2];
4163  trash[1] = m_receiveBuf.constData()[bufPos - 1];
4164  int trashBufPos = 2;
4165  bool done = false;
4166  while (!done && !m_isEOF) {
4167  if (trashBufPos > 3) {
4168  // shift everything but the last three bytes out of the buffer
4169  for (int i = 0; i < 3; i++) {
4170  trash[i] = trash[trashBufPos - 3 + i];
4171  }
4172  trashBufPos = 3;
4173  }
4174  done = readDelimitedText(trash, &trashBufPos, 4096, 2);
4175  }
4176  if (m_isEOF && !done) {
4177  kDebug(7113) << "Failed to read chunk trailer.";
4178  return -1;
4179  }
4180 
4181  return 0;
4182  }
4183  }
4184 
4185  int bytesReceived = readLimited();
4186  if (!m_iBytesLeft) {
4187  m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk
4188  }
4189  return bytesReceived;
4190 }
4191 
4192 int HTTPProtocol::readLimited()
4193 {
4194  if (!m_iBytesLeft)
4195  return 0;
4196 
4197  m_receiveBuf.resize(4096);
4198 
4199  int bytesToReceive;
4200  if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size()))
4201  bytesToReceive = m_receiveBuf.size();
4202  else
4203  bytesToReceive = m_iBytesLeft;
4204 
4205  const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false);
4206 
4207  if (bytesReceived <= 0)
4208  return -1; // Error: connection lost
4209 
4210  m_iBytesLeft -= bytesReceived;
4211  return bytesReceived;
4212 }
4213 
4214 int HTTPProtocol::readUnlimited()
4215 {
4216  if (m_request.isKeepAlive)
4217  {
4218  kDebug(7113) << "Unbounded datastream on a Keep-alive connection!";
4219  m_request.isKeepAlive = false;
4220  }
4221 
4222  m_receiveBuf.resize(4096);
4223 
4224  int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size());
4225  if (result > 0)
4226  return result;
4227 
4228  m_isEOF = true;
4229  m_iBytesLeft = 0;
4230  return 0;
4231 }
4232 
4233 void HTTPProtocol::slotData(const QByteArray &_d)
4234 {
4235  if (!_d.size())
4236  {
4237  m_isEOD = true;
4238  return;
4239  }
4240 
4241  if (m_iContentLeft != NO_SIZE)
4242  {
4243  if (m_iContentLeft >= KIO::filesize_t(_d.size()))
4244  m_iContentLeft -= _d.size();
4245  else
4246  m_iContentLeft = NO_SIZE;
4247  }
4248 
4249  QByteArray d = _d;
4250  if ( !m_dataInternal )
4251  {
4252  // If a broken server does not send the mime-type,
4253  // we try to id it from the content before dealing
4254  // with the content itself.
4255  if ( m_mimeType.isEmpty() && !m_isRedirection &&
4256  !( m_request.responseCode >= 300 && m_request.responseCode <=399) )
4257  {
4258  kDebug(7113) << "Determining mime-type from content...";
4259  int old_size = m_mimeTypeBuffer.size();
4260  m_mimeTypeBuffer.resize( old_size + d.size() );
4261  memcpy( m_mimeTypeBuffer.data() + old_size, d.data(), d.size() );
4262  if ( (m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0)
4263  && (m_mimeTypeBuffer.size() < 1024) )
4264  {
4265  m_cpMimeBuffer = true;
4266  return; // Do not send up the data since we do not yet know its mimetype!
4267  }
4268 
4269  kDebug(7113) << "Mimetype buffer size:" << m_mimeTypeBuffer.size();
4270 
4271  KMimeType::Ptr mime = KMimeType::findByNameAndContent(m_request.url.fileName(), m_mimeTypeBuffer);
4272  if( mime && !mime->isDefault() )
4273  {
4274  m_mimeType = mime->name();
4275  kDebug(7113) << "Mimetype from content:" << m_mimeType;
4276  }
4277 
4278  if ( m_mimeType.isEmpty() )
4279  {
4280  m_mimeType = QLatin1String( DEFAULT_MIME_TYPE );
4281  kDebug(7113) << "Using default mimetype:" << m_mimeType;
4282  }
4283 
4284  //### we could also open the cache file here
4285 
4286  if ( m_cpMimeBuffer )
4287  {
4288  d.resize(0);
4289  d.resize(m_mimeTypeBuffer.size());
4290  memcpy(d.data(), m_mimeTypeBuffer.data(), d.size());
4291  }
4292  mimeType(m_mimeType);
4293  m_mimeTypeBuffer.resize(0);
4294  }
4295 
4296  //kDebug(7113) << "Sending data of size" << d.size();
4297  data( d );
4298  if (m_request.cacheTag.ioMode == WriteToCache) {
4299  cacheFileWritePayload(d);
4300  }
4301  }
4302  else
4303  {
4304  uint old_size = m_webDavDataBuf.size();
4305  m_webDavDataBuf.resize (old_size + d.size());
4306  memcpy (m_webDavDataBuf.data() + old_size, d.data(), d.size());
4307  }
4308 }
4309 
4319 bool HTTPProtocol::readBody( bool dataInternal /* = false */ )
4320 {
4321  // special case for reading cached body since we also do it in this function. oh well.
4322  if (!canHaveResponseBody(m_request.responseCode, m_request.method) &&
4323  !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 &&
4324  m_request.method != HTTP_HEAD)) {
4325  return true;
4326  }
4327 
4328  m_isEOD = false;
4329  // Note that when dataInternal is true, we are going to:
4330  // 1) save the body data to a member variable, m_webDavDataBuf
4331  // 2) _not_ advertise the data, speed, size, etc., through the
4332  // corresponding functions.
4333  // This is used for returning data to WebDAV.
4334  m_dataInternal = dataInternal;
4335  if (dataInternal) {
4336  m_webDavDataBuf.clear();
4337  }
4338 
4339  // Check if we need to decode the data.
4340  // If we are in copy mode, then use only transfer decoding.
4341  bool useMD5 = !m_contentMD5.isEmpty();
4342 
4343  // Deal with the size of the file.
4344  KIO::filesize_t sz = m_request.offset;
4345  if ( sz )
4346  m_iSize += sz;
4347 
4348  if (!m_isRedirection) {
4349  // Update the application with total size except when
4350  // it is compressed, or when the data is to be handled
4351  // internally (webDAV). If compressed we have to wait
4352  // until we uncompress to find out the actual data size
4353  if ( !dataInternal ) {
4354  if ((m_iSize > 0) && (m_iSize != NO_SIZE)) {
4355  totalSize(m_iSize);
4356  infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize),
4357  m_request.url.host()));
4358  } else {
4359  totalSize(0);
4360  }
4361  }
4362 
4363  if (m_request.cacheTag.ioMode == ReadFromCache) {
4364  kDebug(7113) << "reading data from cache...";
4365 
4366  m_iContentLeft = NO_SIZE;
4367 
4368  QByteArray d;
4369  while (true) {
4370  d = cacheFileReadPayload(MAX_IPC_SIZE);
4371  if (d.isEmpty()) {
4372  break;
4373  }
4374  slotData(d);
4375  sz += d.size();
4376  if (!dataInternal) {
4377  processedSize(sz);
4378  }
4379  }
4380 
4381  m_receiveBuf.resize(0);
4382 
4383  if (!dataInternal) {
4384  data(QByteArray());
4385  }
4386 
4387  return true;
4388  }
4389  }
4390 
4391  if (m_iSize != NO_SIZE)
4392  m_iBytesLeft = m_iSize - sz;
4393  else
4394  m_iBytesLeft = NO_SIZE;
4395 
4396  m_iContentLeft = m_iBytesLeft;
4397 
4398  if (m_isChunked)
4399  m_iBytesLeft = NO_SIZE;
4400 
4401  kDebug(7113) << KIO::number(m_iBytesLeft) << "bytes left.";
4402 
4403  // Main incoming loop... Gather everything while we can...
4404  m_cpMimeBuffer = false;
4405  m_mimeTypeBuffer.resize(0);
4406 
4407  HTTPFilterChain chain;
4408 
4409  // redirection ignores the body
4410  if (!m_isRedirection) {
4411  QObject::connect(&chain, SIGNAL(output(QByteArray)),
4412  this, SLOT(slotData(QByteArray)));
4413  }
4414  QObject::connect(&chain, SIGNAL(error(QString)),
4415  this, SLOT(slotFilterError(QString)));
4416 
4417  // decode all of the transfer encodings
4418  while (!m_transferEncodings.isEmpty())
4419  {
4420  QString enc = m_transferEncodings.takeLast();
4421  if ( enc == QLatin1String("gzip") )
4422  chain.addFilter(new HTTPFilterGZip);
4423  else if ( enc == QLatin1String("deflate") )
4424  chain.addFilter(new HTTPFilterDeflate);
4425  }
4426 
4427  // From HTTP 1.1 Draft 6:
4428  // The MD5 digest is computed based on the content of the entity-body,
4429  // including any content-coding that has been applied, but not including
4430  // any transfer-encoding applied to the message-body. If the message is
4431  // received with a transfer-encoding, that encoding MUST be removed
4432  // prior to checking the Content-MD5 value against the received entity.
4433  HTTPFilterMD5 *md5Filter = 0;
4434  if ( useMD5 )
4435  {
4436  md5Filter = new HTTPFilterMD5;
4437  chain.addFilter(md5Filter);
4438  }
4439 
4440  // now decode all of the content encodings
4441  // -- Why ?? We are not
4442  // -- a proxy server, be a client side implementation!! The applications
4443  // -- are capable of determinig how to extract the encoded implementation.
4444  // WB: That's a misunderstanding. We are free to remove the encoding.
4445  // WB: Some braindead www-servers however, give .tgz files an encoding
4446  // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar"
4447  // WB: They shouldn't do that. We can work around that though...
4448  while (!m_contentEncodings.isEmpty())
4449  {
4450  QString enc = m_contentEncodings.takeLast();
4451  if ( enc == QLatin1String("gzip") )
4452  chain.addFilter(new HTTPFilterGZip);
4453  else if ( enc == QLatin1String("deflate") )
4454  chain.addFilter(new HTTPFilterDeflate);
4455  }
4456 
4457  while (!m_isEOF)
4458  {
4459  int bytesReceived;
4460 
4461  if (m_isChunked)
4462  bytesReceived = readChunked();
4463  else if (m_iSize != NO_SIZE)
4464  bytesReceived = readLimited();
4465  else
4466  bytesReceived = readUnlimited();
4467 
4468  // make sure that this wasn't an error, first
4469  // kDebug(7113) << "bytesReceived:"
4470  // << (int) bytesReceived << " m_iSize:" << (int) m_iSize << " Chunked:"
4471  // << m_isChunked << " BytesLeft:"<< (int) m_iBytesLeft;
4472  if (bytesReceived == -1)
4473  {
4474  if (m_iContentLeft == 0)
4475  {
4476  // gzip'ed data sometimes reports a too long content-length.
4477  // (The length of the unzipped data)
4478  m_iBytesLeft = 0;
4479  break;
4480  }
4481  // Oh well... log an error and bug out
4482  kDebug(7113) << "bytesReceived==-1 sz=" << (int)sz
4483  << " Connection broken !";
4484  error(ERR_CONNECTION_BROKEN, m_request.url.host());
4485  return false;
4486  }
4487 
4488  // I guess that nbytes == 0 isn't an error.. but we certainly
4489  // won't work with it!
4490  if (bytesReceived > 0)
4491  {
4492  // Important: truncate the buffer to the actual size received!
4493  // Otherwise garbage will be passed to the app
4494  m_receiveBuf.truncate( bytesReceived );
4495 
4496  chain.slotInput(m_receiveBuf);
4497 
4498  if (m_iError)
4499  return false;
4500 
4501  sz += bytesReceived;
4502  if (!dataInternal)
4503  processedSize( sz );
4504  }
4505  m_receiveBuf.resize(0); // res
4506 
4507  if (m_iBytesLeft && m_isEOD && !m_isChunked)
4508  {
4509  // gzip'ed data sometimes reports a too long content-length.
4510  // (The length of the unzipped data)
4511  m_iBytesLeft = 0;
4512  }
4513 
4514  if (m_iBytesLeft == 0)
4515  {
4516  kDebug(7113) << "EOD received! Left ="<< KIO::number(m_iBytesLeft);
4517  break;
4518  }
4519  }
4520  chain.slotInput(QByteArray()); // Flush chain.
4521 
4522  if ( useMD5 )
4523  {
4524  QString calculatedMD5 = md5Filter->md5();
4525 
4526  if ( m_contentMD5 != calculatedMD5 )
4527  kWarning(7113) << "MD5 checksum MISMATCH! Expected:"
4528  << calculatedMD5 << ", Got:" << m_contentMD5;
4529  }
4530 
4531  // Close cache entry
4532  if (m_iBytesLeft == 0) {
4533  cacheFileClose(); // no-op if not necessary
4534  }
4535 
4536  if (!dataInternal && sz <= 1)
4537  {
4538  if (m_request.responseCode >= 500 && m_request.responseCode <= 599) {
4539  error(ERR_INTERNAL_SERVER, m_request.url.host());
4540  return false;
4541  } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 &&
4542  !isAuthenticationRequired(m_request.responseCode)) {
4543  error(ERR_DOES_NOT_EXIST, m_request.url.host());
4544  return false;
4545  }
4546  }
4547 
4548  if (!dataInternal && !m_isRedirection)
4549  data( QByteArray() );
4550 
4551  return true;
4552 }
4553 
4554 void HTTPProtocol::slotFilterError(const QString &text)
4555 {
4556  error(KIO::ERR_SLAVE_DEFINED, text);
4557 }
4558 
4559 void HTTPProtocol::error( int _err, const QString &_text )
4560 {
4561  // Close the connection only on connection errors. Otherwise, honor the
4562  // keep alive flag.
4563  if (_err == ERR_CONNECTION_BROKEN || _err == ERR_COULD_NOT_CONNECT)
4564  httpClose(false);
4565  else
4566  httpClose(m_request.isKeepAlive);
4567 
4568  if (!m_request.id.isEmpty())
4569  {
4570  forwardHttpResponseHeader();
4571  sendMetaData();
4572  }
4573 
4574  // It's over, we don't need it anymore
4575  clearPostDataBuffer();
4576 
4577  SlaveBase::error( _err, _text );
4578  m_iError = _err;
4579 }
4580 
4581 
4582 void HTTPProtocol::addCookies( const QString &url, const QByteArray &cookieHeader )
4583 {
4584  qlonglong windowId = m_request.windowId.toLongLong();
4585  QDBusInterface kcookiejar( QLatin1String("org.kde.kded"), QLatin1String("/modules/kcookiejar"), QLatin1String("org.kde.KCookieServer") );
4586  (void)kcookiejar.call( QDBus::NoBlock, QLatin1String("addCookies"), url,
4587  cookieHeader, windowId );
4588 }
4589 
4590 QString HTTPProtocol::findCookies( const QString &url)
4591 {
4592  qlonglong windowId = m_request.windowId.toLongLong();
4593  QDBusInterface kcookiejar( QLatin1String("org.kde.kded"), QLatin1String("/modules/kcookiejar"), QLatin1String("org.kde.KCookieServer") );
4594  QDBusReply<QString> reply = kcookiejar.call( QLatin1String("findCookies"), url, windowId );
4595 
4596  if ( !reply.isValid() )
4597  {
4598  kWarning(7113) << "Can't communicate with kded_kcookiejar!";
4599  return QString();
4600  }
4601  return reply;
4602 }
4603 
4604 /******************************* CACHING CODE ****************************/
4605 
4606 HTTPProtocol::CacheTag::CachePlan HTTPProtocol::CacheTag::plan(time_t maxCacheAge) const
4607 {
4608  //notable omission: we're not checking cache file presence or integrity
4609  switch (policy) {
4610  case KIO::CC_Refresh:
4611  // Conditional GET requires the presence of either an ETag or
4612  // last modified date.
4613  if (lastModifiedDate != -1 || !etag.isEmpty()) {
4614  return ValidateCached;
4615  }
4616  break;
4617  case KIO::CC_Reload:
4618  return IgnoreCached;
4619  case KIO::CC_CacheOnly:
4620  case KIO::CC_Cache:
4621  return UseCached;
4622  default:
4623  break;
4624  }
4625 
4626  Q_ASSERT((policy == CC_Verify || policy == CC_Refresh));
4627  time_t currentDate = time(0);
4628  if ((servedDate != -1 && currentDate > (servedDate + maxCacheAge)) ||
4629  (expireDate != -1 && currentDate > expireDate)) {
4630  return ValidateCached;
4631  }
4632  return UseCached;
4633 }
4634 
4635 // !START SYNC!
4636 // The following code should be kept in sync
4637 // with the code in http_cache_cleaner.cpp
4638 
4639 // we use QDataStream; this is just an illustration
4640 struct BinaryCacheFileHeader
4641 {
4642  quint8 version[2];
4643  quint8 compression; // for now fixed to 0
4644  quint8 reserved; // for now; also alignment
4645  qint32 useCount;
4646  qint64 servedDate;
4647  qint64 lastModifiedDate;
4648  qint64 expireDate;
4649  qint32 bytesCached;
4650  // packed size should be 36 bytes; we explicitly set it here to make sure that no compiler
4651  // padding ruins it. We write the fields to disk without any padding.
4652  static const int size = 36;
4653 };
4654 
4655 enum CacheCleanerCommandCode {
4656  InvalidCommand = 0,
4657  CreateFileNotificationCommand,
4658  UpdateFileCommand
4659 };
4660 
4661 // illustration for cache cleaner update "commands"
4662 struct CacheCleanerCommand
4663 {
4664  BinaryCacheFileHeader header;
4665  quint32 commandCode;
4666  // filename in ASCII, binary isn't worth the coding and decoding
4667  quint8 filename[s_hashedUrlNibbles];
4668 };
4669 
4670 QByteArray HTTPProtocol::CacheTag::serialize() const
4671 {
4672  QByteArray ret;
4673  QDataStream stream(&ret, QIODevice::WriteOnly);
4674  stream << quint8('A');
4675  stream << quint8('\n');
4676  stream << quint8(0);
4677  stream << quint8(0);
4678 
4679  stream << fileUseCount;
4680 
4681  // time_t overflow will only be checked when reading; we have no way to tell here.
4682  stream << qint64(servedDate);
4683  stream << qint64(lastModifiedDate);
4684  stream << qint64(expireDate);
4685 
4686  stream << bytesCached;
4687  Q_ASSERT(ret.size() == BinaryCacheFileHeader::size);
4688  return ret;
4689 }
4690 
4691 
4692 static bool compareByte(QDataStream *stream, quint8 value)
4693 {
4694  quint8 byte;
4695  *stream >> byte;
4696  return byte == value;
4697 }
4698 
4699 static bool readTime(QDataStream *stream, time_t *time)
4700 {
4701  qint64 intTime = 0;
4702  *stream >> intTime;
4703  *time = static_cast<time_t>(intTime);
4704 
4705  qint64 check = static_cast<qint64>(*time);
4706  return check == intTime;
4707 }
4708 
4709 // If starting a new file cacheFileWriteVariableSizeHeader() must have been called *before*
4710 // calling this! This is to fill in the headerEnd field.
4711 // If the file is not new headerEnd has already been read from the file and in fact the variable
4712 // size header *may* not be rewritten because a size change would mess up the file layout.
4713 bool HTTPProtocol::CacheTag::deserialize(const QByteArray &d)
4714 {
4715  if (d.size() != BinaryCacheFileHeader::size) {
4716  return false;
4717  }
4718  QDataStream stream(d);
4719  stream.setVersion(QDataStream::Qt_4_5);
4720 
4721  bool ok = true;
4722  ok = ok && compareByte(&stream, 'A');
4723  ok = ok && compareByte(&stream, '\n');
4724  ok = ok && compareByte(&stream, 0);
4725  ok = ok && compareByte(&stream, 0);
4726  if (!ok) {
4727  return false;
4728  }
4729 
4730  stream >> fileUseCount;
4731 
4732  // read and check for time_t overflow
4733  ok = ok && readTime(&stream, &servedDate);
4734  ok = ok && readTime(&stream, &lastModifiedDate);
4735  ok = ok && readTime(&stream, &expireDate);
4736  if (!ok) {
4737  return false;
4738  }
4739 
4740  stream >> bytesCached;
4741 
4742  return true;
4743 }
4744 
4745 /* Text part of the header, directly following the binary first part:
4746 URL\n
4747 etag\n
4748 mimetype\n
4749 header line\n
4750 header line\n
4751 ...
4752 \n
4753 */
4754 
4755 static KUrl storableUrl(const KUrl &url)
4756 {
4757  KUrl ret(url);
4758  ret.setPassword(QString());
4759  ret.setFragment(QString());
4760  return ret;
4761 }
4762 
4763 static void writeLine(QIODevice *dev, const QByteArray &line)
4764 {
4765  static const char linefeed = '\n';
4766  dev->write(line);
4767  dev->write(&linefeed, 1);
4768 }
4769 
4770 void HTTPProtocol::cacheFileWriteTextHeader()
4771 {
4772  QFile *&file = m_request.cacheTag.file;
4773  Q_ASSERT(file);
4774  Q_ASSERT(file->openMode() & QIODevice::WriteOnly);
4775 
4776  file->seek(BinaryCacheFileHeader::size);
4777  writeLine(file, storableUrl(m_request.url).toEncoded());
4778  writeLine(file, m_request.cacheTag.etag.toLatin1());
4779  writeLine(file, m_mimeType.toLatin1());
4780  writeLine(file, m_responseHeaders.join(QString(QLatin1Char('\n'))).toLatin1());
4781  // join("\n") adds no \n to the end, but writeLine() does.
4782  // Add another newline to mark the end of text.
4783  writeLine(file, QByteArray());
4784 }
4785 
4786 static bool readLineChecked(QIODevice *dev, QByteArray *line)
4787 {
4788  *line = dev->readLine(MAX_IPC_SIZE);
4789  // if nothing read or the line didn't fit into 8192 bytes(!)
4790  if (line->isEmpty() || !line->endsWith('\n')) {
4791  return false;
4792  }
4793  // we don't actually want the newline!
4794  line->chop(1);
4795  return true;
4796 }
4797 
4798 bool HTTPProtocol::cacheFileReadTextHeader1(const KUrl &desiredUrl)
4799 {
4800  QFile *&file = m_request.cacheTag.file;
4801  Q_ASSERT(file);
4802  Q_ASSERT(file->openMode() == QIODevice::ReadOnly);
4803 
4804  QByteArray readBuf;
4805  bool ok = readLineChecked(file, &readBuf);
4806  if (storableUrl(desiredUrl).toEncoded() != readBuf) {
4807  kDebug(7103) << "You have witnessed a very improbable hash collision!";
4808  return false;
4809  }
4810 
4811  ok = ok && readLineChecked(file, &readBuf);
4812  m_request.cacheTag.etag = toQString(readBuf);
4813 
4814  return ok;
4815 }
4816 
4817 bool HTTPProtocol::cacheFileReadTextHeader2()
4818 {
4819  QFile *&file = m_request.cacheTag.file;
4820  Q_ASSERT(file);
4821  Q_ASSERT(file->openMode() == QIODevice::ReadOnly);
4822 
4823  bool ok = true;
4824  QByteArray readBuf;
4825 #ifndef NDEBUG
4826  // we assume that the URL and etag have already been read
4827  qint64 oldPos = file->pos();
4828  file->seek(BinaryCacheFileHeader::size);
4829  ok = ok && readLineChecked(file, &readBuf);
4830  ok = ok && readLineChecked(file, &readBuf);
4831  Q_ASSERT(file->pos() == oldPos);
4832 #endif
4833  ok = ok && readLineChecked(file, &readBuf);
4834  m_mimeType = toQString(readBuf);
4835 
4836  m_responseHeaders.clear();
4837  // read as long as no error and no empty line found
4838  while (true) {
4839  ok = ok && readLineChecked(file, &readBuf);
4840  if (ok && !readBuf.isEmpty()) {
4841  m_responseHeaders.append(toQString(readBuf));
4842  } else {
4843  break;
4844  }
4845  }
4846  return ok; // it may still be false ;)
4847 }
4848 
4849 static QString filenameFromUrl(const KUrl &url)
4850 {
4851  QCryptographicHash hash(QCryptographicHash::Sha1);
4852  hash.addData(storableUrl(url).toEncoded());
4853  return toQString(hash.result().toHex());
4854 }
4855 
4856 QString HTTPProtocol::cacheFilePathFromUrl(const KUrl &url) const
4857 {
4858  QString filePath = m_strCacheDir;
4859  if (!filePath.endsWith(QLatin1Char('/'))) {
4860  filePath.append(QLatin1Char('/'));
4861  }
4862  filePath.append(filenameFromUrl(url));
4863  return filePath;
4864 }
4865 
4866 bool HTTPProtocol::cacheFileOpenRead()
4867 {
4868  kDebug(7113);
4869  QString filename = cacheFilePathFromUrl(m_request.url);
4870 
4871  QFile *&file = m_request.cacheTag.file;
4872  if (file) {
4873  kDebug(7113) << "File unexpectedly open; old file is" << file->fileName()
4874  << "new name is" << filename;
4875  Q_ASSERT(file->fileName() == filename);
4876  }
4877  Q_ASSERT(!file);
4878  file = new QFile(filename);
4879  if (file->open(QIODevice::ReadOnly)) {
4880  QByteArray header = file->read(BinaryCacheFileHeader::size);
4881  if (!m_request.cacheTag.deserialize(header)) {
4882  kDebug(7103) << "Cache file header is invalid.";
4883 
4884  file->close();
4885  }
4886  }
4887 
4888  if (file->isOpen() && !cacheFileReadTextHeader1(m_request.url)) {
4889  file->close();
4890  }
4891 
4892  if (!file->isOpen()) {
4893  cacheFileClose();
4894  return false;
4895  }
4896  return true;
4897 }
4898 
4899 
4900 bool HTTPProtocol::cacheFileOpenWrite()
4901 {
4902  kDebug(7113);
4903  QString filename = cacheFilePathFromUrl(m_request.url);
4904 
4905  // if we open a cache file for writing while we have a file open for reading we must have
4906  // found out that the old cached content is obsolete, so delete the file.
4907  QFile *&file = m_request.cacheTag.file;
4908  if (file) {
4909  // ensure that the file is in a known state - either open for reading or null
4910  Q_ASSERT(!qobject_cast<QTemporaryFile *>(file));
4911  Q_ASSERT((file->openMode() & QIODevice::WriteOnly) == 0);
4912  Q_ASSERT(file->fileName() == filename);
4913  kDebug(7113) << "deleting expired cache entry and recreating.";
4914  file->remove();
4915  delete file;
4916  file = 0;
4917  }
4918 
4919  // note that QTemporaryFile will automatically append random chars to filename
4920  file = new QTemporaryFile(filename);
4921  file->open(QIODevice::WriteOnly);
4922 
4923  // if we have started a new file we have not initialized some variables from disk data.
4924  m_request.cacheTag.fileUseCount = 0; // the file has not been *read* yet
4925  m_request.cacheTag.bytesCached = 0;
4926 
4927  if ((file->openMode() & QIODevice::WriteOnly) == 0) {
4928  kDebug(7113) << "Could not open file for writing:" << file->fileName()
4929  << "due to error" << file->error();
4930  cacheFileClose();
4931  return false;
4932  }
4933  return true;
4934 }
4935 
4936 static QByteArray makeCacheCleanerCommand(const HTTPProtocol::CacheTag &cacheTag,
4937  CacheCleanerCommandCode cmd)
4938 {
4939  QByteArray ret = cacheTag.serialize();
4940  QDataStream stream(&ret, QIODevice::WriteOnly);
4941  stream.setVersion(QDataStream::Qt_4_5);
4942 
4943  stream.skipRawData(BinaryCacheFileHeader::size);
4944  // append the command code
4945  stream << quint32(cmd);
4946  // append the filename
4947  QString fileName = cacheTag.file->fileName();
4948  int basenameStart = fileName.lastIndexOf(QLatin1Char('/')) + 1;
4949  QByteArray baseName = fileName.mid(basenameStart, s_hashedUrlNibbles).toLatin1();
4950  stream.writeRawData(baseName.constData(), baseName.size());
4951 
4952  Q_ASSERT(ret.size() == BinaryCacheFileHeader::size + sizeof(quint32) + s_hashedUrlNibbles);
4953  return ret;
4954 }
4955 
4956 //### not yet 100% sure when and when not to call this
4957 void HTTPProtocol::cacheFileClose()
4958 {
4959  kDebug(7113);
4960 
4961  QFile *&file = m_request.cacheTag.file;
4962  if (!file) {
4963  return;
4964  }
4965 
4966  m_request.cacheTag.ioMode = NoCache;
4967 
4968  QByteArray ccCommand;
4969  QTemporaryFile *tempFile = qobject_cast<QTemporaryFile *>(file);
4970 
4971  if (file->openMode() & QIODevice::WriteOnly) {
4972  Q_ASSERT(tempFile);
4973 
4974  if (m_request.cacheTag.bytesCached && !m_iError) {
4975  QByteArray header = m_request.cacheTag.serialize();
4976  tempFile->seek(0);
4977  tempFile->write(header);
4978 
4979  ccCommand = makeCacheCleanerCommand(m_request.cacheTag, CreateFileNotificationCommand);
4980 
4981  QString oldName = tempFile->fileName();
4982  QString newName = oldName;
4983  int basenameStart = newName.lastIndexOf(QLatin1Char('/')) + 1;
4984  // remove the randomized name part added by QTemporaryFile
4985  newName.chop(newName.length() - basenameStart - s_hashedUrlNibbles);
4986  kDebug(7113) << "Renaming temporary file" << oldName << "to" << newName;
4987 
4988  // on windows open files can't be renamed
4989  tempFile->setAutoRemove(false);
4990  delete tempFile;
4991  file = 0;
4992 
4993  if (!QFile::rename(oldName, newName)) {
4994  // ### currently this hides a minor bug when force-reloading a resource. We
4995  // should not even open a new file for writing in that case.
4996  kDebug(7113) << "Renaming temporary file failed, deleting it instead.";
4997  QFile::remove(oldName);
4998  ccCommand.clear(); // we have nothing of value to tell the cache cleaner
4999  }
5000  } else {
5001  // oh, we've never written payload data to the cache file.
5002  // the temporary file is closed and removed and no proper cache entry is created.
5003  }
5004  } else if (file->openMode() == QIODevice::ReadOnly) {
5005  Q_ASSERT(!tempFile);
5006  ccCommand = makeCacheCleanerCommand(m_request.cacheTag, UpdateFileCommand);
5007  }
5008  delete file;
5009  file = 0;
5010 
5011  if (!ccCommand.isEmpty()) {
5012  sendCacheCleanerCommand(ccCommand);
5013  }
5014 }
5015 
5016 void HTTPProtocol::sendCacheCleanerCommand(const QByteArray &command)
5017 {
5018  kDebug(7113);
5019  Q_ASSERT(command.size() == BinaryCacheFileHeader::size + s_hashedUrlNibbles + sizeof(quint32));
5020  int attempts = 0;
5021  while (m_cacheCleanerConnection.state() != QLocalSocket::ConnectedState && attempts < 6) {
5022  if (attempts == 2) {
5023  KToolInvocation::startServiceByDesktopPath(QLatin1String("http_cache_cleaner.desktop"));
5024  }
5025  QString socketFileName = KStandardDirs::locateLocal("socket", QLatin1String("kio_http_cache_cleaner"));
5026  m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly);
5027  m_cacheCleanerConnection.waitForConnected(1500);
5028  attempts++;
5029  }
5030 
5031  if (m_cacheCleanerConnection.state() == QLocalSocket::ConnectedState) {
5032  m_cacheCleanerConnection.write(command);
5033  m_cacheCleanerConnection.flush();
5034  } else {
5035  // updating the stats is not vital, so we just give up.
5036  kDebug(7113) << "Could not connect to cache cleaner, not updating stats of this cache file.";
5037  }
5038 }
5039 
5040 QByteArray HTTPProtocol::cacheFileReadPayload(int maxLength)
5041 {
5042  Q_ASSERT(m_request.cacheTag.file);
5043  Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache);
5044  Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly);
5045  QByteArray ret = m_request.cacheTag.file->read(maxLength);
5046  if (ret.isEmpty()) {
5047  cacheFileClose();
5048  }
5049  return ret;
5050 }
5051 
5052 
5053 void HTTPProtocol::cacheFileWritePayload(const QByteArray &d)
5054 {
5055  if (!m_request.cacheTag.file) {
5056  return;
5057  }
5058 
5059  // If the file being downloaded is so big that it exceeds the max cache size,
5060  // do not cache it! See BR# 244215. NOTE: this can be improved upon in the
5061  // future...
5062  if (m_iSize >= KIO::filesize_t(m_maxCacheSize * 1024)) {
5063  kDebug(7113) << "Caching disabled because content size is too big.";
5064  cacheFileClose();
5065  return;
5066  }
5067 
5068  Q_ASSERT(m_request.cacheTag.ioMode == WriteToCache);
5069  Q_ASSERT(m_request.cacheTag.file->openMode() & QIODevice::WriteOnly);
5070 
5071  if (d.isEmpty()) {
5072  cacheFileClose();
5073  }
5074 
5075  //TODO: abort if file grows too big!
5076 
5077  // write the variable length text header as soon as we start writing to the file
5078  if (!m_request.cacheTag.bytesCached) {
5079  cacheFileWriteTextHeader();
5080  }
5081  m_request.cacheTag.bytesCached += d.size();
5082  m_request.cacheTag.file->write(d);
5083 }
5084 
5085 void HTTPProtocol::cachePostData(const QByteArray& data)
5086 {
5087  if (!m_POSTbuf) {
5088  m_POSTbuf = createPostBufferDeviceFor(qMax(m_iPostDataSize, static_cast<KIO::filesize_t>(data.size())));
5089  if (!m_POSTbuf)
5090  return;
5091  }
5092 
5093  m_POSTbuf->write (data.constData(), data.size());
5094 }
5095 
5096 void HTTPProtocol::clearPostDataBuffer()
5097 {
5098  if (!m_POSTbuf)
5099  return;
5100 
5101  delete m_POSTbuf;
5102  m_POSTbuf = 0;
5103 }
5104 
5105 bool HTTPProtocol::retrieveAllData()
5106 {
5107  if (!m_POSTbuf) {
5108  m_POSTbuf = createPostBufferDeviceFor(s_MaxInMemPostBufSize + 1);
5109  }
5110 
5111  if (!m_POSTbuf) {
5112  error (ERR_OUT_OF_MEMORY, m_request.url.host());
5113  return false;
5114  }
5115 
5116  while (true) {
5117  dataReq();
5118  QByteArray buffer;
5119  const int bytesRead = readData(buffer);
5120 
5121  if (bytesRead < 0) {
5122  error(ERR_ABORTED, m_request.url.host());
5123  return false;
5124  }
5125 
5126  if (bytesRead == 0) {
5127  break;
5128  }
5129 
5130  m_POSTbuf->write(buffer.constData(), buffer.size());
5131  }
5132 
5133  return true;
5134 }
5135 
5136 // The above code should be kept in sync
5137 // with the code in http_cache_cleaner.cpp
5138 // !END SYNC!
5139 
5140 //************************** AUTHENTICATION CODE ********************/
5141 
5142 QString HTTPProtocol::authenticationHeader()
5143 {
5144  QByteArray ret;
5145 
5146  // If the internal meta-data "cached-www-auth" is set, then check for cached
5147  // authentication data and preemtively send the authentication header if a
5148  // matching one is found.
5149  if (!m_wwwAuth && config()->readEntry("cached-www-auth", false)) {
5150  KIO::AuthInfo authinfo;
5151  authinfo.url = m_request.url;
5152  authinfo.realmValue = config()->readEntry("www-auth-realm", QString());
5153  // If no relam metadata, then make sure path matching is turned on.
5154  authinfo.verifyPath = (authinfo.realmValue.isEmpty());
5155 
5156  const bool useCachedAuth = (m_request.responseCode == 401 || !config()->readEntry("no-preemptive-auth-reuse", false));
5157 
5158  if (useCachedAuth && checkCachedAuthentication(authinfo)) {
5159  const QByteArray cachedChallenge = config()->readEntry("www-auth-challenge", QByteArray());
5160  if (!cachedChallenge.isEmpty()) {
5161  m_wwwAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config());
5162  if (m_wwwAuth) {
5163  kDebug(7113) << "creating www authentcation header from cached info";
5164  m_wwwAuth->setChallenge(cachedChallenge, m_request.url, m_request.methodString());
5165  m_wwwAuth->generateResponse(authinfo.username, authinfo.password);
5166  }
5167  }
5168  }
5169  }
5170 
5171  // If the internal meta-data "cached-proxy-auth" is set, then check for cached
5172  // authentication data and preemtively send the authentication header if a
5173  // matching one is found.
5174  if (!m_proxyAuth && config()->readEntry("cached-proxy-auth", false)) {
5175  KIO::AuthInfo authinfo;
5176  authinfo.url = m_request.proxyUrl;
5177  authinfo.realmValue = config()->readEntry("proxy-auth-realm", QString());
5178  // If no relam metadata, then make sure path matching is turned on.
5179  authinfo.verifyPath = (authinfo.realmValue.isEmpty());
5180 
5181  if (checkCachedAuthentication(authinfo)) {
5182  const QByteArray cachedChallenge = config()->readEntry("proxy-auth-challenge", QByteArray());
5183  if (!cachedChallenge.isEmpty()) {
5184  m_proxyAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config());
5185  if (m_proxyAuth) {
5186  kDebug(7113) << "creating proxy authentcation header from cached info";
5187  m_proxyAuth->setChallenge(cachedChallenge, m_request.proxyUrl, m_request.methodString());
5188  m_proxyAuth->generateResponse(authinfo.username, authinfo.password);
5189  }
5190  }
5191  }
5192  }
5193 
5194  // the authentication classes don't know if they are for proxy or webserver authentication...
5195  if (m_wwwAuth && !m_wwwAuth->isError()) {
5196  ret += "Authorization: ";
5197  ret += m_wwwAuth->headerFragment();
5198  }
5199 
5200  if (m_proxyAuth && !m_proxyAuth->isError()) {
5201  ret += "Proxy-Authorization: ";
5202  ret += m_proxyAuth->headerFragment();
5203  }
5204 
5205  return toQString(ret); // ## encoding ok?
5206 }
5207 
5208 static QString protocolForProxyType(QNetworkProxy::ProxyType type)
5209 {
5210  switch (type) {
5211  case QNetworkProxy::DefaultProxy:
5212  break;
5213  case QNetworkProxy::Socks5Proxy:
5214  return QLatin1String("socks");
5215  case QNetworkProxy::NoProxy:
5216  break;
5217  case QNetworkProxy::HttpProxy:
5218  case QNetworkProxy::HttpCachingProxy:
5219  case QNetworkProxy::FtpCachingProxy:
5220  default:
5221  break;
5222  }
5223 
5224  return QLatin1String("http");
5225 }
5226 
5227 void HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator)
5228 {
5229  kDebug(7113) << "realm:" << authenticator->realm() << "user:" << authenticator->user();
5230 
5231  // Set the proxy URL...
5232  m_request.proxyUrl.setProtocol(protocolForProxyType(proxy.type()));
5233  m_request.proxyUrl.setUser(proxy.user());
5234  m_request.proxyUrl.setHost(proxy.hostName());
5235  m_request.proxyUrl.setPort(proxy.port());
5236 
5237  AuthInfo info;
5238  info.url = m_request.proxyUrl;
5239  info.realmValue = authenticator->realm();
5240  info.username = authenticator->user();
5241  info.verifyPath = info.realmValue.isEmpty();
5242 
5243  const bool haveCachedCredentials = checkCachedAuthentication(info);
5244  const bool retryAuth = (m_socketProxyAuth != 0);
5245 
5246  // if m_socketProxyAuth is a valid pointer then authentication has been attempted before,
5247  // and it was not successful. see below and saveProxyAuthenticationForSocket().
5248  if (!haveCachedCredentials || retryAuth) {
5249  // Save authentication info if the connection succeeds. We need to disconnect
5250  // this after saving the auth data (or an error) so we won't save garbage afterwards!
5251  connect(socket(), SIGNAL(connected()),
5252  this, SLOT(saveProxyAuthenticationForSocket()));
5253  //### fillPromptInfo(&info);
5254  info.prompt = i18n("You need to supply a username and a password for "
5255  "the proxy server listed below before you are allowed "
5256  "to access any sites.");
5257  info.keepPassword = true;
5258  info.commentLabel = i18n("Proxy:");
5259  info.comment = i18n("<b>%1</b> at <b>%2</b>", htmlEscape(info.realmValue), m_request.proxyUrl.host());
5260 
5261  const QString errMsg ((retryAuth ? i18n("Proxy Authentication Failed.") : QString()));
5262 
5263  if (!openPasswordDialog(info, errMsg)) {
5264  kDebug(7113) << "looks like the user canceled proxy authentication.";
5265  error(ERR_USER_CANCELED, m_request.proxyUrl.host());
5266  delete m_proxyAuth;
5267  m_proxyAuth = 0;
5268  return;
5269  }
5270  }
5271  authenticator->setUser(info.username);
5272  authenticator->setPassword(info.password);
5273  authenticator->setOption(QLatin1String("keepalive"), info.keepPassword);
5274 
5275  if (m_socketProxyAuth) {
5276  *m_socketProxyAuth = *authenticator;
5277  } else {
5278  m_socketProxyAuth = new QAuthenticator(*authenticator);
5279  }
5280 
5281  if (!m_request.proxyUrl.user().isEmpty()) {
5282  m_request.proxyUrl.setUser(info.username);
5283  }
5284 }
5285 
5286 void HTTPProtocol::saveProxyAuthenticationForSocket()
5287 {
5288  kDebug(7113) << "Saving authenticator";
5289  disconnect(socket(), SIGNAL(connected()),
5290  this, SLOT(saveProxyAuthenticationForSocket()));
5291  Q_ASSERT(m_socketProxyAuth);
5292  if (m_socketProxyAuth) {
5293  kDebug(7113) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user();
5294  KIO::AuthInfo a;
5295  a.verifyPath = true;
5296  a.url = m_request.proxyUrl;
5297  a.realmValue = m_socketProxyAuth->realm();
5298  a.username = m_socketProxyAuth->user();
5299  a.password = m_socketProxyAuth->password();
5300  a.keepPassword = m_socketProxyAuth->option(QLatin1String("keepalive")).toBool();
5301  cacheAuthentication(a);
5302  }
5303  delete m_socketProxyAuth;
5304  m_socketProxyAuth = 0;
5305 }
5306 
5307 void HTTPProtocol::saveAuthenticationData()
5308 {
5309  KIO::AuthInfo authinfo;
5310  bool alreadyCached = false;
5311  KAbstractHttpAuthentication *auth = 0;
5312  switch (m_request.prevResponseCode) {
5313  case 401:
5314  auth = m_wwwAuth;
5315  alreadyCached = config()->readEntry("cached-www-auth", false);
5316  break;
5317  case 407:
5318  auth = m_proxyAuth;
5319  alreadyCached = config()->readEntry("cached-proxy-auth", false);
5320  break;
5321  default:
5322  Q_ASSERT(false); // should never happen!
5323  }
5324 
5325  // Prevent recaching of the same credentials over and over again.
5326  if (auth && (!auth->realm().isEmpty() || !alreadyCached)) {
5327  auth->fillKioAuthInfo(&authinfo);
5328  if (auth == m_wwwAuth) {
5329  setMetaData(QLatin1String("{internal~currenthost}cached-www-auth"), QLatin1String("true"));
5330  if (!authinfo.realmValue.isEmpty())
5331  setMetaData(QLatin1String("{internal~currenthost}www-auth-realm"), authinfo.realmValue);
5332  if (!authinfo.digestInfo.isEmpty())
5333  setMetaData(QLatin1String("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo);
5334  } else {
5335  setMetaData(QLatin1String("{internal~allhosts}cached-proxy-auth"), QLatin1String("true"));
5336  if (!authinfo.realmValue.isEmpty())
5337  setMetaData(QLatin1String("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue);
5338  if (!authinfo.digestInfo.isEmpty())
5339  setMetaData(QLatin1String("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo);
5340  }
5341 
5342  kDebug(7113) << "Cache authentication info ?" << authinfo.keepPassword;
5343 
5344  if (authinfo.keepPassword) {
5345  cacheAuthentication(authinfo);
5346  kDebug(7113) << "Cached authentication for" << m_request.url;
5347  }
5348  }
5349  // Update our server connection state which includes www and proxy username and password.
5350  m_server.updateCredentials(m_request);
5351 }
5352 
5353 bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer* tokenizer)
5354 {
5355  KIO::AuthInfo authinfo;
5356  QList<QByteArray> authTokens;
5357  KAbstractHttpAuthentication **auth;
5358 
5359  if (m_request.responseCode == 401) {
5360  auth = &m_wwwAuth;
5361  authTokens = tokenizer->iterator("www-authenticate").all();
5362  authinfo.url = m_request.url;
5363  authinfo.username = m_server.url.user();
5364  authinfo.prompt = i18n("You need to supply a username and a "
5365  "password to access this site.");
5366  authinfo.commentLabel = i18n("Site:");
5367  } else {
5368  // make sure that the 407 header hasn't escaped a lower layer when it shouldn't.
5369  // this may break proxy chains which were never tested anyway, and AFAIK they are
5370  // rare to nonexistent in the wild.
5371  Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy);
5372  auth = &m_proxyAuth;
5373  authTokens = tokenizer->iterator("proxy-authenticate").all();
5374  authinfo.url = m_request.proxyUrl;
5375  authinfo.username = m_request.proxyUrl.user();
5376  authinfo.prompt = i18n("You need to supply a username and a password for "
5377  "the proxy server listed below before you are allowed "
5378  "to access any sites." );
5379  authinfo.commentLabel = i18n("Proxy:");
5380  }
5381 
5382  bool authRequiresAnotherRoundtrip = false;
5383 
5384  // Workaround brain dead server responses that violate the spec and
5385  // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate
5386  // header fields. See bug 215736...
5387  if (!authTokens.isEmpty()) {
5388  QString errorMsg;
5389  authRequiresAnotherRoundtrip = true;
5390 
5391  if (m_request.responseCode == m_request.prevResponseCode && *auth) {
5392  // Authentication attempt failed. Retry...
5393  if ((*auth)->wasFinalStage()) {
5394  errorMsg = (m_request.responseCode == 401 ?
5395  i18n("Authentication Failed.") :
5396  i18n("Proxy Authentication Failed."));
5397  delete *auth;
5398  *auth = 0;
5399  } else { // Create authentication header
5400  // WORKAROUND: The following piece of code prevents brain dead IIS
5401  // servers that send back multiple "WWW-Authenticate" headers from
5402  // screwing up our authentication logic during the challenge
5403  // phase (Type 2) of NTLM authenticaiton.
5404  QMutableListIterator<QByteArray> it (authTokens);
5405  const QByteArray authScheme ((*auth)->scheme().trimmed());
5406  while (it.hasNext()) {
5407  if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) {
5408  it.remove();
5409  }
5410  }
5411  }
5412  }
5413 
5414 try_next_auth_scheme:
5415  QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens);
5416  if (*auth) {
5417  const QByteArray authScheme ((*auth)->scheme().trimmed());
5418  if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) {
5419  // huh, the strongest authentication scheme offered has changed.
5420  delete *auth;
5421  *auth = 0;
5422  }
5423  }
5424 
5425  if (!(*auth)) {
5426  *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config());
5427  }
5428 
5429  if (*auth) {
5430  kDebug(7113) << "Trying authentication scheme:" << (*auth)->scheme();
5431 
5432  // remove trailing space from the method string, or digest auth will fail
5433  (*auth)->setChallenge(bestOffer, authinfo.url, m_request.methodString());
5434 
5435  QString username, password;
5436  bool generateAuthHeader = true;
5437  if ((*auth)->needCredentials()) {
5438  // use credentials supplied by the application if available
5439  if (!m_request.url.user().isEmpty() && !m_request.url.pass().isEmpty()) {
5440  username = m_request.url.user();
5441  password = m_request.url.pass();
5442  // don't try this password any more
5443  m_request.url.setPass(QString());
5444  } else {
5445  // try to get credentials from kpasswdserver's cache, then try asking the user.
5446  authinfo.verifyPath = false; // we have realm, no path based checking please!
5447  authinfo.realmValue = (*auth)->realm();
5448  if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching())
5449  authinfo.realmValue = QLatin1String((*auth)->scheme());
5450 
5451  // Save the current authinfo url because it can be modified by the call to
5452  // checkCachedAuthentication. That way we can restore it if the call
5453  // modified it.
5454  const KUrl reqUrl = authinfo.url;
5455  if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) {
5456  // Reset url to the saved url...
5457  authinfo.url = reqUrl;
5458  authinfo.keepPassword = true;
5459  authinfo.comment = i18n("<b>%1</b> at <b>%2</b>",
5460  htmlEscape(authinfo.realmValue), authinfo.url.host());
5461 
5462  if (!openPasswordDialog(authinfo, errorMsg)) {
5463  generateAuthHeader = false;
5464  authRequiresAnotherRoundtrip = false;
5465  if (!sendErrorPageNotification()) {
5466  error(ERR_ACCESS_DENIED, reqUrl.host());
5467  }
5468  kDebug(7113) << "looks like the user canceled the authentication dialog";
5469  delete *auth;
5470  *auth = 0;
5471  }
5472  }
5473  username = authinfo.username;
5474  password = authinfo.password;
5475  }
5476  }
5477 
5478  if (generateAuthHeader) {
5479  (*auth)->generateResponse(username, password);
5480  (*auth)->setCachePasswordEnabled(authinfo.keepPassword);
5481 
5482  kDebug(7113) << "isError=" << (*auth)->isError()
5483  << "needCredentials=" << (*auth)->needCredentials()
5484  << "forceKeepAlive=" << (*auth)->forceKeepAlive()
5485  << "forceDisconnect=" << (*auth)->forceDisconnect();
5486 
5487  if ((*auth)->isError()) {
5488  authTokens.removeOne(bestOffer);
5489  if (!authTokens.isEmpty()) {
5490  goto try_next_auth_scheme;
5491  } else {
5492  error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed."));
5493  authRequiresAnotherRoundtrip = false;
5494  }
5495  //### return false; ?
5496  } else if ((*auth)->forceKeepAlive()) {
5497  //### think this through for proxied / not proxied
5498  m_request.isKeepAlive = true;
5499  } else if ((*auth)->forceDisconnect()) {
5500  //### think this through for proxied / not proxied
5501  m_request.isKeepAlive = false;
5502  httpCloseConnection();
5503  }
5504  }
5505  } else {
5506  authRequiresAnotherRoundtrip = false;
5507  if (!sendErrorPageNotification()) {
5508  error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method."));
5509  }
5510  }
5511  }
5512 
5513  return authRequiresAnotherRoundtrip;
5514 }
5515 
5516 
5517 #include "http.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Mon Dec 10 2012 14:00:22 by doxygen 1.8.1.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KIOSlave

Skip menu "KIOSlave"
  • Main Page
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Related Pages

kdelibs-4.9.4 API Reference

Skip menu "kdelibs-4.9.4 API Reference"
  • DNSSD
  • Interfaces
  •   KHexEdit
  •   KMediaPlayer
  •   KSpeech
  •   KTextEditor
  • kconf_update
  • KDE3Support
  •   KUnitTest
  • KDECore
  • KDED
  • KDEsu
  • KDEUI
  • KDEWebKit
  • KDocTools
  • KFile
  • KHTML
  • KImgIO
  • KInit
  • kio
  • KIOSlave
  • KJS
  •   KJS-API
  •   WTF
  • kjsembed
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUnitConversion
  • KUtils
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver
Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal