2008 lines
71 KiB
C++
2008 lines
71 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
|
|
** All rights reserved.
|
|
** Contact: Nokia Corporation (qt-info@nokia.com)
|
|
**
|
|
** This file is part of the QtNetwork module of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:LGPL$
|
|
** No Commercial Usage
|
|
** This file contains pre-release code and may not be distributed.
|
|
** You may use this file in accordance with the terms and conditions
|
|
** contained in the Technology Preview License Agreement accompanying
|
|
** this package.
|
|
**
|
|
** GNU Lesser General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU Lesser
|
|
** General Public License version 2.1 as published by the Free Software
|
|
** Foundation and appearing in the file LICENSE.LGPL included in the
|
|
** packaging of this file. Please review the following information to
|
|
** ensure the GNU Lesser General Public License version 2.1 requirements
|
|
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
|
|
**
|
|
** In addition, as a special exception, Nokia gives you certain additional
|
|
** rights. These rights are described in the Nokia Qt LGPL Exception
|
|
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
|
|
**
|
|
** If you have questions regarding the use of this file, please contact
|
|
** Nokia at qt-info@nokia.com.
|
|
**
|
|
**
|
|
**
|
|
**
|
|
**
|
|
**
|
|
**
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
//#define QNETWORKACCESSHTTPBACKEND_DEBUG
|
|
|
|
#include "qnetworkreplyhttpimpl_p.h"
|
|
#include "qnetworkaccessmanager_p.h"
|
|
#include "qnetworkaccesscache_p.h"
|
|
#include "qabstractnetworkcache.h"
|
|
#include "qnetworkrequest.h"
|
|
#include "qnetworkreply.h"
|
|
#include "qnetworkrequest_p.h"
|
|
#include "qnetworkcookie_p.h"
|
|
#include "QtCore/qdatetime.h"
|
|
#include "QtCore/qelapsedtimer.h"
|
|
#include "QtNetwork/qsslconfiguration.h"
|
|
#include "qhttpthreaddelegate_p.h"
|
|
#include "qthread.h"
|
|
#include "QtCore/qcoreapplication.h"
|
|
|
|
#include "qnetworkcookiejar.h"
|
|
|
|
#ifndef QT_NO_HTTP
|
|
|
|
#include <string.h> // for strchr
|
|
|
|
Q_DECLARE_METATYPE(QSharedPointer<char>)
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
class QNetworkProxy;
|
|
|
|
static inline bool isSeparator(register char c)
|
|
{
|
|
static const char separators[] = "()<>@,;:\\\"/[]?={}";
|
|
return isLWS(c) || strchr(separators, c) != 0;
|
|
}
|
|
|
|
// ### merge with nextField in cookiejar.cpp
|
|
static QHash<QByteArray, QByteArray> parseHttpOptionHeader(const QByteArray &header)
|
|
{
|
|
// The HTTP header is of the form:
|
|
// header = #1(directives)
|
|
// directives = token | value-directive
|
|
// value-directive = token "=" (token | quoted-string)
|
|
QHash<QByteArray, QByteArray> result;
|
|
|
|
int pos = 0;
|
|
while (true) {
|
|
// skip spaces
|
|
pos = nextNonWhitespace(header, pos);
|
|
if (pos == header.length())
|
|
return result; // end of parsing
|
|
|
|
// pos points to a non-whitespace
|
|
int comma = header.indexOf(',', pos);
|
|
int equal = header.indexOf('=', pos);
|
|
if (comma == pos || equal == pos)
|
|
// huh? Broken header.
|
|
return result;
|
|
|
|
// The key name is delimited by either a comma, an equal sign or the end
|
|
// of the header, whichever comes first
|
|
int end = comma;
|
|
if (end == -1)
|
|
end = header.length();
|
|
if (equal != -1 && end > equal)
|
|
end = equal; // equal sign comes before comma/end
|
|
QByteArray key = QByteArray(header.constData() + pos, end - pos).trimmed().toLower();
|
|
pos = end + 1;
|
|
|
|
if (uint(equal) < uint(comma)) {
|
|
// case: token "=" (token | quoted-string)
|
|
// skip spaces
|
|
pos = nextNonWhitespace(header, pos);
|
|
if (pos == header.length())
|
|
// huh? Broken header
|
|
return result;
|
|
|
|
QByteArray value;
|
|
value.reserve(header.length() - pos);
|
|
if (header.at(pos) == '"') {
|
|
// case: quoted-string
|
|
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
|
// qdtext = <any TEXT except <">>
|
|
// quoted-pair = "\" CHAR
|
|
++pos;
|
|
while (pos < header.length()) {
|
|
register char c = header.at(pos);
|
|
if (c == '"') {
|
|
// end of quoted text
|
|
break;
|
|
} else if (c == '\\') {
|
|
++pos;
|
|
if (pos >= header.length())
|
|
// broken header
|
|
return result;
|
|
c = header.at(pos);
|
|
}
|
|
|
|
value += c;
|
|
++pos;
|
|
}
|
|
} else {
|
|
// case: token
|
|
while (pos < header.length()) {
|
|
register char c = header.at(pos);
|
|
if (isSeparator(c))
|
|
break;
|
|
value += c;
|
|
++pos;
|
|
}
|
|
}
|
|
|
|
result.insert(key, value);
|
|
|
|
// find the comma now:
|
|
comma = header.indexOf(',', pos);
|
|
if (comma == -1)
|
|
return result; // end of parsing
|
|
pos = comma + 1;
|
|
} else {
|
|
// case: token
|
|
// key is already set
|
|
result.insert(key, QByteArray());
|
|
}
|
|
}
|
|
}
|
|
|
|
QNetworkReplyHttpImpl::QNetworkReplyHttpImpl(QNetworkAccessManager* const manager,
|
|
const QNetworkRequest& request,
|
|
QNetworkAccessManager::Operation& operation,
|
|
QIODevice* outgoingData)
|
|
: QNetworkReply(*new QNetworkReplyHttpImplPrivate, manager)
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
d->manager = manager;
|
|
d->managerPrivate = manager->d_func();
|
|
d->request = request;
|
|
d->operation = operation;
|
|
d->outgoingData = outgoingData;
|
|
d->url = request.url();
|
|
#ifndef QT_NO_OPENSSL
|
|
d->sslConfiguration = request.sslConfiguration();
|
|
#endif
|
|
|
|
// FIXME Later maybe set to Unbuffered, especially if it is zerocopy or from cache?
|
|
QIODevice::open(QIODevice::ReadOnly);
|
|
|
|
|
|
// Internal code that does a HTTP reply for the synchronous Ajax
|
|
// in QtWebKit.
|
|
QVariant synchronousHttpAttribute = request.attribute(
|
|
static_cast<QNetworkRequest::Attribute>(QNetworkRequest::SynchronousRequestAttribute));
|
|
if (synchronousHttpAttribute.isValid()) {
|
|
d->synchronous = synchronousHttpAttribute.toBool();
|
|
if (d->synchronous && outgoingData) {
|
|
// The synchronous HTTP is a corner case, we will put all upload data in one big QByteArray in the outgoingDataBuffer.
|
|
// Yes, this is not the most efficient thing to do, but on the other hand synchronous XHR needs to die anyway.
|
|
d->outgoingDataBuffer = QSharedPointer<QRingBuffer>(new QRingBuffer());
|
|
qint64 previousDataSize = 0;
|
|
do {
|
|
previousDataSize = d->outgoingDataBuffer->size();
|
|
d->outgoingDataBuffer->append(d->outgoingData->readAll());
|
|
} while (d->outgoingDataBuffer->size() != previousDataSize);
|
|
d->_q_startOperation();
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
if (outgoingData) {
|
|
// there is data to be uploaded, e.g. HTTP POST.
|
|
|
|
if (!d->outgoingData->isSequential()) {
|
|
// fixed size non-sequential (random-access)
|
|
// just start the operation
|
|
QMetaObject::invokeMethod(this, "_q_startOperation", Qt::QueuedConnection);
|
|
// FIXME make direct call?
|
|
} else {
|
|
bool bufferingDisallowed =
|
|
request.attribute(QNetworkRequest::DoNotBufferUploadDataAttribute,
|
|
false).toBool();
|
|
|
|
if (bufferingDisallowed) {
|
|
// if a valid content-length header for the request was supplied, we can disable buffering
|
|
// if not, we will buffer anyway
|
|
if (request.header(QNetworkRequest::ContentLengthHeader).isValid()) {
|
|
QMetaObject::invokeMethod(this, "_q_startOperation", Qt::QueuedConnection);
|
|
// FIXME make direct call?
|
|
} else {
|
|
d->state = d->Buffering;
|
|
QMetaObject::invokeMethod(this, "_q_bufferOutgoingData", Qt::QueuedConnection);
|
|
}
|
|
} else {
|
|
// _q_startOperation will be called when the buffering has finished.
|
|
d->state = d->Buffering;
|
|
QMetaObject::invokeMethod(this, "_q_bufferOutgoingData", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
} else {
|
|
// No outgoing data (POST, ..)
|
|
d->_q_startOperation();
|
|
}
|
|
}
|
|
|
|
QNetworkReplyHttpImpl::~QNetworkReplyHttpImpl()
|
|
{
|
|
// Most work is done in private destructor
|
|
}
|
|
|
|
void QNetworkReplyHttpImpl::close()
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
|
|
if (d->state == QNetworkReplyHttpImplPrivate::Aborted ||
|
|
d->state == QNetworkReplyHttpImplPrivate::Finished)
|
|
return;
|
|
|
|
// According to the documentation close only stops the download
|
|
// by closing we can ignore the download part and continue uploading.
|
|
QNetworkReply::close();
|
|
|
|
// call finished which will emit signals
|
|
// FIXME shouldn't this be emitted Queued?
|
|
d->error(OperationCanceledError, tr("Operation canceled"));
|
|
d->finished();
|
|
}
|
|
|
|
void QNetworkReplyHttpImpl::abort()
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
// FIXME
|
|
if (d->state == QNetworkReplyHttpImplPrivate::Finished || d->state == QNetworkReplyHttpImplPrivate::Aborted)
|
|
return;
|
|
|
|
QNetworkReply::close();
|
|
|
|
if (d->state != QNetworkReplyHttpImplPrivate::Finished) {
|
|
// call finished which will emit signals
|
|
// FIXME shouldn't this be emitted Queued?
|
|
d->error(OperationCanceledError, tr("Operation canceled"));
|
|
d->finished();
|
|
}
|
|
|
|
d->state = QNetworkReplyHttpImplPrivate::Aborted;
|
|
|
|
emit abortHttpRequest();
|
|
}
|
|
|
|
qint64 QNetworkReplyHttpImpl::bytesAvailable() const
|
|
{
|
|
Q_D(const QNetworkReplyHttpImpl);
|
|
|
|
// if we load from cache device
|
|
if (d->cacheLoadDevice) {
|
|
return QNetworkReply::bytesAvailable() + d->cacheLoadDevice->bytesAvailable() + d->downloadMultiBuffer.byteAmount();
|
|
}
|
|
|
|
// zerocopy buffer
|
|
if (d->downloadZerocopyBuffer) {
|
|
return QNetworkReply::bytesAvailable() + d->downloadBufferCurrentSize - d->downloadBufferReadPosition;
|
|
}
|
|
|
|
// normal buffer
|
|
return QNetworkReply::bytesAvailable() + d->downloadMultiBuffer.byteAmount();
|
|
}
|
|
|
|
bool QNetworkReplyHttpImpl::isSequential () const
|
|
{
|
|
// FIXME In the cache of a cached load or the zero-copy buffer we could actually be non-sequential.
|
|
// FIXME however this requires us to implement stuff like seek() too.
|
|
return true;
|
|
}
|
|
|
|
qint64 QNetworkReplyHttpImpl::size() const
|
|
{
|
|
// FIXME At some point, this could return a proper value, e.g. if we're non-sequential.
|
|
return QNetworkReply::size();
|
|
}
|
|
|
|
qint64 QNetworkReplyHttpImpl::readData(char* data, qint64 maxlen)
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
qDebug() << "QNetworkReplyHttpImpl::readData()" << maxlen;
|
|
|
|
// FIXME cacheload device
|
|
if (d->cacheLoadDevice) {
|
|
// FIXME bytesdownloaded, position etc?
|
|
|
|
// There is something already in the buffer we buffered before because the user did not read()
|
|
// anything, so we read there first:
|
|
if (!d->downloadMultiBuffer.isEmpty()) {
|
|
return d->downloadMultiBuffer.read(data, maxlen);
|
|
}
|
|
|
|
qint64 ret = d->cacheLoadDevice->read(data, maxlen);
|
|
return ret;
|
|
}
|
|
|
|
// FIXME 0-copy buffer
|
|
if (d->downloadZerocopyBuffer) {
|
|
// bla
|
|
qint64 howMuch = qMin(maxlen, (d->downloadBufferCurrentSize - d->downloadBufferReadPosition));
|
|
memcpy(data, d->downloadZerocopyBuffer + d->downloadBufferReadPosition, howMuch);
|
|
d->downloadBufferReadPosition += howMuch;
|
|
return howMuch;
|
|
|
|
}
|
|
|
|
// FIXME normal buffer
|
|
if (d->downloadMultiBuffer.isEmpty())
|
|
return d->state == d->Finished ? -1 : 0;
|
|
// FIXME what about "Aborted" state?
|
|
|
|
//d->backendNotify(QNetworkReplyImplPrivate::NotifyDownstreamReadyWrite);
|
|
if (maxlen == 1) {
|
|
// optimization for getChar()
|
|
*data = d->downloadMultiBuffer.getChar();
|
|
return 1;
|
|
}
|
|
|
|
maxlen = qMin<qint64>(maxlen, d->downloadMultiBuffer.byteAmount());
|
|
return d->downloadMultiBuffer.read(data, maxlen);
|
|
}
|
|
|
|
void QNetworkReplyHttpImpl::setReadBufferSize(qint64 size)
|
|
{
|
|
return; // FIXME, unsupported right now
|
|
}
|
|
|
|
bool QNetworkReplyHttpImpl::canReadLine () const
|
|
{
|
|
Q_D(const QNetworkReplyHttpImpl);
|
|
|
|
if (QNetworkReply::canReadLine())
|
|
return true;
|
|
|
|
if (d->cacheLoadDevice)
|
|
return d->cacheLoadDevice->canReadLine() || d->downloadMultiBuffer.canReadLine();
|
|
|
|
return d->downloadMultiBuffer.canReadLine();
|
|
}
|
|
|
|
#ifndef QT_NO_OPENSSL
|
|
void QNetworkReplyHttpImpl::ignoreSslErrors()
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
|
|
d->pendingIgnoreAllSslErrors = true;
|
|
}
|
|
|
|
void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError> &errors)
|
|
{
|
|
Q_D(QNetworkReplyHttpImpl);
|
|
|
|
// the pending list is set if QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
|
|
// is called before QNetworkAccessManager::get() (or post(), etc.)
|
|
d->pendingIgnoreSslErrorsList = errors;
|
|
}
|
|
|
|
void QNetworkReplyHttpImpl::setSslConfigurationImplementation(const QSslConfiguration &newconfig)
|
|
{
|
|
// Setting a SSL configuration on a reply is not supported. The user needs to set
|
|
// her/his QSslConfiguration on the QNetworkRequest.
|
|
Q_UNUSED(newconfig);
|
|
}
|
|
|
|
QSslConfiguration QNetworkReplyHttpImpl::sslConfigurationImplementation() const
|
|
{
|
|
Q_D(const QNetworkReplyHttpImpl);
|
|
qDebug() << "sslConfigurationImplementation";
|
|
return d->sslConfiguration;
|
|
}
|
|
#endif
|
|
|
|
QNetworkReplyHttpImplPrivate::QNetworkReplyHttpImplPrivate()
|
|
// FIXME order etc
|
|
: QNetworkReplyPrivate()
|
|
|
|
, manager(0)
|
|
, managerPrivate(0)
|
|
, synchronous(false)
|
|
|
|
, state(Idle)
|
|
|
|
, statusCode(0)
|
|
|
|
, outgoingData(0)
|
|
|
|
, bytesUploaded(-1)
|
|
|
|
|
|
, cacheLoadDevice(0)
|
|
, loadingFromCache(false)
|
|
|
|
, cacheSaveDevice(0)
|
|
, cacheEnabled(false)
|
|
|
|
|
|
, resumeOffset(0)
|
|
, preMigrationDownloaded(-1)
|
|
|
|
, bytesDownloaded(0)
|
|
, lastBytesDownloaded(-1)
|
|
, downloadBufferReadPosition(0)
|
|
, downloadBufferCurrentSize(0)
|
|
, downloadBufferMaximumSize(0)
|
|
, downloadZerocopyBuffer(0)
|
|
, pendingDownloadDataEmissions(new QAtomicInt())
|
|
, pendingDownloadProgressEmissions(new QAtomicInt())
|
|
#ifndef QT_NO_OPENSSL
|
|
, pendingIgnoreAllSslErrors(false)
|
|
#endif
|
|
|
|
{
|
|
}
|
|
|
|
QNetworkReplyHttpImplPrivate::~QNetworkReplyHttpImplPrivate()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
// This will do nothing if the request was already finished or aborted
|
|
emit q->abortHttpRequest();
|
|
}
|
|
|
|
/*
|
|
For a given httpRequest
|
|
1) If AlwaysNetwork, return
|
|
2) If we have a cache entry for this url populate headers so the server can return 304
|
|
3) Calculate if response_is_fresh and if so send the cache and set loadedFromCache to true
|
|
*/
|
|
bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &httpRequest)
|
|
{
|
|
QNetworkRequest::CacheLoadControl CacheLoadControlAttribute =
|
|
(QNetworkRequest::CacheLoadControl)request.attribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferNetwork).toInt();
|
|
if (CacheLoadControlAttribute == QNetworkRequest::AlwaysNetwork) {
|
|
// If the request does not already specify preferred cache-control
|
|
// force reload from the network and tell any caching proxy servers to reload too
|
|
if (!request.rawHeaderList().contains("Cache-Control")) {
|
|
httpRequest.setHeaderField("Cache-Control", "no-cache");
|
|
httpRequest.setHeaderField("Pragma", "no-cache");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The disk cache API does not currently support partial content retrieval.
|
|
// That is why we don't use the disk cache for any such requests.
|
|
if (request.hasRawHeader("Range"))
|
|
return false;
|
|
|
|
QAbstractNetworkCache *nc = managerPrivate->networkCache;
|
|
if (!nc)
|
|
return false; // no local cache
|
|
|
|
QNetworkCacheMetaData metaData = nc->metaData(request.url());
|
|
if (!metaData.isValid())
|
|
return false; // not in cache
|
|
|
|
if (!metaData.saveToDisk())
|
|
return false;
|
|
|
|
QNetworkHeadersPrivate cacheHeaders;
|
|
QNetworkHeadersPrivate::RawHeadersList::ConstIterator it;
|
|
cacheHeaders.setAllRawHeaders(metaData.rawHeaders());
|
|
|
|
it = cacheHeaders.findRawHeader("etag");
|
|
if (it != cacheHeaders.rawHeaders.constEnd())
|
|
httpRequest.setHeaderField("If-None-Match", it->second);
|
|
|
|
QDateTime lastModified = metaData.lastModified();
|
|
if (lastModified.isValid())
|
|
httpRequest.setHeaderField("If-Modified-Since", QNetworkHeadersPrivate::toHttpDate(lastModified));
|
|
|
|
if (CacheLoadControlAttribute == QNetworkRequest::PreferNetwork) {
|
|
it = cacheHeaders.findRawHeader("Cache-Control");
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(it->second);
|
|
if (cacheControl.contains("must-revalidate"))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QDateTime currentDateTime = QDateTime::currentDateTime();
|
|
QDateTime expirationDate = metaData.expirationDate();
|
|
|
|
#if 0
|
|
/*
|
|
* age_value
|
|
* is the value of Age: header received by the cache with
|
|
* this response.
|
|
* date_value
|
|
* is the value of the origin server's Date: header
|
|
* request_time
|
|
* is the (local) time when the cache made the request
|
|
* that resulted in this cached response
|
|
* response_time
|
|
* is the (local) time when the cache received the
|
|
* response
|
|
* now
|
|
* is the current (local) time
|
|
*/
|
|
int age_value = 0;
|
|
it = cacheHeaders.findRawHeader("age");
|
|
if (it != cacheHeaders.rawHeaders.constEnd())
|
|
age_value = it->second.toInt();
|
|
|
|
QDateTime dateHeader;
|
|
int date_value = 0;
|
|
it = cacheHeaders.findRawHeader("date");
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
dateHeader = QNetworkHeadersPrivate::fromHttpDate(it->second);
|
|
date_value = dateHeader.toTime_t();
|
|
}
|
|
|
|
int now = currentDateTime.toUTC().toTime_t();
|
|
int request_time = now;
|
|
int response_time = now;
|
|
|
|
// Algorithm from RFC 2616 section 13.2.3
|
|
int apparent_age = qMax(0, response_time - date_value);
|
|
int corrected_received_age = qMax(apparent_age, age_value);
|
|
int response_delay = response_time - request_time;
|
|
int corrected_initial_age = corrected_received_age + response_delay;
|
|
int resident_time = now - response_time;
|
|
int current_age = corrected_initial_age + resident_time;
|
|
|
|
// RFC 2616 13.2.4 Expiration Calculations
|
|
if (!expirationDate.isValid()) {
|
|
if (lastModified.isValid()) {
|
|
int diff = currentDateTime.secsTo(lastModified);
|
|
expirationDate = lastModified;
|
|
expirationDate.addSecs(diff / 10);
|
|
if (httpRequest.headerField("Warning").isEmpty()) {
|
|
QDateTime dt;
|
|
dt.setTime_t(current_age);
|
|
if (dt.daysTo(currentDateTime) > 1)
|
|
httpRequest.setHeaderField("Warning", "113");
|
|
}
|
|
}
|
|
}
|
|
|
|
// the cache-saving code below sets the expirationDate with date+max_age
|
|
// if "max-age" is present, or to Expires otherwise
|
|
int freshness_lifetime = dateHeader.secsTo(expirationDate);
|
|
bool response_is_fresh = (freshness_lifetime > current_age);
|
|
#else
|
|
bool response_is_fresh = currentDateTime.secsTo(expirationDate) >= 0;
|
|
#endif
|
|
|
|
if (!response_is_fresh)
|
|
return false;
|
|
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
qDebug() << "response_is_fresh" << CacheLoadControlAttribute;
|
|
#endif
|
|
return sendCacheContents(metaData);
|
|
}
|
|
|
|
QHttpNetworkRequest::Priority QNetworkReplyHttpImplPrivate::convert(const QNetworkRequest::Priority& prio)
|
|
{
|
|
switch (prio) {
|
|
case QNetworkRequest::LowPriority:
|
|
return QHttpNetworkRequest::LowPriority;
|
|
case QNetworkRequest::HighPriority:
|
|
return QHttpNetworkRequest::HighPriority;
|
|
case QNetworkRequest::NormalPriority:
|
|
default:
|
|
return QHttpNetworkRequest::NormalPriority;
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::postRequest()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
QThread *thread = 0;
|
|
if (synchronous) {
|
|
// A synchronous HTTP request uses its own thread
|
|
qDebug() << "sync!";
|
|
thread = new QThread();
|
|
QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
|
|
thread->start();
|
|
} else if (!managerPrivate->httpThread) {
|
|
// We use the manager-global thread.
|
|
// At some point we could switch to having multiple threads if it makes sense.
|
|
managerPrivate->httpThread = new QThread();
|
|
QObject::connect(managerPrivate->httpThread, SIGNAL(finished()), managerPrivate->httpThread, SLOT(deleteLater()));
|
|
managerPrivate->httpThread->start();
|
|
|
|
thread = managerPrivate->httpThread;
|
|
} else {
|
|
// Asynchronous request, thread already exists
|
|
thread = managerPrivate->httpThread;
|
|
}
|
|
|
|
QUrl url = request.url();
|
|
httpRequest.setUrl(url);
|
|
|
|
bool ssl = url.scheme().toLower() == QLatin1String("https");
|
|
q->setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, ssl);
|
|
httpRequest.setSsl(ssl);
|
|
|
|
|
|
#ifndef QT_NO_NETWORKPROXY
|
|
QNetworkProxy transparentProxy, cacheProxy;
|
|
|
|
// FIXME the proxy stuff should be done in the HTTP thread
|
|
foreach (const QNetworkProxy &p, managerPrivate->queryProxy(QNetworkProxyQuery(request.url()))) {
|
|
//foreach (const QNetworkProxy &p, proxyList()) {
|
|
// use the first proxy that works
|
|
// for non-encrypted connections, any transparent or HTTP proxy
|
|
// for encrypted, only transparent proxies
|
|
if (!ssl
|
|
&& (p.capabilities() & QNetworkProxy::CachingCapability)
|
|
&& (p.type() == QNetworkProxy::HttpProxy ||
|
|
p.type() == QNetworkProxy::HttpCachingProxy)) {
|
|
cacheProxy = p;
|
|
transparentProxy = QNetworkProxy::NoProxy;
|
|
break;
|
|
}
|
|
if (p.isTransparentProxy()) {
|
|
transparentProxy = p;
|
|
cacheProxy = QNetworkProxy::NoProxy;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if at least one of the proxies
|
|
if (transparentProxy.type() == QNetworkProxy::DefaultProxy &&
|
|
cacheProxy.type() == QNetworkProxy::DefaultProxy) {
|
|
// unsuitable proxies
|
|
QMetaObject::invokeMethod(q, "_q_error", synchronous ? Qt::DirectConnection : Qt::QueuedConnection,
|
|
Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ProxyNotFoundError),
|
|
Q_ARG(QString, q->tr("No suitable proxy found")));
|
|
QMetaObject::invokeMethod(q, "_q_finished", synchronous ? Qt::DirectConnection : Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
|
|
bool loadedFromCache = false;
|
|
httpRequest.setPriority(convert(request.priority()));
|
|
|
|
switch (operation) {
|
|
case QNetworkAccessManager::GetOperation:
|
|
httpRequest.setOperation(QHttpNetworkRequest::Get);
|
|
loadedFromCache = loadFromCacheIfAllowed(httpRequest);
|
|
break;
|
|
|
|
case QNetworkAccessManager::HeadOperation:
|
|
httpRequest.setOperation(QHttpNetworkRequest::Head);
|
|
loadedFromCache = loadFromCacheIfAllowed(httpRequest);
|
|
break;
|
|
|
|
case QNetworkAccessManager::PostOperation:
|
|
invalidateCache();
|
|
httpRequest.setOperation(QHttpNetworkRequest::Post);
|
|
createUploadByteDevice();
|
|
break;
|
|
|
|
case QNetworkAccessManager::PutOperation:
|
|
invalidateCache();
|
|
httpRequest.setOperation(QHttpNetworkRequest::Put);
|
|
createUploadByteDevice();
|
|
break;
|
|
|
|
case QNetworkAccessManager::DeleteOperation:
|
|
invalidateCache();
|
|
httpRequest.setOperation(QHttpNetworkRequest::Delete);
|
|
break;
|
|
|
|
case QNetworkAccessManager::CustomOperation:
|
|
invalidateCache(); // for safety reasons, we don't know what the operation does
|
|
httpRequest.setOperation(QHttpNetworkRequest::Custom);
|
|
createUploadByteDevice();
|
|
httpRequest.setCustomVerb(request.attribute(
|
|
QNetworkRequest::CustomVerbAttribute).toByteArray());
|
|
break;
|
|
|
|
default:
|
|
break; // can't happen
|
|
}
|
|
|
|
if (loadedFromCache) {
|
|
return; // no need to send the request! :)
|
|
}
|
|
|
|
QList<QByteArray> headers = request.rawHeaderList();
|
|
if (resumeOffset != 0) {
|
|
if (headers.contains("Range")) {
|
|
// Need to adjust resume offset for user specified range
|
|
|
|
headers.removeOne("Range");
|
|
|
|
// We've already verified that requestRange starts with "bytes=", see canResume.
|
|
QByteArray requestRange = request.rawHeader("Range").mid(6);
|
|
|
|
int index = requestRange.indexOf('-');
|
|
|
|
quint64 requestStartOffset = requestRange.left(index).toULongLong();
|
|
quint64 requestEndOffset = requestRange.mid(index + 1).toULongLong();
|
|
|
|
requestRange = "bytes=" + QByteArray::number(resumeOffset + requestStartOffset) +
|
|
'-' + QByteArray::number(requestEndOffset);
|
|
|
|
httpRequest.setHeaderField("Range", requestRange);
|
|
} else {
|
|
httpRequest.setHeaderField("Range", "bytes=" + QByteArray::number(resumeOffset) + '-');
|
|
}
|
|
}
|
|
|
|
foreach (const QByteArray &header, headers)
|
|
httpRequest.setHeaderField(header, request.rawHeader(header));
|
|
|
|
if (request.attribute(QNetworkRequest::HttpPipeliningAllowedAttribute).toBool() == true)
|
|
httpRequest.setPipeliningAllowed(true);
|
|
|
|
if (static_cast<QNetworkRequest::LoadControl>
|
|
(request.attribute(QNetworkRequest::AuthenticationReuseAttribute,
|
|
QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual)
|
|
httpRequest.setWithCredentials(false);
|
|
|
|
|
|
// Create the HTTP thread delegate
|
|
QHttpThreadDelegate *delegate = new QHttpThreadDelegate;
|
|
|
|
// For the synchronous HTTP, this is the normal way the delegate gets deleted
|
|
// For the asynchronous HTTP this is a safety measure, the delegate deletes itself when HTTP is finished
|
|
QObject::connect(thread, SIGNAL(finished()), delegate, SLOT(deleteLater()));
|
|
|
|
// Set the properties it needs
|
|
delegate->httpRequest = httpRequest;
|
|
#ifndef QT_NO_NETWORKPROXY
|
|
delegate->cacheProxy = cacheProxy;
|
|
delegate->transparentProxy = transparentProxy;
|
|
#endif
|
|
delegate->ssl = ssl;
|
|
#ifndef QT_NO_OPENSSL
|
|
if (ssl)
|
|
delegate->incomingSslConfiguration = request.sslConfiguration();
|
|
#endif
|
|
|
|
// Do we use synchronous HTTP?
|
|
delegate->synchronous = synchronous;
|
|
|
|
// The authentication manager is used to avoid the BlockingQueuedConnection communication
|
|
// from HTTP thread to user thread in some cases.
|
|
delegate->authenticationManager = managerPrivate->authenticationManager;
|
|
|
|
if (!synchronous) {
|
|
// Tell our zerocopy policy to the delegate
|
|
delegate->downloadBufferMaximumSize =
|
|
request.attribute(QNetworkRequest::MaximumDownloadBufferSizeAttribute).toLongLong();
|
|
|
|
// These atomic integers are used for signal compression
|
|
delegate->pendingDownloadData = pendingDownloadDataEmissions;
|
|
delegate->pendingDownloadProgress = pendingDownloadProgressEmissions;
|
|
|
|
// Connect the signals of the delegate to us
|
|
QObject::connect(delegate, SIGNAL(downloadData(QByteArray)),
|
|
q, SLOT(replyDownloadData(QByteArray)),
|
|
Qt::QueuedConnection);
|
|
QObject::connect(delegate, SIGNAL(downloadFinished()),
|
|
q, SLOT(replyFinished()),
|
|
Qt::QueuedConnection);
|
|
QObject::connect(delegate, SIGNAL(downloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64)),
|
|
q, SLOT(replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64)),
|
|
Qt::QueuedConnection);
|
|
QObject::connect(delegate, SIGNAL(downloadProgress(qint64,qint64)),
|
|
q, SLOT(replyDownloadProgressSlot(qint64,qint64)),
|
|
Qt::QueuedConnection);
|
|
QObject::connect(delegate, SIGNAL(error(QNetworkReply::NetworkError,QString)),
|
|
q, SLOT(httpError(QNetworkReply::NetworkError, const QString)),
|
|
Qt::QueuedConnection);
|
|
#ifndef QT_NO_OPENSSL
|
|
QObject::connect(delegate, SIGNAL(sslConfigurationChanged(QSslConfiguration)),
|
|
q, SLOT(replySslConfigurationChanged(QSslConfiguration)),
|
|
Qt::QueuedConnection);
|
|
#endif
|
|
// Those need to report back, therefire BlockingQueuedConnection
|
|
QObject::connect(delegate, SIGNAL(authenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
|
|
q, SLOT(httpAuthenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
|
|
Qt::BlockingQueuedConnection);
|
|
#ifndef QT_NO_NETWORKPROXY
|
|
QObject::connect(delegate, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
|
|
q, SLOT(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
|
|
Qt::BlockingQueuedConnection);
|
|
#endif
|
|
#ifndef QT_NO_OPENSSL
|
|
QObject::connect(delegate, SIGNAL(sslErrors(QList<QSslError>,bool*,QList<QSslError>*)),
|
|
q, SLOT(replySslErrors(const QList<QSslError> &, bool *, QList<QSslError> *)),
|
|
Qt::BlockingQueuedConnection);
|
|
#endif
|
|
// This signal we will use to start the request.
|
|
QObject::connect(q, SIGNAL(startHttpRequest()), delegate, SLOT(startRequest()));
|
|
QObject::connect(q, SIGNAL(abortHttpRequest()), delegate, SLOT(abortRequest()));
|
|
|
|
if (uploadByteDevice) {
|
|
QNonContiguousByteDeviceThreadForwardImpl *forwardUploadDevice =
|
|
new QNonContiguousByteDeviceThreadForwardImpl(uploadByteDevice->atEnd(), uploadByteDevice->size());
|
|
if (uploadByteDevice->isResetDisabled())
|
|
forwardUploadDevice->disableReset();
|
|
forwardUploadDevice->setParent(delegate); // needed to make sure it is moved on moveToThread()
|
|
delegate->httpRequest.setUploadByteDevice(forwardUploadDevice);
|
|
|
|
// From main thread to user thread:
|
|
QObject::connect(q, SIGNAL(haveUploadData(QByteArray, bool, qint64)),
|
|
forwardUploadDevice, SLOT(haveDataSlot(QByteArray, bool, qint64)), Qt::QueuedConnection);
|
|
QObject::connect(uploadByteDevice.data(), SIGNAL(readyRead()),
|
|
forwardUploadDevice, SIGNAL(readyRead()),
|
|
Qt::QueuedConnection);
|
|
|
|
// From http thread to user thread:
|
|
QObject::connect(forwardUploadDevice, SIGNAL(wantData(qint64)),
|
|
q, SLOT(wantUploadDataSlot(qint64)));
|
|
QObject::connect(forwardUploadDevice, SIGNAL(processedData(qint64)),
|
|
q, SLOT(sentUploadDataSlot(qint64)));
|
|
QObject::connect(forwardUploadDevice, SIGNAL(resetData(bool*)),
|
|
q, SLOT(resetUploadDataSlot(bool*)),
|
|
Qt::BlockingQueuedConnection); // this is the only one with BlockingQueued!
|
|
}
|
|
} else if (synchronous) {
|
|
QObject::connect(q, SIGNAL(startHttpRequestSynchronously()), delegate, SLOT(startRequestSynchronously()), Qt::BlockingQueuedConnection);
|
|
|
|
if (uploadByteDevice) {
|
|
// For the synchronous HTTP use case the use thread (this one here) is blocked
|
|
// so we cannot use the asynchronous upload architecture.
|
|
// We therefore won't use the QNonContiguousByteDeviceThreadForwardImpl but directly
|
|
// use the uploadByteDevice provided to us by the QNetworkReplyImpl.
|
|
// The code that is in start() makes sure it is safe to use from a thread
|
|
// since it only wraps a QRingBuffer
|
|
delegate->httpRequest.setUploadByteDevice(uploadByteDevice.data());
|
|
}
|
|
}
|
|
|
|
|
|
// Move the delegate to the http thread
|
|
delegate->moveToThread(thread);
|
|
// This call automatically moves the uploadDevice too for the asynchronous case.
|
|
|
|
// Send an signal to the delegate so it starts working in the other thread
|
|
if (synchronous) {
|
|
emit q->startHttpRequestSynchronously(); // This one is BlockingQueuedConnection, so it will return when all work is done
|
|
|
|
if (delegate->incomingErrorCode != QNetworkReply::NoError) {
|
|
replyDownloadMetaData
|
|
(delegate->incomingHeaders,
|
|
delegate->incomingStatusCode,
|
|
delegate->incomingReasonPhrase,
|
|
delegate->isPipeliningUsed,
|
|
QSharedPointer<char>(),
|
|
delegate->incomingContentLength);
|
|
httpError(delegate->incomingErrorCode, delegate->incomingErrorDetail);
|
|
} else {
|
|
replyDownloadMetaData
|
|
(delegate->incomingHeaders,
|
|
delegate->incomingStatusCode,
|
|
delegate->incomingReasonPhrase,
|
|
delegate->isPipeliningUsed,
|
|
QSharedPointer<char>(),
|
|
delegate->incomingContentLength);
|
|
replyDownloadData(delegate->synchronousDownloadData);
|
|
}
|
|
|
|
// End the thread. It will delete itself from the finished() signal
|
|
thread->quit();
|
|
|
|
finished();
|
|
} else {
|
|
emit q->startHttpRequest(); // Signal to the HTTP thread and go back to user.
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::invalidateCache()
|
|
{
|
|
QAbstractNetworkCache *nc = managerPrivate->networkCache;
|
|
if (nc)
|
|
nc->remove(request.url());
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::initCacheSaveDevice()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
// The disk cache does not support partial content, so don't even try to
|
|
// save any such content into the cache.
|
|
if (q->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 206) {
|
|
cacheEnabled = false;
|
|
return;
|
|
}
|
|
|
|
// save the meta data
|
|
QNetworkCacheMetaData metaData;
|
|
metaData.setUrl(url);
|
|
metaData = fetchCacheMetaData(metaData);
|
|
|
|
// save the redirect request also in the cache
|
|
QVariant redirectionTarget = q->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
|
if (redirectionTarget.isValid()) {
|
|
QNetworkCacheMetaData::AttributesMap attributes = metaData.attributes();
|
|
attributes.insert(QNetworkRequest::RedirectionTargetAttribute, redirectionTarget);
|
|
metaData.setAttributes(attributes);
|
|
}
|
|
|
|
cacheSaveDevice = managerPrivate->networkCache->prepare(metaData);
|
|
|
|
if (!cacheSaveDevice || (cacheSaveDevice && !cacheSaveDevice->isOpen())) {
|
|
if (cacheSaveDevice && !cacheSaveDevice->isOpen())
|
|
qCritical("QNetworkReplyImpl: network cache returned a device that is not open -- "
|
|
"class %s probably needs to be fixed",
|
|
managerPrivate->networkCache->metaObject()->className());
|
|
|
|
managerPrivate->networkCache->remove(url);
|
|
cacheSaveDevice = 0;
|
|
cacheEnabled = false;
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
qDebug() << "QNetworkReplyHttpImplPrivate::replyDownloadData" << d.size();
|
|
|
|
// If we're closed just ignore this data
|
|
if (!q->isOpen())
|
|
return;
|
|
|
|
int pendingSignals = (int)pendingDownloadDataEmissions->fetchAndAddAcquire(-1) - 1;
|
|
|
|
if (pendingSignals > 0) {
|
|
// Some more signal emissions to this slot are pending.
|
|
// Instead of writing the downstream data, we wait
|
|
// and do it in the next call we get
|
|
// (signal comppression)
|
|
pendingDownloadData.append(d);
|
|
return;
|
|
}
|
|
|
|
pendingDownloadData.append(d);
|
|
d.clear();
|
|
// We need to usa a copy for calling writeDownstreamData as we could
|
|
// possibly recurse into this this function when we call
|
|
// appendDownstreamDataSignalEmissions because the user might call
|
|
// processEvents() or spin an event loop when this occur.
|
|
QByteDataBuffer pendingDownloadDataCopy = pendingDownloadData;
|
|
pendingDownloadData.clear();
|
|
|
|
// FIXME
|
|
//writeDownstreamData(pendingDownloadDataCopy);
|
|
// instead we do:
|
|
|
|
if (cacheEnabled && !cacheSaveDevice) {
|
|
initCacheSaveDevice();
|
|
}
|
|
|
|
qint64 bytesWritten = 0;
|
|
for (int i = 0; i < pendingDownloadDataCopy.bufferCount(); i++) {
|
|
QByteArray const &item = pendingDownloadDataCopy[i];
|
|
|
|
if (cacheSaveDevice)
|
|
cacheSaveDevice->write(item.constData(), item.size());
|
|
downloadMultiBuffer.append(item);
|
|
|
|
bytesWritten += item.size();
|
|
}
|
|
pendingDownloadDataCopy.clear();
|
|
|
|
bytesDownloaded += bytesWritten;
|
|
lastBytesDownloaded = bytesDownloaded;
|
|
|
|
//appendDownstreamDataSignalEmissions();
|
|
// instead:
|
|
QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader);
|
|
if (preMigrationDownloaded != Q_INT64_C(-1))
|
|
totalSize = totalSize.toLongLong() + preMigrationDownloaded;
|
|
// pauseNotificationHandling();
|
|
// important: At the point of this readyRead(), the data parameter list must be empty,
|
|
// else implicit sharing will trigger memcpy when the user is reading data!
|
|
emit q->readyRead();
|
|
// emit readyRead before downloadProgress incase this will cause events to be
|
|
// processed and we get into a recursive call (as in QProgressDialog).
|
|
emit q->downloadProgress(bytesDownloaded,
|
|
totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong());
|
|
|
|
// resumeNotificationHandling();
|
|
|
|
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::replyFinished()
|
|
{
|
|
// We are already loading from cache, we still however
|
|
// got this signal because it was posted already
|
|
if (loadingFromCache)
|
|
return;
|
|
|
|
finished();
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
switch (statusCode) {
|
|
case 301: // Moved Permanently
|
|
case 302: // Found
|
|
case 303: // See Other
|
|
case 307: // Temporary Redirect
|
|
// What do we do about the caching of the HTML note?
|
|
// The response to a 303 MUST NOT be cached, while the response to
|
|
// all of the others is cacheable if the headers indicate it to be
|
|
QByteArray header = q->rawHeader("location");
|
|
QUrl url = QUrl::fromEncoded(header);
|
|
if (!url.isValid())
|
|
url = QUrl(QLatin1String(header));
|
|
//redirectionRequested(url);
|
|
q->setAttribute(QNetworkRequest::RedirectionTargetAttribute, url);
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::replyDownloadMetaData
|
|
(QList<QPair<QByteArray,QByteArray> > hm,
|
|
int sc,QString rp,bool pu,
|
|
QSharedPointer<char> db,
|
|
qint64 contentLength)
|
|
{
|
|
qDebug() << "QNetworkReplyHttpImplPrivate::replyDownloadMetaData" << contentLength << sc;
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
statusCode = sc;
|
|
reasonPhrase = rp;
|
|
|
|
// Download buffer
|
|
if (!db.isNull()) {
|
|
//setDownloadBuffer(db, contentLength);
|
|
downloadBufferPointer = db;
|
|
downloadZerocopyBuffer = downloadBufferPointer.data();
|
|
downloadBufferCurrentSize = 0;
|
|
downloadBufferMaximumSize = contentLength;
|
|
q->setAttribute(QNetworkRequest::DownloadBufferAttribute, qVariantFromValue<QSharedPointer<char> > (downloadBufferPointer));
|
|
}
|
|
|
|
q->setAttribute(QNetworkRequest::HttpPipeliningWasUsedAttribute, pu);
|
|
|
|
// reconstruct the HTTP header
|
|
QList<QPair<QByteArray, QByteArray> > headerMap = hm;
|
|
QList<QPair<QByteArray, QByteArray> >::ConstIterator it = headerMap.constBegin(),
|
|
end = headerMap.constEnd();
|
|
QByteArray header;
|
|
|
|
for (; it != end; ++it) {
|
|
QByteArray value = q->rawHeader(it->first);
|
|
if (!value.isEmpty()) {
|
|
if (qstricmp(it->first.constData(), "set-cookie") == 0)
|
|
value += '\n';
|
|
else
|
|
value += ", ";
|
|
}
|
|
value += it->second;
|
|
q->setRawHeader(it->first, value);
|
|
}
|
|
|
|
q->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCode);
|
|
q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reasonPhrase);
|
|
|
|
// is it a redirection?
|
|
checkForRedirect(statusCode);
|
|
|
|
if (statusCode >= 500 && statusCode < 600) {
|
|
QAbstractNetworkCache *nc = managerPrivate->networkCache;
|
|
if (nc) {
|
|
QNetworkCacheMetaData metaData = nc->metaData(request.url());
|
|
QNetworkHeadersPrivate cacheHeaders;
|
|
cacheHeaders.setAllRawHeaders(metaData.rawHeaders());
|
|
QNetworkHeadersPrivate::RawHeadersList::ConstIterator it;
|
|
it = cacheHeaders.findRawHeader("Cache-Control");
|
|
bool mustReValidate = false;
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(it->second);
|
|
if (cacheControl.contains("must-revalidate"))
|
|
mustReValidate = true;
|
|
}
|
|
if (!mustReValidate && sendCacheContents(metaData))
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (statusCode == 304) {
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
qDebug() << "Received a 304 from" << url();
|
|
#endif
|
|
QAbstractNetworkCache *nc = managerPrivate->networkCache;
|
|
if (nc) {
|
|
QNetworkCacheMetaData oldMetaData = nc->metaData(request.url());
|
|
QNetworkCacheMetaData metaData = fetchCacheMetaData(oldMetaData);
|
|
if (oldMetaData != metaData)
|
|
nc->updateMetaData(metaData);
|
|
if (sendCacheContents(metaData))
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
if (statusCode != 304 && statusCode != 303) {
|
|
if (!isCachingEnabled())
|
|
setCachingEnabled(true);
|
|
}
|
|
|
|
metaDataChanged();
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::replyDownloadProgressSlot(qint64 bytesReceived, qint64 bytesTotal)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
// If we're closed just ignore this data
|
|
if (!q->isOpen())
|
|
return;
|
|
|
|
// we can be sure here that there is a download buffer
|
|
|
|
int pendingSignals = (int)pendingDownloadProgressEmissions->fetchAndAddAcquire(-1) - 1;
|
|
if (pendingSignals > 0) {
|
|
// Let's ignore this signal and look at the next one coming in
|
|
// (signal comppression)
|
|
return;
|
|
}
|
|
|
|
if (!q->isOpen())
|
|
return;
|
|
|
|
if (cacheEnabled && bytesReceived == bytesTotal) {
|
|
// Write everything in one go if we use a download buffer. might be more performant.
|
|
initCacheSaveDevice();
|
|
// need to check again if cache enabled and device exists
|
|
if (cacheSaveDevice && cacheEnabled)
|
|
cacheSaveDevice->write(downloadZerocopyBuffer, bytesTotal);
|
|
// FIXME where is it closed?
|
|
}
|
|
|
|
bytesDownloaded = bytesReceived;
|
|
lastBytesDownloaded = bytesReceived;
|
|
|
|
downloadBufferCurrentSize = bytesReceived;
|
|
|
|
// Only emit readyRead when actual data is there
|
|
// emit readyRead before downloadProgress incase this will cause events to be
|
|
// processed and we get into a recursive call (as in QProgressDialog).
|
|
if (bytesDownloaded > 0)
|
|
emit q->readyRead();
|
|
emit q->downloadProgress(bytesDownloaded, bytesTotal);
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::httpAuthenticationRequired(const QHttpNetworkRequest &,
|
|
QAuthenticator *auth)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
managerPrivate->authenticationRequired(auth, q_func(), synchronous, url, &urlForLastAuthentication);
|
|
}
|
|
|
|
#ifndef QT_NO_NETWORKPROXY
|
|
void QNetworkReplyHttpImplPrivate::proxyAuthenticationRequired(const QNetworkProxy &proxy,
|
|
QAuthenticator *authenticator)
|
|
{
|
|
managerPrivate->proxyAuthenticationRequired(proxy, synchronous, authenticator, &lastProxyAuthentication);
|
|
}
|
|
#endif
|
|
|
|
void QNetworkReplyHttpImplPrivate::httpError(QNetworkReply::NetworkError errorCode,
|
|
const QString &errorString)
|
|
{
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
qDebug() << "http error!" << errorCode << errorString;
|
|
#endif
|
|
|
|
// FIXME?
|
|
error(errorCode, errorString);
|
|
}
|
|
|
|
#ifndef QT_NO_OPENSSL
|
|
void QNetworkReplyHttpImplPrivate::replySslErrors(
|
|
const QList<QSslError> &list, bool *ignoreAll, QList<QSslError> *toBeIgnored)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
emit q->sslErrors(list);
|
|
// Check if the callback set any ignore and return this here to http thread
|
|
if (pendingIgnoreAllSslErrors)
|
|
*ignoreAll = true;
|
|
if (!pendingIgnoreSslErrorsList.isEmpty())
|
|
*toBeIgnored = pendingIgnoreSslErrorsList;
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::replySslConfigurationChanged(const QSslConfiguration &sslConfiguration)
|
|
{
|
|
// Receiving the used SSL configuration from the HTTP thread
|
|
this->sslConfiguration = sslConfiguration;
|
|
}
|
|
#endif
|
|
|
|
// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
|
|
void QNetworkReplyHttpImplPrivate::resetUploadDataSlot(bool *r)
|
|
{
|
|
*r = uploadByteDevice->reset();
|
|
}
|
|
|
|
// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
|
|
void QNetworkReplyHttpImplPrivate::sentUploadDataSlot(qint64 amount)
|
|
{
|
|
uploadByteDevice->advanceReadPointer(amount);
|
|
}
|
|
|
|
// Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread
|
|
void QNetworkReplyHttpImplPrivate::wantUploadDataSlot(qint64 maxSize)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
// call readPointer
|
|
qint64 currentUploadDataLength = 0;
|
|
char *data = const_cast<char*>(uploadByteDevice->readPointer(maxSize, currentUploadDataLength));
|
|
// Let's make a copy of this data
|
|
QByteArray dataArray(data, currentUploadDataLength);
|
|
|
|
// Communicate back to HTTP thread
|
|
emit q->haveUploadData(dataArray, uploadByteDevice->atEnd(), uploadByteDevice->size());
|
|
}
|
|
|
|
/*
|
|
A simple web page that can be used to test us: http://www.procata.com/cachetest/
|
|
*/
|
|
bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData &metaData)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
setCachingEnabled(false);
|
|
if (!metaData.isValid())
|
|
return false;
|
|
|
|
QAbstractNetworkCache *nc = managerPrivate->networkCache;
|
|
Q_ASSERT(nc);
|
|
QIODevice *contents = nc->data(url);
|
|
if (!contents) {
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
qDebug() << "Can not send cache, the contents are 0" << url;
|
|
#endif
|
|
return false;
|
|
}
|
|
contents->setParent(q);
|
|
|
|
QNetworkCacheMetaData::AttributesMap attributes = metaData.attributes();
|
|
int status = attributes.value(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
if (status < 100)
|
|
status = 200; // fake it
|
|
|
|
q->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, status);
|
|
q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, attributes.value(QNetworkRequest::HttpReasonPhraseAttribute));
|
|
q->setAttribute(QNetworkRequest::SourceIsFromCacheAttribute, true);
|
|
|
|
QNetworkCacheMetaData::RawHeaderList rawHeaders = metaData.rawHeaders();
|
|
QNetworkCacheMetaData::RawHeaderList::ConstIterator it = rawHeaders.constBegin(),
|
|
end = rawHeaders.constEnd();
|
|
for ( ; it != end; ++it)
|
|
setRawHeader(it->first, it->second);
|
|
|
|
checkForRedirect(status);
|
|
|
|
cacheLoadDevice = contents;
|
|
q->connect(cacheLoadDevice, SIGNAL(readyRead()), SLOT(_q_cacheLoadReadyRead()));
|
|
q->connect(cacheLoadDevice, SIGNAL(readChannelFinished()), SLOT(_q_cacheLoadReadyRead()));
|
|
|
|
// This needs to be emitted in the event loop because it can be reached at
|
|
// the direct code path of qnam.get(...) before the user has a chance
|
|
// to connect any signals.
|
|
QMetaObject::invokeMethod(q, "metaDataChanged", Qt::QueuedConnection);
|
|
QMetaObject::invokeMethod(q, "_q_cacheLoadReadyRead", Qt::QueuedConnection);
|
|
|
|
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
qDebug() << "Successfully sent cache:" << url << contents->size() << "bytes";
|
|
#endif
|
|
|
|
// Set the following flag so we can ignore some signals from HTTP thread
|
|
// that would still come
|
|
loadingFromCache = true;
|
|
return true;
|
|
}
|
|
|
|
QNetworkCacheMetaData QNetworkReplyHttpImplPrivate::fetchCacheMetaData(const QNetworkCacheMetaData &oldMetaData) const
|
|
{
|
|
Q_Q(const QNetworkReplyHttpImpl);
|
|
|
|
QNetworkCacheMetaData metaData = oldMetaData;
|
|
|
|
QNetworkHeadersPrivate cacheHeaders;
|
|
cacheHeaders.setAllRawHeaders(metaData.rawHeaders());
|
|
QNetworkHeadersPrivate::RawHeadersList::ConstIterator it;
|
|
|
|
QList<QByteArray> newHeaders = q->rawHeaderList();
|
|
foreach (QByteArray header, newHeaders) {
|
|
QByteArray originalHeader = header;
|
|
header = header.toLower();
|
|
bool hop_by_hop =
|
|
(header == "connection"
|
|
|| header == "keep-alive"
|
|
|| header == "proxy-authenticate"
|
|
|| header == "proxy-authorization"
|
|
|| header == "te"
|
|
|| header == "trailers"
|
|
|| header == "transfer-encoding"
|
|
|| header == "upgrade");
|
|
if (hop_by_hop)
|
|
continue;
|
|
|
|
// for 4.6.0, we were planning to not store the date header in the
|
|
// cached resource; through that we planned to reduce the number
|
|
// of writes to disk when using a QNetworkDiskCache (i.e. don't
|
|
// write to disk when only the date changes).
|
|
// However, without the date we cannot calculate the age of the page
|
|
// anymore.
|
|
//if (header == "date")
|
|
//continue;
|
|
|
|
// Don't store Warning 1xx headers
|
|
if (header == "warning") {
|
|
QByteArray v = q->rawHeader(header);
|
|
if (v.length() == 3
|
|
&& v[0] == '1'
|
|
&& v[1] >= '0' && v[1] <= '9'
|
|
&& v[2] >= '0' && v[2] <= '9')
|
|
continue;
|
|
}
|
|
|
|
it = cacheHeaders.findRawHeader(header);
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
// Match the behavior of Firefox and assume Cache-Control: "no-transform"
|
|
if (header == "content-encoding"
|
|
|| header == "content-range"
|
|
|| header == "content-type")
|
|
continue;
|
|
|
|
// For MS servers that send "Content-Length: 0" on 304 responses
|
|
// ignore this too
|
|
if (header == "content-length")
|
|
continue;
|
|
}
|
|
|
|
#if defined(QNETWORKACCESSHTTPBACKEND_DEBUG)
|
|
QByteArray n = rawHeader(header);
|
|
QByteArray o;
|
|
if (it != cacheHeaders.rawHeaders.constEnd())
|
|
o = (*it).second;
|
|
if (n != o && header != "date") {
|
|
qDebug() << "replacing" << header;
|
|
qDebug() << "new" << n;
|
|
qDebug() << "old" << o;
|
|
}
|
|
#endif
|
|
cacheHeaders.setRawHeader(originalHeader, q->rawHeader(header));
|
|
}
|
|
metaData.setRawHeaders(cacheHeaders.rawHeaders);
|
|
|
|
bool checkExpired = true;
|
|
|
|
QHash<QByteArray, QByteArray> cacheControl;
|
|
it = cacheHeaders.findRawHeader("Cache-Control");
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
cacheControl = parseHttpOptionHeader(it->second);
|
|
QByteArray maxAge = cacheControl.value("max-age");
|
|
if (!maxAge.isEmpty()) {
|
|
checkExpired = false;
|
|
QDateTime dt = QDateTime::currentDateTime();
|
|
dt = dt.addSecs(maxAge.toInt());
|
|
metaData.setExpirationDate(dt);
|
|
}
|
|
}
|
|
if (checkExpired) {
|
|
it = cacheHeaders.findRawHeader("expires");
|
|
if (it != cacheHeaders.rawHeaders.constEnd()) {
|
|
QDateTime expiredDateTime = QNetworkHeadersPrivate::fromHttpDate(it->second);
|
|
metaData.setExpirationDate(expiredDateTime);
|
|
}
|
|
}
|
|
|
|
it = cacheHeaders.findRawHeader("last-modified");
|
|
if (it != cacheHeaders.rawHeaders.constEnd())
|
|
metaData.setLastModified(QNetworkHeadersPrivate::fromHttpDate(it->second));
|
|
|
|
bool canDiskCache;
|
|
// only cache GET replies by default, all other replies (POST, PUT, DELETE)
|
|
// are not cacheable by default (according to RFC 2616 section 9)
|
|
if (httpRequest.operation() == QHttpNetworkRequest::Get) {
|
|
|
|
canDiskCache = true;
|
|
// 14.32
|
|
// HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the client
|
|
// had sent "Cache-Control: no-cache".
|
|
it = cacheHeaders.findRawHeader("pragma");
|
|
if (it != cacheHeaders.rawHeaders.constEnd()
|
|
&& it->second == "no-cache")
|
|
canDiskCache = false;
|
|
|
|
// HTTP/1.1. Check the Cache-Control header
|
|
if (cacheControl.contains("no-cache"))
|
|
canDiskCache = false;
|
|
else if (cacheControl.contains("no-store"))
|
|
canDiskCache = false;
|
|
|
|
// responses to POST might be cacheable
|
|
} else if (httpRequest.operation() == QHttpNetworkRequest::Post) {
|
|
|
|
canDiskCache = false;
|
|
// some pages contain "expires:" and "cache-control: no-cache" field,
|
|
// so we only might cache POST requests if we get "cache-control: max-age ..."
|
|
if (cacheControl.contains("max-age"))
|
|
canDiskCache = true;
|
|
|
|
// responses to PUT and DELETE are not cacheable
|
|
} else {
|
|
canDiskCache = false;
|
|
}
|
|
|
|
metaData.setSaveToDisk(canDiskCache);
|
|
QNetworkCacheMetaData::AttributesMap attributes;
|
|
if (statusCode != 304) {
|
|
// update the status code
|
|
attributes.insert(QNetworkRequest::HttpStatusCodeAttribute, statusCode);
|
|
attributes.insert(QNetworkRequest::HttpReasonPhraseAttribute, reasonPhrase);
|
|
} else {
|
|
// this is a redirection, keep the attributes intact
|
|
attributes = oldMetaData.attributes();
|
|
}
|
|
metaData.setAttributes(attributes);
|
|
return metaData;
|
|
}
|
|
|
|
bool QNetworkReplyHttpImplPrivate::canResume() const
|
|
{
|
|
Q_Q(const QNetworkReplyHttpImpl);
|
|
|
|
// Only GET operation supports resuming.
|
|
if (operation != QNetworkAccessManager::GetOperation)
|
|
return false;
|
|
|
|
// Can only resume if server/resource supports Range header.
|
|
QByteArray acceptRangesheaderName("Accept-Ranges");
|
|
if (!q->hasRawHeader(acceptRangesheaderName) || q->rawHeader(acceptRangesheaderName) == "none")
|
|
return false;
|
|
|
|
// We only support resuming for byte ranges.
|
|
if (request.hasRawHeader("Range")) {
|
|
QByteArray range = request.rawHeader("Range");
|
|
if (!range.startsWith("bytes="))
|
|
return false;
|
|
}
|
|
|
|
// If we're using a download buffer then we don't support resuming/migration
|
|
// right now. Too much trouble.
|
|
if (downloadZerocopyBuffer)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::setResumeOffset(quint64 offset)
|
|
{
|
|
resumeOffset = offset;
|
|
}
|
|
|
|
/*!
|
|
Starts the backend. Returns true if the backend is started. Returns false if the backend
|
|
could not be started due to an unopened or roaming session. The caller should recall this
|
|
function once the session has been opened or the roaming process has finished.
|
|
*/
|
|
bool QNetworkReplyHttpImplPrivate::start()
|
|
{
|
|
if (!managerPrivate->networkSession) {
|
|
postRequest();
|
|
return true;
|
|
}
|
|
|
|
// This is not ideal.
|
|
const QString host = url.host();
|
|
if (host == QLatin1String("localhost") ||
|
|
QHostAddress(host) == QHostAddress::LocalHost ||
|
|
QHostAddress(host) == QHostAddress::LocalHostIPv6) {
|
|
// Don't need an open session for localhost access.
|
|
postRequest();
|
|
return true;
|
|
}
|
|
|
|
if (managerPrivate->networkSession->isOpen() &&
|
|
managerPrivate->networkSession->state() == QNetworkSession::Connected) {
|
|
postRequest();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_startOperation()
|
|
{
|
|
// ensure this function is only being called once
|
|
if (state == Working) {
|
|
qDebug("QNetworkReplyImpl::_q_startOperation was called more than once");
|
|
return;
|
|
}
|
|
state = Working;
|
|
|
|
#ifndef QT_NO_BEARERMANAGEMENT
|
|
if (!start()) { // ### we should call that method even if bearer is not used
|
|
// backend failed to start because the session state is not Connected.
|
|
// QNetworkAccessManager will call reply->backend->start() again for us when the session
|
|
// state changes.
|
|
state = WaitingForSession;
|
|
|
|
QNetworkSession *session = managerPrivate->networkSession.data();
|
|
|
|
if (session) {
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
QObject::connect(session, SIGNAL(error(QNetworkSession::SessionError)),
|
|
q, SLOT(_q_networkSessionFailed()));
|
|
|
|
if (!session->isOpen())
|
|
session->open();
|
|
} else {
|
|
qWarning("Backend is waiting for QNetworkSession to connect, but there is none!");
|
|
}
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (synchronous) {
|
|
state = Finished;
|
|
q_func()->setFinished(true);
|
|
} else {
|
|
if (state != Finished) {
|
|
// if (operation == QNetworkAccessManager::GetOperation)
|
|
// pendingNotifications.append(NotifyDownstreamReadyWrite);
|
|
|
|
// handleNotifications();
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
if (state != Working)
|
|
return;
|
|
if (!cacheLoadDevice || !q->isOpen() || !cacheLoadDevice->bytesAvailable())
|
|
return;
|
|
|
|
// FIXME Optimize to use zerocopy download buffer if it is a QBuffer.
|
|
// Needs to be done where sendCacheContents() (?) of HTTP is emitting
|
|
// metaDataChanged ?
|
|
|
|
|
|
// FIXME
|
|
lastBytesDownloaded = bytesDownloaded;
|
|
QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader);
|
|
|
|
//pauseNotificationHandling();
|
|
// emit readyRead before downloadProgress incase this will cause events to be
|
|
// processed and we get into a recursive call (as in QProgressDialog).
|
|
|
|
// This readyRead() goes to the user. The user then may or may not read() anything.
|
|
emit q->readyRead();
|
|
emit q->downloadProgress(bytesDownloaded,
|
|
totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong());
|
|
|
|
// If there are still bytes available in the cacheLoadDevice then the user did not read
|
|
// in response to the readyRead() signal. This means we have to load from the cacheLoadDevice
|
|
// and buffer that stuff. This is needed to be able to properly emit finished() later.
|
|
while (cacheLoadDevice->bytesAvailable()) {
|
|
downloadMultiBuffer.append(cacheLoadDevice->readAll());
|
|
}
|
|
|
|
if (cacheLoadDevice->isSequential()) {
|
|
// check if end and we can read the EOF -1
|
|
char c;
|
|
qint64 actualCount = cacheLoadDevice->read(&c, 1);
|
|
if (actualCount < 0) {
|
|
cacheLoadDevice->deleteLater();
|
|
cacheLoadDevice = 0;
|
|
QMetaObject::invokeMethod(q, "_q_finished", Qt::QueuedConnection);
|
|
} else if (actualCount == 1) {
|
|
// This is most probably not happening since most QIODevice returned something proper for bytesAvailable()
|
|
// and had already been "emptied".
|
|
cacheLoadDevice->ungetChar(c);
|
|
}
|
|
} else if ((!cacheLoadDevice->isSequential() && cacheLoadDevice->atEnd())) {
|
|
// This codepath is in case the cache device is a QBuffer, e.g. from QNetworkDiskCache.
|
|
cacheLoadDevice->deleteLater();
|
|
cacheLoadDevice = 0;
|
|
QMetaObject::invokeMethod(q, "_q_finished", Qt::QueuedConnection);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingDataFinished()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
// make sure this is only called once, ever.
|
|
//_q_bufferOutgoingData may call it or the readChannelFinished emission
|
|
if (state != Buffering)
|
|
return;
|
|
|
|
// disconnect signals
|
|
QObject::disconnect(outgoingData, SIGNAL(readyRead()), q, SLOT(_q_bufferOutgoingData()));
|
|
QObject::disconnect(outgoingData, SIGNAL(readChannelFinished()), q, SLOT(_q_bufferOutgoingDataFinished()));
|
|
|
|
// finally, start the request
|
|
QMetaObject::invokeMethod(q, "_q_startOperation", Qt::QueuedConnection);
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingData()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
if (!outgoingDataBuffer) {
|
|
// first call, create our buffer
|
|
outgoingDataBuffer = QSharedPointer<QRingBuffer>(new QRingBuffer());
|
|
|
|
QObject::connect(outgoingData, SIGNAL(readyRead()), q, SLOT(_q_bufferOutgoingData()));
|
|
QObject::connect(outgoingData, SIGNAL(readChannelFinished()), q, SLOT(_q_bufferOutgoingDataFinished()));
|
|
}
|
|
|
|
qint64 bytesBuffered = 0;
|
|
qint64 bytesToBuffer = 0;
|
|
|
|
// read data into our buffer
|
|
forever {
|
|
bytesToBuffer = outgoingData->bytesAvailable();
|
|
// unknown? just try 2 kB, this also ensures we always try to read the EOF
|
|
if (bytesToBuffer <= 0)
|
|
bytesToBuffer = 2*1024;
|
|
|
|
char *dst = outgoingDataBuffer->reserve(bytesToBuffer);
|
|
bytesBuffered = outgoingData->read(dst, bytesToBuffer);
|
|
|
|
if (bytesBuffered == -1) {
|
|
// EOF has been reached.
|
|
outgoingDataBuffer->chop(bytesToBuffer);
|
|
|
|
_q_bufferOutgoingDataFinished();
|
|
break;
|
|
} else if (bytesBuffered == 0) {
|
|
// nothing read right now, just wait until we get called again
|
|
outgoingDataBuffer->chop(bytesToBuffer);
|
|
|
|
break;
|
|
} else {
|
|
// don't break, try to read() again
|
|
outgoingDataBuffer->chop(bytesToBuffer - bytesBuffered);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef QT_NO_BEARERMANAGEMENT
|
|
void QNetworkReplyHttpImplPrivate::_q_networkSessionConnected()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
if (!manager)
|
|
return;
|
|
|
|
QNetworkSession *session = managerPrivate->networkSession.data();
|
|
if (!session)
|
|
return;
|
|
|
|
if (session->state() != QNetworkSession::Connected)
|
|
return;
|
|
|
|
switch (state) {
|
|
case QNetworkReplyImplPrivate::Buffering:
|
|
case QNetworkReplyImplPrivate::Working:
|
|
case QNetworkReplyImplPrivate::Reconnecting:
|
|
// Migrate existing downloads to new network connection.
|
|
migrateBackend();
|
|
break;
|
|
case QNetworkReplyImplPrivate::WaitingForSession:
|
|
// Start waiting requests.
|
|
QMetaObject::invokeMethod(q, "_q_startOperation", Qt::QueuedConnection);
|
|
break;
|
|
default:
|
|
;
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_networkSessionFailed()
|
|
{
|
|
// Abort waiting and working replies.
|
|
if (state == WaitingForSession || state == Working) {
|
|
state = Working;
|
|
error(QNetworkReplyImpl::UnknownNetworkError,
|
|
QCoreApplication::translate("QNetworkReply", "Network session error."));
|
|
finished();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
// need to have this function since the reply is a private member variable
|
|
// and the special backends need to access this.
|
|
void QNetworkReplyHttpImplPrivate::emitReplyUploadProgress(qint64 bytesSent, qint64 bytesTotal)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
if (isFinished)
|
|
return;
|
|
emit q->uploadProgress(bytesSent, bytesTotal);
|
|
}
|
|
|
|
QNonContiguousByteDevice* QNetworkReplyHttpImplPrivate::createUploadByteDevice()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
if (outgoingDataBuffer)
|
|
uploadByteDevice = QSharedPointer<QNonContiguousByteDevice>(QNonContiguousByteDeviceFactory::create(outgoingDataBuffer));
|
|
else if (outgoingData) {
|
|
uploadByteDevice = QSharedPointer<QNonContiguousByteDevice>(QNonContiguousByteDeviceFactory::create(outgoingData));
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
bool bufferDisallowed =
|
|
request.attribute(QNetworkRequest::DoNotBufferUploadDataAttribute,
|
|
QVariant(false)) == QVariant(true);
|
|
if (bufferDisallowed)
|
|
uploadByteDevice->disableReset();
|
|
|
|
// We want signal emissions only for normal asynchronous uploads
|
|
if (!synchronous)
|
|
QObject::connect(uploadByteDevice.data(), SIGNAL(readProgress(qint64,qint64)),
|
|
q, SLOT(emitReplyUploadProgress(qint64,qint64)));
|
|
|
|
return uploadByteDevice.data();
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_finished()
|
|
{
|
|
// This gets called queued, just forward to real call then
|
|
finished();
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::finished()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
if (state == Finished || state == Aborted || state == WaitingForSession)
|
|
return;
|
|
|
|
//pauseNotificationHandling();
|
|
QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader);
|
|
if (preMigrationDownloaded != Q_INT64_C(-1))
|
|
totalSize = totalSize.toLongLong() + preMigrationDownloaded;
|
|
|
|
// FIXME why should it be 0
|
|
if (manager) {
|
|
#ifndef QT_NO_BEARERMANAGEMENT
|
|
QNetworkSession *session = managerPrivate->networkSession.data();
|
|
if (session && session->state() == QNetworkSession::Roaming &&
|
|
state == Working && errorCode != QNetworkReply::OperationCanceledError) {
|
|
// only content with a known size will fail with a temporary network failure error
|
|
if (!totalSize.isNull()) {
|
|
if (bytesDownloaded != totalSize) {
|
|
if (migrateBackend()) {
|
|
// either we are migrating or the request is finished/aborted
|
|
if (state == Reconnecting || state == WaitingForSession) {
|
|
//resumeNotificationHandling();
|
|
return; // exit early if we are migrating.
|
|
}
|
|
} else {
|
|
error(QNetworkReply::TemporaryNetworkFailureError,
|
|
QNetworkReply::tr("Temporary network failure."));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
//resumeNotificationHandling();
|
|
|
|
state = Finished;
|
|
q->setFinished(true);
|
|
|
|
//pendingNotifications.clear();
|
|
|
|
//pauseNotificationHandling();
|
|
if (totalSize.isNull() || totalSize == -1) {
|
|
emit q->downloadProgress(bytesDownloaded, bytesDownloaded);
|
|
}
|
|
|
|
if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer))
|
|
emit q->uploadProgress(0, 0);
|
|
//resumeNotificationHandling();
|
|
|
|
// if we don't know the total size of or we received everything save the cache
|
|
if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize)
|
|
completeCacheSave();
|
|
|
|
// note: might not be a good idea, since users could decide to delete us
|
|
// which would delete the backend too...
|
|
// maybe we should protect the backend
|
|
//pauseNotificationHandling();
|
|
emit q->readChannelFinished();
|
|
emit q->finished();
|
|
//resumeNotificationHandling();
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::_q_error(QNetworkReplyImpl::NetworkError code, const QString &errorMessage)
|
|
{
|
|
this->error(code, errorMessage);
|
|
}
|
|
|
|
|
|
void QNetworkReplyHttpImplPrivate::error(QNetworkReplyImpl::NetworkError code, const QString &errorMessage)
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
// Can't set and emit multiple errors.
|
|
if (errorCode != QNetworkReply::NoError) {
|
|
qWarning() << "QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once.";
|
|
return;
|
|
}
|
|
|
|
errorCode = code;
|
|
q->setErrorString(errorMessage);
|
|
|
|
// note: might not be a good idea, since users could decide to delete us
|
|
// which would delete the backend too...
|
|
// maybe we should protect the backend
|
|
emit q->error(code);
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::metaDataChanged()
|
|
{
|
|
// FIXME merge this with replyDownloadMetaData(); ?
|
|
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
// 1. do we have cookies?
|
|
// 2. are we allowed to set them?
|
|
if (cookedHeaders.contains(QNetworkRequest::SetCookieHeader) && manager
|
|
&& (static_cast<QNetworkRequest::LoadControl>
|
|
(request.attribute(QNetworkRequest::CookieSaveControlAttribute,
|
|
QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Automatic)) {
|
|
QList<QNetworkCookie> cookies =
|
|
qvariant_cast<QList<QNetworkCookie> >(cookedHeaders.value(QNetworkRequest::SetCookieHeader));
|
|
QNetworkCookieJar *jar = manager->cookieJar();
|
|
if (jar)
|
|
jar->setCookiesFromUrl(cookies, url);
|
|
}
|
|
emit q->metaDataChanged();
|
|
}
|
|
|
|
/*
|
|
Migrates the backend of the QNetworkReply to a new network connection if required. Returns
|
|
true if the reply is migrated or it is not required; otherwise returns false.
|
|
*/
|
|
bool QNetworkReplyHttpImplPrivate::migrateBackend()
|
|
{
|
|
Q_Q(QNetworkReplyHttpImpl);
|
|
|
|
// Network reply is already finished or aborted, don't need to migrate.
|
|
if (state == Finished || state == Aborted)
|
|
return true;
|
|
|
|
// Backend does not support resuming download.
|
|
if (!canResume())
|
|
return false;
|
|
|
|
// Request has outgoing data, not migrating.
|
|
if (outgoingData)
|
|
return false;
|
|
|
|
// Request is serviced from the cache, don't need to migrate.
|
|
if (cacheLoadDevice)
|
|
return true;
|
|
|
|
state = Reconnecting;
|
|
|
|
// if (backend) {
|
|
// delete backend;
|
|
// backend = 0;
|
|
// }
|
|
|
|
cookedHeaders.clear();
|
|
rawHeaders.clear();
|
|
|
|
preMigrationDownloaded = bytesDownloaded;
|
|
|
|
// backend = manager->d_func()->findBackend(operation, request);
|
|
|
|
// if (backend) {
|
|
// backend->setParent(q);
|
|
// backend->reply = this;
|
|
// backend->setResumeOffset(bytesDownloaded);
|
|
// }
|
|
|
|
// FIXME
|
|
Q_ASSERT(0);
|
|
// What probably needs to be done is an abort and then re-send?
|
|
|
|
QMetaObject::invokeMethod(q, "_q_startOperation", Qt::QueuedConnection);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void QNetworkReplyHttpImplPrivate::createCache()
|
|
{
|
|
// check if we can save and if we're allowed to
|
|
if (!managerPrivate->networkCache
|
|
|| !request.attribute(QNetworkRequest::CacheSaveControlAttribute, true).toBool()
|
|
|| request.attribute(QNetworkRequest::CacheLoadControlAttribute,
|
|
QNetworkRequest::PreferNetwork).toInt()
|
|
== QNetworkRequest::AlwaysNetwork)
|
|
return;
|
|
cacheEnabled = true;
|
|
}
|
|
|
|
bool QNetworkReplyHttpImplPrivate::isCachingEnabled() const
|
|
{
|
|
return (cacheEnabled && managerPrivate->networkCache != 0);
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::setCachingEnabled(bool enable)
|
|
{
|
|
if (!enable && !cacheEnabled)
|
|
return; // nothing to do
|
|
if (enable && cacheEnabled)
|
|
return; // nothing to do either!
|
|
|
|
if (enable) {
|
|
if (bytesDownloaded) {
|
|
qDebug() << "x" << bytesDownloaded;
|
|
// refuse to enable in this case
|
|
qCritical("QNetworkReplyImpl: backend error: caching was enabled after some bytes had been written");
|
|
return;
|
|
}
|
|
|
|
createCache();
|
|
} else {
|
|
// someone told us to turn on, then back off?
|
|
// ok... but you should make up your mind
|
|
qDebug("QNetworkReplyImpl: setCachingEnabled(true) called after setCachingEnabled(false)");
|
|
managerPrivate->networkCache->remove(url);
|
|
cacheSaveDevice = 0;
|
|
cacheEnabled = false;
|
|
}
|
|
}
|
|
|
|
void QNetworkReplyHttpImplPrivate::completeCacheSave()
|
|
{
|
|
if (cacheEnabled && errorCode != QNetworkReplyImpl::NoError) {
|
|
managerPrivate->networkCache->remove(url);
|
|
} else if (cacheEnabled && cacheSaveDevice) {
|
|
managerPrivate->networkCache->insert(cacheSaveDevice);
|
|
}
|
|
cacheSaveDevice = 0;
|
|
cacheEnabled = false;
|
|
}
|
|
|
|
QT_END_NAMESPACE
|
|
|
|
#endif // QT_NO_HTTP
|