| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtNetwork module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $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.h" |
| #include "qnetworkcookie_p.h" |
| #include "QtCore/qdatetime.h" |
| #include "QtCore/qelapsedtimer.h" |
| #include "QtNetwork/qsslconfiguration.h" |
| #include "qhttpthreaddelegate_p.h" |
| #include "qhsts_p.h" |
| #include "qthread.h" |
| #include "QtCore/qcoreapplication.h" |
| |
| #include <QtCore/private/qthread_p.h> |
| |
| #include "qnetworkcookiejar.h" |
| #include "qnetconmonitor_p.h" |
| |
| #include <string.h> // for strchr |
| |
| QT_BEGIN_NAMESPACE |
| |
| class QNetworkProxy; |
| |
| static inline bool isSeparator(char c) |
| { |
| static const char separators[] = "()<>@,;:\\\"/[]?={}"; |
| return isLWS(c) || strchr(separators, c) != nullptr; |
| } |
| |
| // ### 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()) { |
| 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()) { |
| 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()); |
| } |
| } |
| } |
| |
| #if QT_CONFIG(bearermanagement) |
| static bool isSessionNeeded(const QUrl &url) |
| { |
| if (QNetworkStatusMonitor::isEnabled()) { |
| // In case QNetworkStatus/QNetConManager are in business, |
| // no session, no bearer manager are involved. |
| return false; |
| } |
| // Connections to the local machine does not require a session |
| QString host = url.host().toLower(); |
| return !QHostAddress(host).isLoopback() && host != QLatin1String("localhost") |
| && host != QSysInfo::machineHostName().toLower(); |
| } |
| #endif // bearer management |
| |
| QNetworkReplyHttpImpl::QNetworkReplyHttpImpl(QNetworkAccessManager* const manager, |
| const QNetworkRequest& request, |
| QNetworkAccessManager::Operation& operation, |
| QIODevice* outgoingData) |
| : QNetworkReply(*new QNetworkReplyHttpImplPrivate, manager) |
| { |
| Q_D(QNetworkReplyHttpImpl); |
| Q_ASSERT(manager); |
| d->manager = manager; |
| d->managerPrivate = manager->d_func(); |
| d->request = request; |
| d->originalRequest = request; |
| d->operation = operation; |
| d->outgoingData = outgoingData; |
| d->url = request.url(); |
| #ifndef QT_NO_SSL |
| if (request.url().scheme() == QLatin1String("https")) |
| d->sslConfiguration.reset(new QSslConfiguration(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 Qt WebKit. |
| 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>::create(); |
| 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() |
| { |
| // This will do nothing if the request was already finished or aborted |
| emit abortHttpRequest(); |
| } |
| |
| void QNetworkReplyHttpImpl::close() |
| { |
| Q_D(QNetworkReplyHttpImpl); |
| |
| if (d->state == QNetworkReplyPrivate::Aborted || |
| d->state == QNetworkReplyPrivate::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 == QNetworkReplyPrivate::Finished || d->state == QNetworkReplyPrivate::Aborted) |
| return; |
| |
| QNetworkReply::close(); |
| |
| if (d->state != QNetworkReplyPrivate::Finished) { |
| // call finished which will emit signals |
| // FIXME shouldn't this be emitted Queued? |
| d->error(OperationCanceledError, tr("Operation canceled")); |
| |
| // If state is WaitingForSession, calling finished has no effect |
| if (d->state == QNetworkReplyPrivate::WaitingForSession) |
| d->state = QNetworkReplyPrivate::Working; |
| d->finished(); |
| } |
| |
| d->state = QNetworkReplyPrivate::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(); |
| } |
| |
| // zerocopy buffer |
| if (d->downloadZerocopyBuffer) { |
| return QNetworkReply::bytesAvailable() + d->downloadBufferCurrentSize - d->downloadBufferReadPosition; |
| } |
| |
| // normal buffer |
| return QNetworkReply::bytesAvailable(); |
| } |
| |
| 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); |
| |
| // cacheload device |
| if (d->cacheLoadDevice) { |
| // FIXME bytesdownloaded, position etc? |
| |
| qint64 ret = d->cacheLoadDevice->read(data, maxlen); |
| return ret; |
| } |
| |
| // zerocopy buffer |
| if (d->downloadZerocopyBuffer) { |
| // FIXME bytesdownloaded, position etc? |
| |
| qint64 howMuch = qMin(maxlen, (d->downloadBufferCurrentSize - d->downloadBufferReadPosition)); |
| memcpy(data, d->downloadZerocopyBuffer + d->downloadBufferReadPosition, howMuch); |
| d->downloadBufferReadPosition += howMuch; |
| return howMuch; |
| |
| } |
| |
| // normal buffer |
| if (d->state == d->Finished || d->state == d->Aborted) |
| return -1; |
| |
| qint64 wasBuffered = d->bytesBuffered; |
| d->bytesBuffered = 0; |
| if (readBufferSize()) |
| emit readBufferFreed(wasBuffered); |
| return 0; |
| } |
| |
| void QNetworkReplyHttpImpl::setReadBufferSize(qint64 size) |
| { |
| QNetworkReply::setReadBufferSize(size); |
| emit readBufferSizeChanged(size); |
| return; |
| } |
| |
| bool QNetworkReplyHttpImpl::canReadLine () const |
| { |
| Q_D(const QNetworkReplyHttpImpl); |
| |
| if (QNetworkReply::canReadLine()) |
| return true; |
| |
| if (d->cacheLoadDevice) |
| return d->cacheLoadDevice->canReadLine(); |
| |
| if (d->downloadZerocopyBuffer) |
| return memchr(d->downloadZerocopyBuffer + d->downloadBufferReadPosition, '\n', d->downloadBufferCurrentSize - d->downloadBufferReadPosition); |
| |
| return false; |
| } |
| |
| #ifndef QT_NO_SSL |
| void QNetworkReplyHttpImpl::ignoreSslErrors() |
| { |
| Q_D(QNetworkReplyHttpImpl); |
| Q_ASSERT(d->managerPrivate); |
| |
| if (d->managerPrivate->stsEnabled && d->managerPrivate->stsCache.isKnownHost(url())) { |
| // We cannot ignore any Security Transport-related errors for this host. |
| return; |
| } |
| |
| d->pendingIgnoreAllSslErrors = true; |
| } |
| |
| void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError> &errors) |
| { |
| Q_D(QNetworkReplyHttpImpl); |
| Q_ASSERT(d->managerPrivate); |
| |
| if (d->managerPrivate->stsEnabled && d->managerPrivate->stsCache.isKnownHost(url())) { |
| // We cannot ignore any Security Transport-related errors for this host. |
| return; |
| } |
| |
| // 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); |
| } |
| |
| void QNetworkReplyHttpImpl::sslConfigurationImplementation(QSslConfiguration &configuration) const |
| { |
| Q_D(const QNetworkReplyHttpImpl); |
| if (d->sslConfiguration.data()) |
| configuration = *d->sslConfiguration; |
| else |
| configuration = request().sslConfiguration(); |
| } |
| #endif |
| |
| QNetworkReplyHttpImplPrivate::QNetworkReplyHttpImplPrivate() |
| : QNetworkReplyPrivate() |
| , manager(nullptr) |
| , managerPrivate(nullptr) |
| , synchronous(false) |
| , state(Idle) |
| , statusCode(0) |
| , uploadByteDevicePosition(false) |
| , uploadDeviceChoking(false) |
| , outgoingData(nullptr) |
| , bytesUploaded(-1) |
| , cacheLoadDevice(nullptr) |
| , loadingFromCache(false) |
| , cacheSaveDevice(nullptr) |
| , cacheEnabled(false) |
| , resumeOffset(0) |
| , preMigrationDownloaded(-1) |
| , bytesDownloaded(0) |
| , bytesBuffered(0) |
| , downloadBufferReadPosition(0) |
| , downloadBufferCurrentSize(0) |
| , downloadZerocopyBuffer(nullptr) |
| , pendingDownloadDataEmissions(QSharedPointer<QAtomicInt>::create()) |
| , pendingDownloadProgressEmissions(QSharedPointer<QAtomicInt>::create()) |
| #ifndef QT_NO_SSL |
| , pendingIgnoreAllSslErrors(false) |
| #endif |
| |
| { |
| } |
| |
| QNetworkReplyHttpImplPrivate::~QNetworkReplyHttpImplPrivate() |
| { |
| } |
| |
| /* |
| 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(httpRequest.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)); |
| |
| it = cacheHeaders.findRawHeader("Cache-Control"); |
| if (it != cacheHeaders.rawHeaders.constEnd()) { |
| QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(it->second); |
| if (cacheControl.contains("must-revalidate")) |
| return false; |
| if (cacheControl.contains("no-cache")) |
| return false; |
| } |
| |
| QDateTime currentDateTime = QDateTime::currentDateTimeUtc(); |
| QDateTime expirationDate = metaData.expirationDate(); |
| |
| bool response_is_fresh; |
| if (!expirationDate.isValid()) { |
| /* |
| * 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 |
| */ |
| qint64 age_value = 0; |
| it = cacheHeaders.findRawHeader("age"); |
| if (it != cacheHeaders.rawHeaders.constEnd()) |
| age_value = it->second.toLongLong(); |
| |
| QDateTime dateHeader; |
| qint64 date_value = 0; |
| it = cacheHeaders.findRawHeader("date"); |
| if (it != cacheHeaders.rawHeaders.constEnd()) { |
| dateHeader = QNetworkHeadersPrivate::fromHttpDate(it->second); |
| date_value = dateHeader.toSecsSinceEpoch(); |
| } |
| |
| qint64 now = currentDateTime.toSecsSinceEpoch(); |
| qint64 request_time = now; |
| qint64 response_time = now; |
| |
| // Algorithm from RFC 2616 section 13.2.3 |
| qint64 apparent_age = qMax<qint64>(0, response_time - date_value); |
| qint64 corrected_received_age = qMax(apparent_age, age_value); |
| qint64 response_delay = response_time - request_time; |
| qint64 corrected_initial_age = corrected_received_age + response_delay; |
| qint64 resident_time = now - response_time; |
| qint64 current_age = corrected_initial_age + resident_time; |
| |
| qint64 freshness_lifetime = 0; |
| |
| // RFC 2616 13.2.4 Expiration Calculations |
| if (lastModified.isValid() && dateHeader.isValid()) { |
| qint64 diff = lastModified.secsTo(dateHeader); |
| freshness_lifetime = diff / 10; |
| if (httpRequest.headerField("Warning").isEmpty()) { |
| QDateTime dt = currentDateTime.addSecs(current_age); |
| if (currentDateTime.daysTo(dt) > 1) |
| httpRequest.setHeaderField("Warning", "113"); |
| } |
| } |
| |
| // the cache-saving code below sets the freshness_lifetime with (dateHeader - last_modified) / 10 |
| // if "last-modified" is present, or to Expires otherwise |
| response_is_fresh = (freshness_lifetime > current_age); |
| } else { |
| // expiration date was calculated earlier (e.g. when storing object to the cache) |
| response_is_fresh = currentDateTime.secsTo(expirationDate) >= 0; |
| } |
| |
| 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(const QNetworkRequest &newHttpRequest) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| |
| QThread *thread = nullptr; |
| if (synchronous) { |
| // A synchronous HTTP request uses its own thread |
| thread = new QThread(); |
| thread->setObjectName(QStringLiteral("Qt HTTP synchronous thread")); |
| QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); |
| thread->start(); |
| } else { |
| // We use the manager-global thread. |
| // At some point we could switch to having multiple threads if it makes sense. |
| thread = managerPrivate->createThread(); |
| } |
| |
| QUrl url = newHttpRequest.url(); |
| httpRequest.setUrl(url); |
| httpRequest.setRedirectCount(newHttpRequest.maximumRedirectsAllowed()); |
| |
| QString scheme = url.scheme(); |
| bool ssl = (scheme == QLatin1String("https") |
| || scheme == QLatin1String("preconnect-https")); |
| q->setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, ssl); |
| httpRequest.setSsl(ssl); |
| |
| bool preConnect = (scheme == QLatin1String("preconnect-http") |
| || scheme == QLatin1String("preconnect-https")); |
| httpRequest.setPreConnect(preConnect); |
| |
| #ifndef QT_NO_NETWORKPROXY |
| QNetworkProxy transparentProxy, cacheProxy; |
| |
| // FIXME the proxy stuff should be done in the HTTP thread |
| const auto proxies = managerPrivate->queryProxy(QNetworkProxyQuery(newHttpRequest.url())); |
| for (const QNetworkProxy &p : proxies) { |
| // 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, QNetworkReplyHttpImpl::tr("No suitable proxy found"))); |
| QMetaObject::invokeMethod(q, "_q_finished", synchronous ? Qt::DirectConnection : Qt::QueuedConnection); |
| return; |
| } |
| #endif |
| |
| auto redirectPolicy = QNetworkRequest::ManualRedirectPolicy; |
| const QVariant value = newHttpRequest.attribute(QNetworkRequest::RedirectPolicyAttribute); |
| if (value.isValid()) |
| redirectPolicy = value.value<QNetworkRequest::RedirectPolicy>(); |
| else if (newHttpRequest.attribute(QNetworkRequest::FollowRedirectsAttribute).toBool()) |
| redirectPolicy = QNetworkRequest::NoLessSafeRedirectPolicy; |
| |
| httpRequest.setRedirectPolicy(redirectPolicy); |
| |
| httpRequest.setPriority(convert(newHttpRequest.priority())); |
| |
| switch (operation) { |
| case QNetworkAccessManager::GetOperation: |
| httpRequest.setOperation(QHttpNetworkRequest::Get); |
| if (loadFromCacheIfAllowed(httpRequest)) |
| return; // no need to send the request! :) |
| break; |
| |
| case QNetworkAccessManager::HeadOperation: |
| httpRequest.setOperation(QHttpNetworkRequest::Head); |
| if (loadFromCacheIfAllowed(httpRequest)) |
| return; // no need to send the request! :) |
| 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(newHttpRequest.attribute( |
| QNetworkRequest::CustomVerbAttribute).toByteArray()); |
| break; |
| |
| default: |
| break; // can't happen |
| } |
| |
| QList<QByteArray> headers = newHttpRequest.rawHeaderList(); |
| if (resumeOffset != 0) { |
| const int rangeIndex = headers.indexOf("Range"); |
| if (rangeIndex != -1) { |
| // Need to adjust resume offset for user specified range |
| |
| headers.removeAt(rangeIndex); |
| |
| // We've already verified that requestRange starts with "bytes=", see canResume. |
| QByteArray requestRange = newHttpRequest.rawHeader("Range").mid(6); |
| |
| int index = requestRange.indexOf('-'); |
| |
| quint64 requestStartOffset = requestRange.left(index).toULongLong(); |
| quint64 requestEndOffset = requestRange.mid(index + 1).toULongLong(); |
| |
| // In case an end offset is not given it is skipped from the request range |
| requestRange = "bytes=" + QByteArray::number(resumeOffset + requestStartOffset) + |
| '-' + (requestEndOffset ? QByteArray::number(requestEndOffset) : QByteArray()); |
| |
| httpRequest.setHeaderField("Range", requestRange); |
| } else { |
| httpRequest.setHeaderField("Range", "bytes=" + QByteArray::number(resumeOffset) + '-'); |
| } |
| } |
| |
| for (const QByteArray &header : qAsConst(headers)) |
| httpRequest.setHeaderField(header, newHttpRequest.rawHeader(header)); |
| |
| if (newHttpRequest.attribute(QNetworkRequest::HttpPipeliningAllowedAttribute).toBool()) |
| httpRequest.setPipeliningAllowed(true); |
| |
| if (request.attribute(QNetworkRequest::SpdyAllowedAttribute).toBool()) |
| httpRequest.setSPDYAllowed(true); |
| |
| if (request.attribute(QNetworkRequest::HTTP2AllowedAttribute).toBool()) |
| httpRequest.setHTTP2Allowed(true); |
| |
| if (request.attribute(QNetworkRequest::Http2DirectAttribute).toBool()) { |
| // Intentionally mutually exclusive - cannot be both direct and 'allowed' |
| httpRequest.setHTTP2Direct(true); |
| httpRequest.setHTTP2Allowed(false); |
| } |
| |
| if (static_cast<QNetworkRequest::LoadControl> |
| (newHttpRequest.attribute(QNetworkRequest::AuthenticationReuseAttribute, |
| QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual) |
| httpRequest.setWithCredentials(false); |
| |
| if (request.attribute(QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool()) |
| emitAllUploadProgressSignals = true; |
| |
| httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName()); |
| |
| // Create the HTTP thread delegate |
| QHttpThreadDelegate *delegate = new QHttpThreadDelegate; |
| // Propagate Http/2 settings: |
| delegate->http2Parameters = request.http2Configuration(); |
| #ifndef QT_NO_BEARERMANAGEMENT |
| if (!QNetworkStatusMonitor::isEnabled()) |
| delegate->networkSession = managerPrivate->getNetworkSession(); |
| #endif |
| |
| // 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_SSL |
| if (ssl) |
| delegate->incomingSslConfiguration.reset(new QSslConfiguration(newHttpRequest.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 |
| QVariant downloadBufferMaximumSizeAttribute = newHttpRequest.attribute(QNetworkRequest::MaximumDownloadBufferSizeAttribute); |
| if (downloadBufferMaximumSizeAttribute.isValid()) { |
| delegate->downloadBufferMaximumSize = downloadBufferMaximumSizeAttribute.toLongLong(); |
| } else { |
| // If there is no MaximumDownloadBufferSizeAttribute set (which is for the majority |
| // of QNetworkRequest) then we can assume we'll do it anyway for small HTTP replies. |
| // This helps with performance and memory fragmentation. |
| delegate->downloadBufferMaximumSize = 128*1024; |
| } |
| |
| |
| // 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, qint64, |
| bool)), |
| q, SLOT(replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >, |
| int, QString, bool, |
| QSharedPointer<char>, qint64, qint64, bool)), |
| 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,QString)), |
| Qt::QueuedConnection); |
| QObject::connect(delegate, SIGNAL(redirected(QUrl,int,int)), |
| q, SLOT(onRedirected(QUrl,int,int)), |
| Qt::QueuedConnection); |
| |
| QObject::connect(q, SIGNAL(redirectAllowed()), q, SLOT(followRedirect()), |
| Qt::QueuedConnection); |
| |
| #ifndef QT_NO_SSL |
| QObject::connect(delegate, SIGNAL(sslConfigurationChanged(QSslConfiguration)), |
| q, SLOT(replySslConfigurationChanged(QSslConfiguration)), |
| Qt::QueuedConnection); |
| #endif |
| // Those need to report back, therefore 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_SSL |
| QObject::connect(delegate, SIGNAL(encrypted()), q, SLOT(replyEncrypted()), |
| Qt::BlockingQueuedConnection); |
| QObject::connect(delegate, SIGNAL(sslErrors(QList<QSslError>,bool*,QList<QSslError>*)), |
| q, SLOT(replySslErrors(QList<QSslError>,bool*,QList<QSslError>*)), |
| Qt::BlockingQueuedConnection); |
| QObject::connect(delegate, SIGNAL(preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)), |
| q, SLOT(replyPreSharedKeyAuthenticationRequiredSlot(QSslPreSharedKeyAuthenticator*)), |
| 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())); |
| |
| // To throttle the connection. |
| QObject::connect(q, SIGNAL(readBufferSizeChanged(qint64)), delegate, SLOT(readBufferSizeChanged(qint64))); |
| QObject::connect(q, SIGNAL(readBufferFreed(qint64)), delegate, SLOT(readBufferFreed(qint64))); |
| |
| if (uploadByteDevice) { |
| QNonContiguousByteDeviceThreadForwardImpl *forwardUploadDevice = |
| new QNonContiguousByteDeviceThreadForwardImpl(uploadByteDevice->atEnd(), uploadByteDevice->size()); |
| forwardUploadDevice->setParent(delegate); // needed to make sure it is moved on moveToThread() |
| delegate->httpRequest.setUploadByteDevice(forwardUploadDevice); |
| |
| // If the device in the user thread claims it has more data, keep the flow to HTTP thread going |
| QObject::connect(uploadByteDevice.data(), SIGNAL(readyRead()), |
| q, SLOT(uploadByteDeviceReadyReadSlot()), |
| Qt::QueuedConnection); |
| |
| // From user thread to http thread: |
| QObject::connect(q, SIGNAL(haveUploadData(qint64,QByteArray,bool,qint64)), |
| forwardUploadDevice, SLOT(haveDataSlot(qint64,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,qint64)), |
| q, SLOT(sentUploadDataSlot(qint64,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. |
| |
| // Prepare timers for progress notifications |
| downloadProgressSignalChoke.start(); |
| uploadProgressSignalChoke.invalidate(); |
| |
| // 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, |
| delegate->removedContentLength, |
| delegate->isSpdyUsed); |
| replyDownloadData(delegate->synchronousDownloadData); |
| httpError(delegate->incomingErrorCode, delegate->incomingErrorDetail); |
| } else { |
| replyDownloadMetaData |
| (delegate->incomingHeaders, |
| delegate->incomingStatusCode, |
| delegate->incomingReasonPhrase, |
| delegate->isPipeliningUsed, |
| QSharedPointer<char>(), |
| delegate->incomingContentLength, |
| delegate->removedContentLength, |
| delegate->isSpdyUsed); |
| replyDownloadData(delegate->synchronousDownloadData); |
| } |
| |
| thread->quit(); |
| thread->wait(5000); |
| if (thread->isFinished()) |
| delete thread; |
| else |
| QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); |
| |
| 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(httpRequest.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) |
| q->connect(cacheSaveDevice, SIGNAL(aboutToClose()), SLOT(_q_cacheSaveDeviceAboutToClose())); |
| |
| if (!cacheSaveDevice || (cacheSaveDevice && !cacheSaveDevice->isOpen())) { |
| if (Q_UNLIKELY(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 = nullptr; |
| cacheEnabled = false; |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| |
| // If we're closed just ignore this data |
| if (!q->isOpen()) |
| return; |
| |
| if (cacheEnabled && isCachingAllowed() && !cacheSaveDevice) |
| initCacheSaveDevice(); |
| |
| // This is going to look a little strange. When downloading data while a |
| // HTTP redirect is happening (and enabled), we write the redirect |
| // response to the cache. However, we do not append it to our internal |
| // buffer as that will contain the response data only for the final |
| // response |
| if (cacheSaveDevice) |
| cacheSaveDevice->write(d); |
| |
| if (!isHttpRedirectResponse()) { |
| buffer.append(d); |
| bytesDownloaded += d.size(); |
| } |
| bytesBuffered += d.size(); |
| |
| int pendingSignals = pendingDownloadDataEmissions->fetchAndSubAcquire(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) |
| return; |
| } |
| |
| if (isHttpRedirectResponse()) |
| return; |
| |
| QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); |
| if (preMigrationDownloaded != Q_INT64_C(-1)) |
| totalSize = totalSize.toLongLong() + preMigrationDownloaded; |
| |
| 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). |
| if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
| downloadProgressSignalChoke.restart(); |
| emit q->downloadProgress(bytesDownloaded, |
| totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); |
| } |
| |
| } |
| |
| void QNetworkReplyHttpImplPrivate::replyFinished() |
| { |
| // We are already loading from cache, we still however |
| // got this signal because it was posted already |
| if (loadingFromCache) |
| return; |
| |
| finished(); |
| } |
| |
| QNetworkAccessManager::Operation QNetworkReplyHttpImplPrivate::getRedirectOperation(QNetworkAccessManager::Operation currentOp, int httpStatus) |
| { |
| // HTTP status code can be used to decide if we can redirect with a GET |
| // operation or not. See http://www.ietf.org/rfc/rfc2616.txt [Sec 10.3] for |
| // more details |
| |
| // We MUST keep using the verb that was used originally when being redirected with 307 or 308. |
| if (httpStatus == 307 || httpStatus == 308) |
| return currentOp; |
| |
| switch (currentOp) { |
| case QNetworkAccessManager::HeadOperation: |
| return QNetworkAccessManager::HeadOperation; |
| default: |
| break; |
| } |
| // Use GET for everything else. |
| return QNetworkAccessManager::GetOperation; |
| } |
| |
| bool QNetworkReplyHttpImplPrivate::isHttpRedirectResponse() const |
| { |
| return httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(statusCode); |
| } |
| |
| QNetworkRequest QNetworkReplyHttpImplPrivate::createRedirectRequest(const QNetworkRequest &originalRequest, |
| const QUrl &url, |
| int maxRedirectsRemaining) |
| { |
| QNetworkRequest newRequest(originalRequest); |
| newRequest.setUrl(url); |
| newRequest.setMaximumRedirectsAllowed(maxRedirectsRemaining); |
| |
| return newRequest; |
| } |
| |
| void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int httpStatus, int maxRedirectsRemaining) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| Q_ASSERT(manager); |
| Q_ASSERT(managerPrivate); |
| |
| if (isFinished) |
| return; |
| |
| const QString schemeBefore(url.scheme()); |
| if (httpRequest.isFollowRedirects()) // update the reply's url as it could've changed |
| url = redirectUrl; |
| |
| if (managerPrivate->stsEnabled && managerPrivate->stsCache.isKnownHost(url)) { |
| // RFC6797, 8.3: |
| // The UA MUST replace the URI scheme with "https" [RFC2818], |
| // and if the URI contains an explicit port component of "80", |
| // then the UA MUST convert the port component to be "443", or |
| // if the URI contains an explicit port component that is not |
| // equal to "80", the port component value MUST be preserved; |
| // otherwise, if the URI does not contain an explicit port |
| // component, the UA MUST NOT add one. |
| url.setScheme(QLatin1String("https")); |
| if (url.port() == 80) |
| url.setPort(443); |
| } |
| |
| const bool isLessSafe = schemeBefore == QLatin1String("https") |
| && url.scheme() == QLatin1String("http"); |
| if (httpRequest.redirectPolicy() == QNetworkRequest::NoLessSafeRedirectPolicy |
| && isLessSafe) { |
| error(QNetworkReply::InsecureRedirectError, |
| QCoreApplication::translate("QHttp", "Insecure redirect")); |
| return; |
| } |
| |
| redirectRequest = createRedirectRequest(originalRequest, url, maxRedirectsRemaining); |
| operation = getRedirectOperation(operation, httpStatus); |
| |
| if (const QNetworkCookieJar *const cookieJar = manager->cookieJar()) { |
| auto cookies = cookieJar->cookiesForUrl(url); |
| if (!cookies.empty()) { |
| redirectRequest.setHeader(QNetworkRequest::KnownHeaders::CookieHeader, |
| QVariant::fromValue(cookies)); |
| } |
| } |
| |
| if (httpRequest.redirectPolicy() != QNetworkRequest::UserVerifiedRedirectPolicy) |
| followRedirect(); |
| |
| emit q->redirected(url); |
| } |
| |
| void QNetworkReplyHttpImplPrivate::followRedirect() |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| Q_ASSERT(managerPrivate); |
| |
| rawHeaders.clear(); |
| cookedHeaders.clear(); |
| |
| if (managerPrivate->thread) |
| managerPrivate->thread->disconnect(); |
| |
| #if QT_CONFIG(bearermanagement) |
| // If the original request didn't need a session (i.e. it was to localhost) |
| // then we might not have a session open, to which to redirect, if the |
| // new URL is remote. When this happens, we need to open the session now: |
| if (isSessionNeeded(url)) { |
| if (auto session = managerPrivate->getNetworkSession()) { |
| if (session->state() != QNetworkSession::State::Connected || !session->isOpen()) { |
| startWaitForSession(session); |
| // Need to set 'request' to the redirectRequest so that when QNAM restarts |
| // the request after the session starts it will not repeat the previous request. |
| request = redirectRequest; |
| // Return now, QNAM will start the request when the session has started. |
| return; |
| } |
| } |
| } |
| #endif // bearer management |
| |
| QMetaObject::invokeMethod(q, "start", Qt::QueuedConnection, |
| Q_ARG(QNetworkRequest, redirectRequest)); |
| } |
| |
| 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 |
| case 308: // Permanent 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(QString::fromUtf8(header)); |
| if (!url.isValid()) |
| url = QUrl(QLatin1String(header)); |
| q->setAttribute(QNetworkRequest::RedirectionTargetAttribute, url); |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QList<QPair<QByteArray,QByteArray> > &hm, |
| int sc, const QString &rp, bool pu, |
| QSharedPointer<char> db, |
| qint64 contentLength, |
| qint64 removedContentLength, |
| bool spdyWasUsed) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| Q_UNUSED(contentLength); |
| |
| statusCode = sc; |
| reasonPhrase = rp; |
| |
| #ifndef QT_NO_SSL |
| // We parse this header only if we're using secure transport: |
| // |
| // RFC6797, 8.1 |
| // If an HTTP response is received over insecure transport, the UA MUST |
| // ignore any present STS header field(s). |
| if (url.scheme() == QLatin1String("https") && managerPrivate->stsEnabled) |
| managerPrivate->stsCache.updateFromHeaders(hm, url); |
| #endif |
| // Download buffer |
| if (!db.isNull()) { |
| downloadBufferPointer = db; |
| downloadZerocopyBuffer = downloadBufferPointer.data(); |
| downloadBufferCurrentSize = 0; |
| q->setAttribute(QNetworkRequest::DownloadBufferAttribute, QVariant::fromValue<QSharedPointer<char> > (downloadBufferPointer)); |
| } |
| |
| q->setAttribute(QNetworkRequest::HttpPipeliningWasUsedAttribute, pu); |
| const QVariant http2Allowed = request.attribute(QNetworkRequest::HTTP2AllowedAttribute); |
| const QVariant http2Direct = request.attribute(QNetworkRequest::Http2DirectAttribute); |
| if ((http2Allowed.isValid() && http2Allowed.toBool()) |
| || (http2Direct.isValid() && http2Direct.toBool())) { |
| q->setAttribute(QNetworkRequest::HTTP2WasUsedAttribute, spdyWasUsed); |
| q->setAttribute(QNetworkRequest::SpdyWasUsedAttribute, false); |
| } else { |
| q->setAttribute(QNetworkRequest::SpdyWasUsedAttribute, spdyWasUsed); |
| q->setAttribute(QNetworkRequest::HTTP2WasUsedAttribute, false); |
| } |
| |
| // reconstruct the HTTP header |
| QList<QPair<QByteArray, QByteArray> > headerMap = hm; |
| QList<QPair<QByteArray, QByteArray> >::ConstIterator it = headerMap.constBegin(), |
| end = headerMap.constEnd(); |
| for (; it != end; ++it) { |
| QByteArray value = q->rawHeader(it->first); |
| |
| // Reset any previous "location" header set in the reply. In case of |
| // redirects, we don't want to 'append' multiple location header values, |
| // rather we keep only the latest one |
| if (it->first.toLower() == "location") |
| value.clear(); |
| |
| if (!value.isEmpty()) { |
| // Why are we appending values for headers which are already |
| // present? |
| if (it->first.compare("set-cookie", Qt::CaseInsensitive) == 0) |
| value += '\n'; |
| else |
| value += ", "; |
| } |
| value += it->second; |
| q->setRawHeader(it->first, value); |
| } |
| |
| q->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCode); |
| q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reasonPhrase); |
| if (removedContentLength != -1) |
| q->setAttribute(QNetworkRequest::OriginalContentLengthAttribute, removedContentLength); |
| |
| // is it a redirection? |
| if (!isHttpRedirectResponse()) |
| checkForRedirect(statusCode); |
| |
| if (statusCode >= 500 && statusCode < 600) { |
| QAbstractNetworkCache *nc = managerPrivate->networkCache; |
| if (nc) { |
| QNetworkCacheMetaData metaData = nc->metaData(httpRequest.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" << request.url(); |
| #endif |
| QAbstractNetworkCache *nc = managerPrivate->networkCache; |
| if (nc) { |
| QNetworkCacheMetaData oldMetaData = nc->metaData(httpRequest.url()); |
| QNetworkCacheMetaData metaData = fetchCacheMetaData(oldMetaData); |
| if (oldMetaData != metaData) |
| nc->updateMetaData(metaData); |
| if (sendCacheContents(metaData)) |
| return; |
| } |
| } |
| |
| |
| if (statusCode != 304 && statusCode != 303) { |
| if (!isCachingEnabled()) |
| setCachingEnabled(true); |
| } |
| |
| _q_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 && isCachingAllowed() && 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? |
| } |
| |
| if (isHttpRedirectResponse()) |
| return; |
| |
| bytesDownloaded = 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(); |
| if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
| downloadProgressSignalChoke.restart(); |
| emit q->downloadProgress(bytesDownloaded, bytesTotal); |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::httpAuthenticationRequired(const QHttpNetworkRequest &request, |
| QAuthenticator *auth) |
| { |
| managerPrivate->authenticationRequired(auth, q_func(), synchronous, url, &urlForLastAuthentication, request.withCredentials()); |
| } |
| |
| #ifndef QT_NO_NETWORKPROXY |
| void QNetworkReplyHttpImplPrivate::proxyAuthenticationRequired(const QNetworkProxy &proxy, |
| QAuthenticator *authenticator) |
| { |
| managerPrivate->proxyAuthenticationRequired(request.url(), 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_SSL |
| void QNetworkReplyHttpImplPrivate::replyEncrypted() |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| emit q->encrypted(); |
| } |
| |
| 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 &newSslConfiguration) |
| { |
| // Receiving the used SSL configuration from the HTTP thread |
| if (sslConfiguration.data()) |
| *sslConfiguration = newSslConfiguration; |
| else |
| sslConfiguration.reset(new QSslConfiguration(newSslConfiguration)); |
| } |
| |
| void QNetworkReplyHttpImplPrivate::replyPreSharedKeyAuthenticationRequiredSlot(QSslPreSharedKeyAuthenticator *authenticator) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| emit q->preSharedKeyAuthenticationRequired(authenticator); |
| } |
| #endif |
| |
| // Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread |
| void QNetworkReplyHttpImplPrivate::resetUploadDataSlot(bool *r) |
| { |
| *r = uploadByteDevice->reset(); |
| if (*r) { |
| // reset our own position which is used for the inter-thread communication |
| uploadByteDevicePosition = 0; |
| } |
| } |
| |
| // Coming from QNonContiguousByteDeviceThreadForwardImpl in HTTP thread |
| void QNetworkReplyHttpImplPrivate::sentUploadDataSlot(qint64 pos, qint64 amount) |
| { |
| if (uploadByteDevicePosition + amount != pos) { |
| // Sanity check, should not happen. |
| error(QNetworkReply::UnknownNetworkError, QString()); |
| return; |
| } |
| uploadByteDevice->advanceReadPointer(amount); |
| uploadByteDevicePosition += 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)); |
| |
| if (currentUploadDataLength == 0) { |
| uploadDeviceChoking = true; |
| // No bytes from upload byte device. There will be bytes later, it will emit readyRead() |
| // and our uploadByteDeviceReadyReadSlot() is called. |
| return; |
| } else { |
| uploadDeviceChoking = false; |
| } |
| |
| // Let's make a copy of this data |
| QByteArray dataArray(data, currentUploadDataLength); |
| |
| // Communicate back to HTTP thread |
| emit q->haveUploadData(uploadByteDevicePosition, dataArray, uploadByteDevice->atEnd(), uploadByteDevice->size()); |
| } |
| |
| void QNetworkReplyHttpImplPrivate::uploadByteDeviceReadyReadSlot() |
| { |
| // Start the flow between this thread and the HTTP thread again by triggering a upload. |
| // However only do this when we were choking before, else the state in |
| // QNonContiguousByteDeviceThreadForwardImpl gets messed up. |
| if (uploadDeviceChoking) { |
| uploadDeviceChoking = false; |
| wantUploadDataSlot(1024); |
| } |
| } |
| |
| |
| /* |
| 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() << "Cannot 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 |
| |
| statusCode = status; |
| |
| 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(); |
| QUrl redirectUrl; |
| for ( ; it != end; ++it) { |
| if (httpRequest.isFollowRedirects() && |
| !it->first.compare("location", Qt::CaseInsensitive)) |
| redirectUrl = QUrl::fromEncoded(it->second); |
| setRawHeader(it->first, it->second); |
| } |
| |
| if (!isHttpRedirectResponse()) |
| 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, "_q_metaDataChanged", Qt::QueuedConnection); |
| QMetaObject::invokeMethod(q, "_q_cacheLoadReadyRead", Qt::QueuedConnection); |
| |
| |
| #if defined(QNETWORKACCESSHTTPBACKEND_DEBUG) |
| qDebug() << "Successfully sent cache:" << url << contents->size() << "bytes"; |
| #endif |
| |
| // Do redirect processing |
| if (httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(status)) { |
| QMetaObject::invokeMethod(q, "onRedirected", Qt::QueuedConnection, |
| Q_ARG(QUrl, redirectUrl), |
| Q_ARG(int, status), |
| Q_ARG(int, httpRequest.redirectCount() - 1)); |
| } |
| |
| // 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; |
| |
| const QList<QByteArray> newHeaders = q->rawHeaderList(); |
| for (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; |
| |
| if (header == "set-cookie") |
| 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; |
| } |
| |
| // IIS has been known to send "Content-Length: 0" on 304 responses, so |
| // ignore this too |
| if (header == "content-length" && statusCode == 304) |
| continue; |
| |
| #if defined(QNETWORKACCESSHTTPBACKEND_DEBUG) |
| QByteArray n = q->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::currentDateTimeUtc(); |
| 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; |
| // HTTP/1.1. Check the Cache-Control header |
| 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 \c true if the backend is started. Returns \c 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(const QNetworkRequest &newHttpRequest) |
| { |
| #ifndef QT_NO_BEARERMANAGEMENT |
| QSharedPointer<QNetworkSession> networkSession(managerPrivate->getNetworkSession()); |
| if (!networkSession || QNetworkStatusMonitor::isEnabled()) { |
| #endif |
| postRequest(newHttpRequest); |
| return true; |
| #ifndef QT_NO_BEARERMANAGEMENT |
| } |
| |
| // This is not ideal. |
| if (!isSessionNeeded(url)) { |
| // Don't need to check for an open session if we don't need one. |
| postRequest(newHttpRequest); |
| return true; |
| } |
| |
| if (networkSession->isOpen() && |
| networkSession->state() == QNetworkSession::Connected) { |
| Q_Q(QNetworkReplyHttpImpl); |
| QObject::connect(networkSession.data(), SIGNAL(usagePoliciesChanged(QNetworkSession::UsagePolicies)), |
| q, SLOT(_q_networkSessionUsagePoliciesChanged(QNetworkSession::UsagePolicies))); |
| postRequest(newHttpRequest); |
| return true; |
| } else if (synchronous) { |
| // Command line applications using the synchronous path such as xmlpatterns may need an extra push. |
| networkSession->open(); |
| if (networkSession->waitForOpened()) { |
| postRequest(newHttpRequest); |
| return true; |
| } |
| } |
| return false; |
| #endif |
| } |
| |
| #if QT_CONFIG(bearermanagement) |
| bool QNetworkReplyHttpImplPrivate::startWaitForSession(QSharedPointer<QNetworkSession> &session) |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| state = WaitingForSession; |
| |
| if (session) { |
| QObject::connect(session.data(), SIGNAL(error(QNetworkSession::SessionError)), |
| q, SLOT(_q_networkSessionFailed()), Qt::QueuedConnection); |
| |
| if (!session->isOpen()) { |
| QVariant isBackground = request.attribute(QNetworkRequest::BackgroundRequestAttribute, |
| QVariant::fromValue(false)); |
| session->setSessionProperty(QStringLiteral("ConnectInBackground"), isBackground); |
| session->open(); |
| } |
| return true; |
| } |
| const Qt::ConnectionType connection = synchronous ? Qt::DirectConnection : Qt::QueuedConnection; |
| qWarning("Backend is waiting for QNetworkSession to connect, but there is none!"); |
| QMetaObject::invokeMethod(q, "_q_error", connection, |
| Q_ARG(QNetworkReply::NetworkError, QNetworkReply::NetworkSessionFailedError), |
| Q_ARG(QString, QCoreApplication::translate("QNetworkReply", "Network session error."))); |
| QMetaObject::invokeMethod(q, "_q_finished", connection); |
| return false; |
| } |
| #endif // QT_CONFIG(bearermanagement) |
| |
| void QNetworkReplyHttpImplPrivate::_q_startOperation() |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| |
| if (state == Working) // ensure this function is only being called once |
| return; |
| |
| state = Working; |
| |
| #ifndef QT_NO_BEARERMANAGEMENT |
| // Do not start background requests if they are not allowed by session policy |
| QSharedPointer<QNetworkSession> session(manager->d_func()->getNetworkSession()); |
| QVariant isBackground = request.attribute(QNetworkRequest::BackgroundRequestAttribute, QVariant::fromValue(false)); |
| if (isBackground.toBool() && session && session->usagePolicies().testFlag(QNetworkSession::NoBackgroundTrafficPolicy)) { |
| QMetaObject::invokeMethod(q, "_q_error", synchronous ? Qt::DirectConnection : Qt::QueuedConnection, |
| Q_ARG(QNetworkReply::NetworkError, QNetworkReply::BackgroundRequestNotAllowedError), |
| Q_ARG(QString, QCoreApplication::translate("QNetworkReply", "Background request not allowed."))); |
| QMetaObject::invokeMethod(q, "_q_finished", synchronous ? Qt::DirectConnection : Qt::QueuedConnection); |
| return; |
| } |
| |
| if (!start(request)) { |
| // 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. |
| if (!startWaitForSession(session)) |
| return; |
| } else if (session && !QNetworkStatusMonitor::isEnabled()) { |
| QObject::connect(session.data(), SIGNAL(stateChanged(QNetworkSession::State)), |
| q, SLOT(_q_networkSessionStateChanged(QNetworkSession::State)), |
| Qt::QueuedConnection); |
| } |
| #else |
| if (!start(request)) { |
| qWarning("Backend start failed"); |
| QMetaObject::invokeMethod(q, "_q_error", synchronous ? Qt::DirectConnection : Qt::QueuedConnection, |
| Q_ARG(QNetworkReply::NetworkError, QNetworkReply::UnknownNetworkError), |
| Q_ARG(QString, QCoreApplication::translate("QNetworkReply", "backend start error."))); |
| QMetaObject::invokeMethod(q, "_q_finished", synchronous ? Qt::DirectConnection : Qt::QueuedConnection); |
| return; |
| } |
| #endif // QT_NO_BEARERMANAGEMENT |
| |
| if (synchronous) { |
| state = Finished; |
| q_func()->setFinished(true); |
| } |
| } |
| |
| 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 ? |
| |
| |
| QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); |
| |
| // emit readyRead before downloadProgress incase this will cause events to be |
| // processed and we get into a recursive call (as in QProgressDialog). |
| |
| if (!(isHttpRedirectResponse())) { |
| // This readyRead() goes to the user. The user then may or may not read() anything. |
| emit q->readyRead(); |
| |
| if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
| downloadProgressSignalChoke.restart(); |
| emit q->downloadProgress(bytesDownloaded, |
| totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); |
| } |
| } |
| |
| // A signal we've emitted might be handled by a slot that aborts, |
| // so we need to check for that and bail out if it's happened: |
| if (!q->isOpen()) |
| return; |
| |
| // 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() && !isHttpRedirectResponse()) |
| buffer.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 = nullptr; |
| 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 = nullptr; |
| 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_cacheSaveDeviceAboutToClose() |
| { |
| // do not keep a dangling pointer to the device around (device |
| // is closing because e.g. QAbstractNetworkCache::remove() was called). |
| cacheSaveDevice = nullptr; |
| } |
| |
| void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingData() |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| |
| if (!outgoingDataBuffer) { |
| // first call, create our buffer |
| outgoingDataBuffer = QSharedPointer<QRingBuffer>::create(); |
| |
| 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); |
| Q_ASSERT(managerPrivate); |
| |
| QSharedPointer<QNetworkSession> session = managerPrivate->getNetworkSession(); |
| if (!session) |
| return; |
| |
| if (session->state() != QNetworkSession::Connected) |
| return; |
| |
| switch (state) { |
| case QNetworkReplyPrivate::Buffering: |
| case QNetworkReplyPrivate::Working: |
| case QNetworkReplyPrivate::Reconnecting: |
| // Migrate existing downloads to new network connection. |
| migrateBackend(); |
| break; |
| case QNetworkReplyPrivate::WaitingForSession: |
| // Start waiting requests. |
| QMetaObject::invokeMethod(q, "_q_startOperation", Qt::QueuedConnection); |
| break; |
| default: |
| ; |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::_q_networkSessionStateChanged(QNetworkSession::State sessionState) |
| { |
| if (sessionState == QNetworkSession::Disconnected |
| && state != Idle && state != Reconnecting) { |
| error(QNetworkReplyImpl::NetworkSessionFailedError, |
| QCoreApplication::translate("QNetworkReply", "Network session error.")); |
| finished(); |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::_q_networkSessionFailed() |
| { |
| // Abort waiting and working replies. |
| if (state == WaitingForSession || state == Working) { |
| state = Working; |
| QSharedPointer<QNetworkSession> session(manager->d_func()->getNetworkSession()); |
| QString errorStr; |
| if (session) |
| errorStr = session->errorString(); |
| else |
| errorStr = QCoreApplication::translate("QNetworkReply", "Network session error."); |
| error(QNetworkReplyImpl::NetworkSessionFailedError, errorStr); |
| finished(); |
| } |
| } |
| |
| void QNetworkReplyHttpImplPrivate::_q_networkSessionUsagePoliciesChanged(QNetworkSession::UsagePolicies newPolicies) |
| { |
| if (request.attribute(QNetworkRequest::BackgroundRequestAttribute).toBool()) { |
| if (newPolicies & QNetworkSession::NoBackgroundTrafficPolicy) { |
| // Abort waiting and working replies. |
| if (state == WaitingForSession || state == Working) { |
| state = Working; |
| error(QNetworkReply::BackgroundRequestNotAllowedError, |
| QCoreApplication::translate("QNetworkReply", "Background request not allowed.")); |
| finished(); |
| } |
| // ### if canResume(), then we could resume automatically |
| } |
| } |
| |
| } |
| #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; |
| |
| if (!emitAllUploadProgressSignals) { |
| //choke signal emissions, except the first and last signals which are unconditional |
| if (uploadProgressSignalChoke.isValid()) { |
| if (bytesSent != bytesTotal && uploadProgressSignalChoke.elapsed() < progressSignalInterval) { |
| return; |
| } |
| uploadProgressSignalChoke.restart(); |
| } else { |
| uploadProgressSignalChoke.start(); |
| } |
| } |
| |
| emit q->uploadProgress(bytesSent, bytesTotal); |
| } |
| |
| QNonContiguousByteDevice* QNetworkReplyHttpImplPrivate::createUploadByteDevice() |
| { |
| Q_Q(QNetworkReplyHttpImpl); |
| |
| if (outgoingDataBuffer) |
| uploadByteDevice = QNonContiguousByteDeviceFactory::createShared(outgoingDataBuffer); |
| else if (outgoingData) { |
| uploadByteDevice = QNonContiguousByteDeviceFactory::createShared(outgoingData); |
| } else { |
| return nullptr; |
| } |
| |
| // 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; |
| |
| QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); |
| if (preMigrationDownloaded != Q_INT64_C(-1)) |
| totalSize = totalSize.toLongLong() + preMigrationDownloaded; |
| |
| #ifndef QT_NO_BEARERMANAGEMENT |
| Q_ASSERT(managerPrivate); |
| QSharedPointer<QNetworkSession> session = managerPrivate->getNetworkSession(); |
| if (!QNetworkStatusMonitor::isEnabled() && 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) { |
| return; // exit early if we are migrating. |
| } |
| } else { |
| error(QNetworkReply::TemporaryNetworkFailureError, |
| QNetworkReply::tr("Temporary network failure.")); |
| } |
| } |
| } |
| } |
| #endif |
| |
| // if we don't know the total size of or we received everything save the cache |
| if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize) |
| completeCacheSave(); |
| |
| // We check for errorCode too as in case of SSL handshake failure, we still |
| // get the HTTP redirect status code (301, 303 etc) |
| if (isHttpRedirectResponse() && errorCode == QNetworkReply::NoError) |
| return; |
| |
| state = Finished; |
| q->setFinished(true); |
| |
| if (totalSize.isNull() || totalSize == -1) { |
| emit q->downloadProgress(bytesDownloaded, bytesDownloaded); |
| } else { |
| emit q->downloadProgress(bytesDownloaded, totalSize.toLongLong()); |
| } |
| |
| if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer)) |
| emit q->uploadProgress(0, 0); |
| |
| emit q->readChannelFinished(); |
| emit q->finished(); |
| } |
| |
| 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::_q_metaDataChanged() |
| { |
| // FIXME merge this with replyDownloadMetaData(); ? |
| |
| Q_Q(QNetworkReplyHttpImpl); |
| // 1. do we have cookies? |
| // 2. are we allowed to set them? |
| Q_ASSERT(manager); |
| const auto it = cookedHeaders.constFind(QNetworkRequest::SetCookieHeader); |
| if (it != cookedHeaders.cend() |
| && request.attribute(QNetworkRequest::CookieSaveControlAttribute, |
| QNetworkRequest::Automatic).toInt() == QNetworkRequest::Automatic) { |
| QNetworkCookieJar *jar = manager->cookieJar(); |
| if (jar) { |
| QList<QNetworkCookie> cookies = |
| qvariant_cast<QList<QNetworkCookie> >(it.value()); |
| 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 \c 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; |
| |
| cookedHeaders.clear(); |
| rawHeaders.clear(); |
| |
| preMigrationDownloaded = bytesDownloaded; |
| |
| setResumeOffset(bytesDownloaded); |
| |
| emit q->abortHttpRequest(); |
| |
| 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()) |
| return; |
| cacheEnabled = true; |
| } |
| |
| bool QNetworkReplyHttpImplPrivate::isCachingEnabled() const |
| { |
| return (cacheEnabled && managerPrivate->networkCache != nullptr); |
| } |
| |
| void QNetworkReplyHttpImplPrivate::setCachingEnabled(bool enable) |
| { |
| if (!enable && !cacheEnabled) |
| return; // nothing to do |
| if (enable && cacheEnabled) |
| return; // nothing to do either! |
| |
| if (enable) { |
| if (Q_UNLIKELY(bytesDownloaded)) { |
| qDebug() << "setCachingEnabled: " << bytesDownloaded << " 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 = nullptr; |
| cacheEnabled = false; |
| } |
| } |
| |
| bool QNetworkReplyHttpImplPrivate::isCachingAllowed() const |
| { |
| return operation == QNetworkAccessManager::GetOperation || operation == QNetworkAccessManager::HeadOperation; |
| } |
| |
| void QNetworkReplyHttpImplPrivate::completeCacheSave() |
| { |
| if (cacheEnabled && errorCode != QNetworkReplyImpl::NoError) { |
| managerPrivate->networkCache->remove(url); |
| } else if (cacheEnabled && cacheSaveDevice) { |
| managerPrivate->networkCache->insert(cacheSaveDevice); |
| } |
| cacheSaveDevice = nullptr; |
| cacheEnabled = false; |
| } |
| |
| QT_END_NAMESPACE |