From 5b14bf342f43bd6cb02ad751db8da851850814bb Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Tue, 21 Jan 2014 16:28:01 +0100 Subject: HTTP internals: introduce protocol handlers ... to defer the decision which protocol will be used on a specific channel. This is to allow using the SPDY protocol instead of HTTP (to be implemented in a later commit); which protocol will be used can only be decided after the SSL handshake. Change-Id: I6b538320668fe4994438f0095ecdc445677cf0a6 Reviewed-by: Peter Hartmann --- src/network/access/access.pri | 4 + src/network/access/qabstractprotocolhandler.cpp | 68 ++++ src/network/access/qabstractprotocolhandler_p.h | 88 +++++ src/network/access/qhttpnetworkconnection_p.h | 1 + .../access/qhttpnetworkconnectionchannel.cpp | 382 +----------------- .../access/qhttpnetworkconnectionchannel_p.h | 4 + src/network/access/qhttpnetworkreply_p.h | 1 + src/network/access/qhttpprotocolhandler.cpp | 431 +++++++++++++++++++++ src/network/access/qhttpprotocolhandler_p.h | 77 ++++ 9 files changed, 692 insertions(+), 364 deletions(-) create mode 100644 src/network/access/qabstractprotocolhandler.cpp create mode 100644 src/network/access/qabstractprotocolhandler_p.h create mode 100644 src/network/access/qhttpprotocolhandler.cpp create mode 100644 src/network/access/qhttpprotocolhandler_p.h (limited to 'src') diff --git a/src/network/access/access.pri b/src/network/access/access.pri index aaaf05b551..3017417da8 100644 --- a/src/network/access/access.pri +++ b/src/network/access/access.pri @@ -7,6 +7,8 @@ HEADERS += \ access/qhttpnetworkreply_p.h \ access/qhttpnetworkconnection_p.h \ access/qhttpnetworkconnectionchannel_p.h \ + access/qabstractprotocolhandler_p.h \ + access/qhttpprotocolhandler_p.h \ access/qnetworkaccessauthenticationmanager_p.h \ access/qnetworkaccessmanager.h \ access/qnetworkaccessmanager_p.h \ @@ -43,6 +45,8 @@ SOURCES += \ access/qhttpnetworkreply.cpp \ access/qhttpnetworkconnection.cpp \ access/qhttpnetworkconnectionchannel.cpp \ + access/qabstractprotocolhandler.cpp \ + access/qhttpprotocolhandler.cpp \ access/qnetworkaccessauthenticationmanager.cpp \ access/qnetworkaccessmanager.cpp \ access/qnetworkaccesscache.cpp \ diff --git a/src/network/access/qabstractprotocolhandler.cpp b/src/network/access/qabstractprotocolhandler.cpp new file mode 100644 index 0000000000..e72bb63236 --- /dev/null +++ b/src/network/access/qabstractprotocolhandler.cpp @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#ifndef QT_NO_HTTP + +QT_BEGIN_NAMESPACE + +QAbstractProtocolHandler::QAbstractProtocolHandler(QHttpNetworkConnectionChannel *channel) + : m_channel(channel), m_reply(0), m_socket(m_channel->socket), m_connection(m_channel->connection) +{ + Q_ASSERT(m_channel); + Q_ASSERT(m_socket); + Q_ASSERT(m_connection); +} + +QAbstractProtocolHandler::~QAbstractProtocolHandler() +{ +} + +void QAbstractProtocolHandler::setReply(QHttpNetworkReply *reply) +{ + m_reply = reply; +} + +QT_END_NAMESPACE + +#endif // QT_NO_HTTP diff --git a/src/network/access/qabstractprotocolhandler_p.h b/src/network/access/qabstractprotocolhandler_p.h new file mode 100644 index 0000000000..387d08ccac --- /dev/null +++ b/src/network/access/qabstractprotocolhandler_p.h @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QABSTRACTPROTOCOLHANDLER_H +#define QABSTRACTPROTOCOLHANDLER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QT_NO_HTTP + +#include + +QT_BEGIN_NAMESPACE + +class QHttpNetworkConnectionChannel; +class QHttpNetworkReply; +class QAbstractSocket; +class QHttpNetworkConnection; + +class QAbstractProtocolHandler { +public: + QAbstractProtocolHandler(QHttpNetworkConnectionChannel *channel); + virtual ~QAbstractProtocolHandler(); + + virtual void _q_receiveReply() = 0; + virtual void _q_readyRead() = 0; + virtual bool sendRequest() = 0; + void setReply(QHttpNetworkReply *reply); + +protected: + QHttpNetworkConnectionChannel *m_channel; + QHttpNetworkReply *m_reply; + QAbstractSocket *m_socket; + QHttpNetworkConnection *m_connection; +}; + +QT_END_NAMESPACE + +#endif // QT_NO_HTTP + +#endif // QABSTRACTPROTOCOLHANDLER_H diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index 2aaaad24ac..526326c3fd 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -139,6 +139,7 @@ private: friend class QHttpNetworkReply; friend class QHttpNetworkReplyPrivate; friend class QHttpNetworkConnectionChannel; + friend class QHttpProtocolHandler; Q_PRIVATE_SLOT(d_func(), void _q_startNextRequest()) Q_PRIVATE_SLOT(d_func(), void _q_hostLookupFinished(QHostInfo)) diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 6e786893ed..6f06c18732 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtNetwork module of the Qt Toolkit. @@ -48,6 +49,8 @@ #ifndef QT_NO_HTTP +#include + #ifndef QT_NO_SSL # include # include @@ -78,6 +81,7 @@ QHttpNetworkConnectionChannel::QHttpNetworkConnectionChannel() , proxyAuthMethod(QAuthenticatorPrivate::None) , authenticationCredentialsSent(false) , proxyCredentialsSent(false) + , protocolHandler(0) #ifndef QT_NO_SSL , ignoreAllSslErrors(false) #endif @@ -163,8 +167,8 @@ void QHttpNetworkConnectionChannel::init() if (!sslConfiguration.isNull()) sslSocket->setSslConfiguration(sslConfiguration); } - #endif + protocolHandler.reset(new QHttpProtocolHandler(this)); #ifndef QT_NO_NETWORKPROXY if (proxy.type() != QNetworkProxy::NoProxy) @@ -193,349 +197,21 @@ void QHttpNetworkConnectionChannel::close() bool QHttpNetworkConnectionChannel::sendRequest() { - if (!reply) { - // heh, how should that happen! - qWarning() << "QHttpNetworkConnectionChannel::sendRequest() called without QHttpNetworkReply"; - state = QHttpNetworkConnectionChannel::IdleState; - return false; - } - - switch (state) { - case QHttpNetworkConnectionChannel::IdleState: { // write the header - if (!ensureConnection()) { - // wait for the connection (and encryption) to be done - // sendRequest will be called again from either - // _q_connected or _q_encrypted - return false; - } - QString scheme = request.url().scheme(); - if (scheme == QLatin1String("preconnect-http") - || scheme == QLatin1String("preconnect-https")) { - state = QHttpNetworkConnectionChannel::IdleState; - reply->d_func()->state = QHttpNetworkReplyPrivate::AllDoneState; - allDone(); - connection->preConnectFinished(); // will only decrease the counter - reply = 0; // so we can reuse this channel - return true; // we have a working connection and are done - } - - written = 0; // excluding the header - bytesTotal = 0; - - QHttpNetworkReplyPrivate *replyPrivate = reply->d_func(); - replyPrivate->clear(); - replyPrivate->connection = connection; - replyPrivate->connectionChannel = this; - replyPrivate->autoDecompress = request.d->autoDecompress; - replyPrivate->pipeliningUsed = false; - - // if the url contains authentication parameters, use the new ones - // both channels will use the new authentication parameters - if (!request.url().userInfo().isEmpty() && request.withCredentials()) { - QUrl url = request.url(); - QAuthenticator &auth = authenticator; - if (url.userName() != auth.user() - || (!url.password().isEmpty() && url.password() != auth.password())) { - auth.setUser(url.userName(QUrl::FullyDecoded)); - auth.setPassword(url.password(QUrl::FullyDecoded)); - connection->d_func()->copyCredentials(connection->d_func()->indexOf(socket), &auth, false); - } - // clear the userinfo, since we use the same request for resending - // userinfo in url can conflict with the one in the authenticator - url.setUserInfo(QString()); - request.setUrl(url); - } - // Will only be false if Qt WebKit is performing a cross-origin XMLHttpRequest - // and withCredentials has not been set to true. - if (request.withCredentials()) - connection->d_func()->createAuthorization(socket, request); -#ifndef QT_NO_NETWORKPROXY - QByteArray header = QHttpNetworkRequestPrivate::header(request, - (connection->d_func()->networkProxy.type() != QNetworkProxy::NoProxy)); -#else - QByteArray header = QHttpNetworkRequestPrivate::header(request, false); -#endif - socket->write(header); - // flushing is dangerous (QSslSocket calls transmit which might read or error) -// socket->flush(); - QNonContiguousByteDevice* uploadByteDevice = request.uploadByteDevice(); - if (uploadByteDevice) { - // connect the signals so this function gets called again - QObject::connect(uploadByteDevice, SIGNAL(readyRead()),this, SLOT(_q_uploadDataReadyRead())); - - bytesTotal = request.contentLength(); - - state = QHttpNetworkConnectionChannel::WritingState; // start writing data - sendRequest(); //recurse - } else { - state = QHttpNetworkConnectionChannel::WaitingState; // now wait for response - sendRequest(); //recurse - } - - break; - } - case QHttpNetworkConnectionChannel::WritingState: - { - // write the data - QNonContiguousByteDevice* uploadByteDevice = request.uploadByteDevice(); - if (!uploadByteDevice || bytesTotal == written) { - if (uploadByteDevice) - emit reply->dataSendProgress(written, bytesTotal); - state = QHttpNetworkConnectionChannel::WaitingState; // now wait for response - sendRequest(); // recurse - break; - } - - // only feed the QTcpSocket buffer when there is less than 32 kB in it - const qint64 socketBufferFill = 32*1024; - const qint64 socketWriteMaxSize = 16*1024; - - -#ifndef QT_NO_SSL - QSslSocket *sslSocket = qobject_cast(socket); - // if it is really an ssl socket, check more than just bytesToWrite() - while ((socket->bytesToWrite() + (sslSocket ? sslSocket->encryptedBytesToWrite() : 0)) - <= socketBufferFill && bytesTotal != written) -#else - while (socket->bytesToWrite() <= socketBufferFill - && bytesTotal != written) -#endif - { - // get pointer to upload data - qint64 currentReadSize = 0; - qint64 desiredReadSize = qMin(socketWriteMaxSize, bytesTotal - written); - const char *readPointer = uploadByteDevice->readPointer(desiredReadSize, currentReadSize); - - if (currentReadSize == -1) { - // premature eof happened - connection->d_func()->emitReplyError(socket, reply, QNetworkReply::UnknownNetworkError); - return false; - } else if (readPointer == 0 || currentReadSize == 0) { - // nothing to read currently, break the loop - break; - } else { - qint64 currentWriteSize = socket->write(readPointer, currentReadSize); - if (currentWriteSize == -1 || currentWriteSize != currentReadSize) { - // socket broke down - connection->d_func()->emitReplyError(socket, reply, QNetworkReply::UnknownNetworkError); - return false; - } else { - written += currentWriteSize; - uploadByteDevice->advanceReadPointer(currentWriteSize); - - emit reply->dataSendProgress(written, bytesTotal); - - if (written == bytesTotal) { - // make sure this function is called once again - state = QHttpNetworkConnectionChannel::WaitingState; - sendRequest(); - break; - } - } - } - } - break; - } - - case QHttpNetworkConnectionChannel::WaitingState: - { - QNonContiguousByteDevice* uploadByteDevice = request.uploadByteDevice(); - if (uploadByteDevice) { - QObject::disconnect(uploadByteDevice, SIGNAL(readyRead()), this, SLOT(_q_uploadDataReadyRead())); - } - - // HTTP pipelining - //connection->d_func()->fillPipeline(socket); - //socket->flush(); - - // ensure we try to receive a reply in all cases, even if _q_readyRead_ hat not been called - // this is needed if the sends an reply before we have finished sending the request. In that - // case receiveReply had been called before but ignored the server reply - if (socket->bytesAvailable()) - QMetaObject::invokeMethod(this, "_q_receiveReply", Qt::QueuedConnection); - break; - } - case QHttpNetworkConnectionChannel::ReadingState: - // ignore _q_bytesWritten in these states - // fall through - default: - break; - } - return true; + Q_ASSERT(!protocolHandler.isNull()); + return protocolHandler->sendRequest(); } void QHttpNetworkConnectionChannel::_q_receiveReply() { - Q_ASSERT(socket); - - if (!reply) { - if (socket->bytesAvailable() > 0) - qWarning() << "QHttpNetworkConnectionChannel::_q_receiveReply() called without QHttpNetworkReply," - << socket->bytesAvailable() << "bytes on socket."; - close(); - return; - } - - // only run when the QHttpNetworkConnection is not currently being destructed, e.g. - // this function is called from _q_disconnected which is called because - // of ~QHttpNetworkConnectionPrivate - if (!qobject_cast(connection)) { - return; - } - - QAbstractSocket::SocketState socketState = socket->state(); - - // connection might be closed to signal the end of data - if (socketState == QAbstractSocket::UnconnectedState) { - if (socket->bytesAvailable() <= 0) { - if (reply->d_func()->state == QHttpNetworkReplyPrivate::ReadingDataState) { - // finish this reply. this case happens when the server did not send a content length - reply->d_func()->state = QHttpNetworkReplyPrivate::AllDoneState; - allDone(); - return; - } else { - handleUnexpectedEOF(); - return; - } - } else { - // socket not connected but still bytes for reading.. just continue in this function - } - } - - // read loop for the response - qint64 bytes = 0; - qint64 lastBytes = bytes; - do { - lastBytes = bytes; - - QHttpNetworkReplyPrivate::ReplyState state = reply->d_func()->state; - switch (state) { - case QHttpNetworkReplyPrivate::NothingDoneState: { - state = reply->d_func()->state = QHttpNetworkReplyPrivate::ReadingStatusState; - // fallthrough - } - case QHttpNetworkReplyPrivate::ReadingStatusState: { - qint64 statusBytes = reply->d_func()->readStatus(socket); - if (statusBytes == -1) { - // connection broke while reading status. also handled if later _q_disconnected is called - handleUnexpectedEOF(); - return; - } - bytes += statusBytes; - lastStatus = reply->d_func()->statusCode; - break; - } - case QHttpNetworkReplyPrivate::ReadingHeaderState: { - QHttpNetworkReplyPrivate *replyPrivate = reply->d_func(); - qint64 headerBytes = replyPrivate->readHeader(socket); - if (headerBytes == -1) { - // connection broke while reading headers. also handled if later _q_disconnected is called - handleUnexpectedEOF(); - return; - } - bytes += headerBytes; - // If headers were parsed successfully now it is the ReadingDataState - if (replyPrivate->state == QHttpNetworkReplyPrivate::ReadingDataState) { - if (replyPrivate->isCompressed() && replyPrivate->autoDecompress) { - // remove the Content-Length from header - replyPrivate->removeAutoDecompressHeader(); - } else { - replyPrivate->autoDecompress = false; - } - if (replyPrivate->statusCode == 100) { - replyPrivate->clearHttpLayerInformation(); - replyPrivate->state = QHttpNetworkReplyPrivate::ReadingStatusState; - break; // ignore - } - if (replyPrivate->shouldEmitSignals()) - emit reply->headerChanged(); - // After headerChanged had been emitted - // we can suddenly have a replyPrivate->userProvidedDownloadBuffer - // this is handled in the ReadingDataState however - - if (!replyPrivate->expectContent()) { - replyPrivate->state = QHttpNetworkReplyPrivate::AllDoneState; - allDone(); - break; - } - } - break; - } - case QHttpNetworkReplyPrivate::ReadingDataState: { - QHttpNetworkReplyPrivate *replyPrivate = reply->d_func(); - if (socket->state() == QAbstractSocket::ConnectedState && - replyPrivate->downstreamLimited && !replyPrivate->responseData.isEmpty() && replyPrivate->shouldEmitSignals()) { - // (only do the following when still connected, not when we have already been disconnected and there is still data) - // We already have some HTTP body data. We don't read more from the socket until - // this is fetched by QHttpNetworkAccessHttpBackend. If we would read more, - // we could not limit our read buffer usage. - // We only do this when shouldEmitSignals==true because our HTTP parsing - // always needs to parse the 401/407 replies. Therefore they don't really obey - // to the read buffer maximum size, but we don't care since they should be small. - return; - } - - if (replyPrivate->userProvidedDownloadBuffer) { - // the user provided a direct buffer where we should put all our data in. - // this only works when we can tell the user the content length and he/she can allocate - // the buffer in that size. - // note that this call will read only from the still buffered data - qint64 haveRead = replyPrivate->readBodyVeryFast(socket, replyPrivate->userProvidedDownloadBuffer + replyPrivate->totalProgress); - if (haveRead > 0) { - bytes += haveRead; - replyPrivate->totalProgress += haveRead; - // the user will get notified of it via progress signal - emit reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); - } else if (haveRead == 0) { - // Happens since this called in a loop. Currently no bytes available. - } else if (haveRead < 0) { - connection->d_func()->emitReplyError(socket, reply, QNetworkReply::RemoteHostClosedError); - break; - } - } else if (!replyPrivate->isChunked() && !replyPrivate->autoDecompress - && replyPrivate->bodyLength > 0) { - // bulk files like images should fulfill these properties and - // we can therefore save on memory copying - qint64 haveRead = replyPrivate->readBodyFast(socket, &replyPrivate->responseData); - bytes += haveRead; - replyPrivate->totalProgress += haveRead; - if (replyPrivate->shouldEmitSignals()) { - emit reply->readyRead(); - emit reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); - } - } - else - { - // use the traditional slower reading (for compressed encoding, chunked encoding, - // no content-length etc) - qint64 haveRead = replyPrivate->readBody(socket, &replyPrivate->responseData); - if (haveRead > 0) { - bytes += haveRead; - replyPrivate->totalProgress += haveRead; - if (replyPrivate->shouldEmitSignals()) { - emit reply->readyRead(); - emit reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); - } - } else if (haveRead == -1) { - // Some error occurred - connection->d_func()->emitReplyError(socket, reply, QNetworkReply::ProtocolFailure); - break; - } - } - // still in ReadingDataState? This function will be called again by the socket's readyRead - if (replyPrivate->state == QHttpNetworkReplyPrivate::ReadingDataState) - break; + Q_ASSERT(!protocolHandler.isNull()); + protocolHandler->_q_receiveReply(); +} - // everything done, fall through - } - case QHttpNetworkReplyPrivate::AllDoneState: - allDone(); - break; - default: - break; - } - } while (bytes != lastBytes && reply); +void QHttpNetworkConnectionChannel::_q_readyRead() +{ + Q_ASSERT(!protocolHandler.isNull()); + protocolHandler->_q_readyRead(); } // called when unexpectedly reading a -1 or when data is expected but socket is closed @@ -724,6 +400,7 @@ void QHttpNetworkConnectionChannel::allDone() if (!resendCurrent) { request = QHttpNetworkRequest(); reply = 0; + protocolHandler->setReply(0); } // move next from pipeline to current request @@ -738,6 +415,7 @@ void QHttpNetworkConnectionChannel::allDone() request = messagePair.first; reply = messagePair.second; + protocolHandler->setReply(messagePair.second); state = QHttpNetworkConnectionChannel::ReadingState; resendCurrent = false; @@ -982,32 +660,6 @@ bool QHttpNetworkConnectionChannel::isSocketReading() const return (state & QHttpNetworkConnectionChannel::ReadingState); } -//private slots -void QHttpNetworkConnectionChannel::_q_readyRead() -{ - if (socket->state() == QAbstractSocket::ConnectedState && socket->bytesAvailable() == 0) { - // We got a readyRead but no bytes are available.. - // This happens for the Unbuffered QTcpSocket - // Also check if socket is in ConnectedState since - // this function may also be invoked via the event loop. - char c; - qint64 ret = socket->peek(&c, 1); - if (ret < 0) { - _q_error(socket->error()); - // We still need to handle the reply so it emits its signals etc. - if (reply) - _q_receiveReply(); - return; - } - } - - if (isSocketWaiting() || isSocketReading()) { - state = QHttpNetworkConnectionChannel::ReadingState; - if (reply) - _q_receiveReply(); - } -} - void QHttpNetworkConnectionChannel::_q_bytesWritten(qint64 bytes) { Q_UNUSED(bytes); @@ -1224,6 +876,8 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket reply->d_func()->errorString = errorString; emit reply->finishedWithError(errorCode, errorString); reply = 0; + if (protocolHandler) + protocolHandler->setReply(0); } // send the next request QMetaObject::invokeMethod(that, "_q_startNextRequest", Qt::QueuedConnection); diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h index c8138b5453..7230eb2543 100644 --- a/src/network/access/qhttpnetworkconnectionchannel_p.h +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h @@ -66,6 +66,7 @@ #include #include +#include #ifndef QT_NO_HTTP @@ -117,6 +118,7 @@ public: QAuthenticator proxyAuthenticator; bool authenticationCredentialsSent; bool proxyCredentialsSent; + QScopedPointer protocolHandler; #ifndef QT_NO_SSL bool ignoreAllSslErrors; QList ignoreSslErrorsList; @@ -193,6 +195,8 @@ public: void _q_sslErrors(const QList &errors); // ssl errors from the socket void _q_encryptedBytesWritten(qint64 bytes); // proceed sending #endif + + friend class QHttpProtocolHandler; }; QT_END_NAMESPACE diff --git a/src/network/access/qhttpnetworkreply_p.h b/src/network/access/qhttpnetworkreply_p.h index 7aea9f14ec..583c3e426f 100644 --- a/src/network/access/qhttpnetworkreply_p.h +++ b/src/network/access/qhttpnetworkreply_p.h @@ -164,6 +164,7 @@ private: friend class QHttpNetworkConnection; friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; + friend class QHttpProtocolHandler; }; diff --git a/src/network/access/qhttpprotocolhandler.cpp b/src/network/access/qhttpprotocolhandler.cpp new file mode 100644 index 0000000000..15cba48285 --- /dev/null +++ b/src/network/access/qhttpprotocolhandler.cpp @@ -0,0 +1,431 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include + +#ifndef QT_NO_HTTP + +QT_BEGIN_NAMESPACE + +QHttpProtocolHandler::QHttpProtocolHandler(QHttpNetworkConnectionChannel *channel) + : QAbstractProtocolHandler(channel) +{ +} + +void QHttpProtocolHandler::_q_receiveReply() +{ + Q_ASSERT(m_socket); + + if (!m_reply) { + if (m_socket->bytesAvailable() > 0) + qWarning() << "QAbstractProtocolHandler::_q_receiveReply() called without QHttpNetworkReply," + << m_socket->bytesAvailable() << "bytes on socket."; + m_channel->close(); + return; + } + + // only run when the QHttpNetworkConnection is not currently being destructed, e.g. + // this function is called from _q_disconnected which is called because + // of ~QHttpNetworkConnectionPrivate + if (!qobject_cast(m_connection)) { + return; + } + + QAbstractSocket::SocketState socketState = m_socket->state(); + + // connection might be closed to signal the end of data + if (socketState == QAbstractSocket::UnconnectedState) { + if (m_socket->bytesAvailable() <= 0) { + if (m_reply->d_func()->state == QHttpNetworkReplyPrivate::ReadingDataState) { + // finish this reply. this case happens when the server did not send a content length + m_reply->d_func()->state = QHttpNetworkReplyPrivate::AllDoneState; + m_channel->allDone(); + return; + } else { + m_channel->handleUnexpectedEOF(); + return; + } + } else { + // socket not connected but still bytes for reading.. just continue in this function + } + } + + // read loop for the response + qint64 bytes = 0; + qint64 lastBytes = bytes; + do { + lastBytes = bytes; + + QHttpNetworkReplyPrivate::ReplyState state = m_reply->d_func()->state; + switch (state) { + case QHttpNetworkReplyPrivate::NothingDoneState: { + m_reply->d_func()->state = QHttpNetworkReplyPrivate::ReadingStatusState; + // fallthrough + } + case QHttpNetworkReplyPrivate::ReadingStatusState: { + qint64 statusBytes = m_reply->d_func()->readStatus(m_socket); + if (statusBytes == -1) { + // connection broke while reading status. also handled if later _q_disconnected is called + m_channel->handleUnexpectedEOF(); + return; + } + bytes += statusBytes; + m_channel->lastStatus = m_reply->d_func()->statusCode; + break; + } + case QHttpNetworkReplyPrivate::ReadingHeaderState: { + QHttpNetworkReplyPrivate *replyPrivate = m_reply->d_func(); + qint64 headerBytes = replyPrivate->readHeader(m_socket); + if (headerBytes == -1) { + // connection broke while reading headers. also handled if later _q_disconnected is called + m_channel->handleUnexpectedEOF(); + return; + } + bytes += headerBytes; + // If headers were parsed successfully now it is the ReadingDataState + if (replyPrivate->state == QHttpNetworkReplyPrivate::ReadingDataState) { + if (replyPrivate->isCompressed() && replyPrivate->autoDecompress) { + // remove the Content-Length from header + replyPrivate->removeAutoDecompressHeader(); + } else { + replyPrivate->autoDecompress = false; + } + if (replyPrivate->statusCode == 100) { + replyPrivate->clearHttpLayerInformation(); + replyPrivate->state = QHttpNetworkReplyPrivate::ReadingStatusState; + break; // ignore + } + if (replyPrivate->shouldEmitSignals()) + emit m_reply->headerChanged(); + // After headerChanged had been emitted + // we can suddenly have a replyPrivate->userProvidedDownloadBuffer + // this is handled in the ReadingDataState however + + if (!replyPrivate->expectContent()) { + replyPrivate->state = QHttpNetworkReplyPrivate::AllDoneState; + m_channel->allDone(); + break; + } + } + break; + } + case QHttpNetworkReplyPrivate::ReadingDataState: { + QHttpNetworkReplyPrivate *replyPrivate = m_reply->d_func(); + if (m_socket->state() == QAbstractSocket::ConnectedState && + replyPrivate->downstreamLimited && !replyPrivate->responseData.isEmpty() && replyPrivate->shouldEmitSignals()) { + // (only do the following when still connected, not when we have already been disconnected and there is still data) + // We already have some HTTP body data. We don't read more from the socket until + // this is fetched by QHttpNetworkAccessHttpBackend. If we would read more, + // we could not limit our read buffer usage. + // We only do this when shouldEmitSignals==true because our HTTP parsing + // always needs to parse the 401/407 replies. Therefore they don't really obey + // to the read buffer maximum size, but we don't care since they should be small. + return; + } + + if (replyPrivate->userProvidedDownloadBuffer) { + // the user provided a direct buffer where we should put all our data in. + // this only works when we can tell the user the content length and he/she can allocate + // the buffer in that size. + // note that this call will read only from the still buffered data + qint64 haveRead = replyPrivate->readBodyVeryFast(m_socket, replyPrivate->userProvidedDownloadBuffer + replyPrivate->totalProgress); + if (haveRead > 0) { + bytes += haveRead; + replyPrivate->totalProgress += haveRead; + // the user will get notified of it via progress signal + emit m_reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); + } else if (haveRead == 0) { + // Happens since this called in a loop. Currently no bytes available. + } else if (haveRead < 0) { + m_connection->d_func()->emitReplyError(m_socket, m_reply, QNetworkReply::RemoteHostClosedError); + break; + } + } else if (!replyPrivate->isChunked() && !replyPrivate->autoDecompress + && replyPrivate->bodyLength > 0) { + // bulk files like images should fulfill these properties and + // we can therefore save on memory copying + qint64 haveRead = replyPrivate->readBodyFast(m_socket, &replyPrivate->responseData); + bytes += haveRead; + replyPrivate->totalProgress += haveRead; + if (replyPrivate->shouldEmitSignals()) { + emit m_reply->readyRead(); + emit m_reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); + } + } + else + { + // use the traditional slower reading (for compressed encoding, chunked encoding, + // no content-length etc) + qint64 haveRead = replyPrivate->readBody(m_socket, &replyPrivate->responseData); + if (haveRead > 0) { + bytes += haveRead; + replyPrivate->totalProgress += haveRead; + if (replyPrivate->shouldEmitSignals()) { + emit m_reply->readyRead(); + emit m_reply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); + } + } else if (haveRead == -1) { + // Some error occurred + m_connection->d_func()->emitReplyError(m_socket, m_reply, QNetworkReply::ProtocolFailure); + break; + } + } + // still in ReadingDataState? This function will be called again by the socket's readyRead + if (replyPrivate->state == QHttpNetworkReplyPrivate::ReadingDataState) + break; + + // everything done, fall through + } + case QHttpNetworkReplyPrivate::AllDoneState: + m_channel->allDone(); + break; + default: + break; + } + } while (bytes != lastBytes && m_reply); +} + +void QHttpProtocolHandler::_q_readyRead() +{ + if (m_socket->state() == QAbstractSocket::ConnectedState && m_socket->bytesAvailable() == 0) { + // We got a readyRead but no bytes are available.. + // This happens for the Unbuffered QTcpSocket + // Also check if socket is in ConnectedState since + // this function may also be invoked via the event loop. + char c; + qint64 ret = m_socket->peek(&c, 1); + if (ret < 0) { + m_channel->_q_error(m_socket->error()); + // We still need to handle the reply so it emits its signals etc. + if (m_reply) + _q_receiveReply(); + return; + } + } + + if (m_channel->isSocketWaiting() || m_channel->isSocketReading()) { + m_channel->state = QHttpNetworkConnectionChannel::ReadingState; + if (m_reply) + _q_receiveReply(); + } +} + +bool QHttpProtocolHandler::sendRequest() +{ + m_reply = m_channel->reply; + + if (!m_reply) { + // heh, how should that happen! + qWarning() << "QAbstractProtocolHandler::sendRequest() called without QHttpNetworkReply"; + m_channel->state = QHttpNetworkConnectionChannel::IdleState; + return false; + } + + switch (m_channel->state) { + case QHttpNetworkConnectionChannel::IdleState: { // write the header + if (!m_channel->ensureConnection()) { + // wait for the connection (and encryption) to be done + // sendRequest will be called again from either + // _q_connected or _q_encrypted + return false; + } + QString scheme = m_channel->request.url().scheme(); + if (scheme == QLatin1String("preconnect-http") + || scheme == QLatin1String("preconnect-https")) { + m_channel->state = QHttpNetworkConnectionChannel::IdleState; + m_reply->d_func()->state = QHttpNetworkReplyPrivate::AllDoneState; + m_channel->allDone(); + m_connection->preConnectFinished(); // will only decrease the counter + m_reply = 0; // so we can reuse this channel + return true; // we have a working connection and are done + } + + m_channel->written = 0; // excluding the header + m_channel->bytesTotal = 0; + + QHttpNetworkReplyPrivate *replyPrivate = m_reply->d_func(); + replyPrivate->clear(); + replyPrivate->connection = m_connection; + replyPrivate->connectionChannel = m_channel; + replyPrivate->autoDecompress = m_channel->request.d->autoDecompress; + replyPrivate->pipeliningUsed = false; + + // if the url contains authentication parameters, use the new ones + // both channels will use the new authentication parameters + if (!m_channel->request.url().userInfo().isEmpty() && m_channel->request.withCredentials()) { + QUrl url = m_channel->request.url(); + QAuthenticator &auth = m_channel->authenticator; + if (url.userName() != auth.user() + || (!url.password().isEmpty() && url.password() != auth.password())) { + auth.setUser(url.userName()); + auth.setPassword(url.password()); + m_connection->d_func()->copyCredentials(m_connection->d_func()->indexOf(m_socket), &auth, false); + } + // clear the userinfo, since we use the same request for resending + // userinfo in url can conflict with the one in the authenticator + url.setUserInfo(QString()); + m_channel->request.setUrl(url); + } + // Will only be false if Qt WebKit is performing a cross-origin XMLHttpRequest + // and withCredentials has not been set to true. + if (m_channel->request.withCredentials()) + m_connection->d_func()->createAuthorization(m_socket, m_channel->request); +#ifndef QT_NO_NETWORKPROXY + QByteArray header = QHttpNetworkRequestPrivate::header(m_channel->request, + (m_connection->d_func()->networkProxy.type() != QNetworkProxy::NoProxy)); +#else + QByteArray header = QHttpNetworkRequestPrivate::header(m_channel->request, false); +#endif + m_socket->write(header); + // flushing is dangerous (QSslSocket calls transmit which might read or error) +// m_socket->flush(); + QNonContiguousByteDevice* uploadByteDevice = m_channel->request.uploadByteDevice(); + if (uploadByteDevice) { + // connect the signals so this function gets called again + QObject::connect(uploadByteDevice, SIGNAL(readyRead()), m_channel, SLOT(_q_uploadDataReadyRead())); + + m_channel->bytesTotal = m_channel->request.contentLength(); + + m_channel->state = QHttpNetworkConnectionChannel::WritingState; // start writing data + sendRequest(); //recurse + } else { + m_channel->state = QHttpNetworkConnectionChannel::WaitingState; // now wait for response + sendRequest(); //recurse + } + + break; + } + case QHttpNetworkConnectionChannel::WritingState: + { + // write the data + QNonContiguousByteDevice* uploadByteDevice = m_channel->request.uploadByteDevice(); + if (!uploadByteDevice || m_channel->bytesTotal == m_channel->written) { + if (uploadByteDevice) + emit m_reply->dataSendProgress(m_channel->written, m_channel->bytesTotal); + m_channel->state = QHttpNetworkConnectionChannel::WaitingState; // now wait for response + sendRequest(); // recurse + break; + } + + // only feed the QTcpSocket buffer when there is less than 32 kB in it + const qint64 socketBufferFill = 32*1024; + const qint64 socketWriteMaxSize = 16*1024; + + +#ifndef QT_NO_SSL + QSslSocket *sslSocket = qobject_cast(m_socket); + // if it is really an ssl socket, check more than just bytesToWrite() + while ((m_socket->bytesToWrite() + (sslSocket ? sslSocket->encryptedBytesToWrite() : 0)) + <= socketBufferFill && m_channel->bytesTotal != m_channel->written) +#else + while (m_socket->bytesToWrite() <= socketBufferFill + && m_channel->bytesTotal != m_channel->written) +#endif + { + // get pointer to upload data + qint64 currentReadSize = 0; + qint64 desiredReadSize = qMin(socketWriteMaxSize, m_channel->bytesTotal - m_channel->written); + const char *readPointer = uploadByteDevice->readPointer(desiredReadSize, currentReadSize); + + if (currentReadSize == -1) { + // premature eof happened + m_connection->d_func()->emitReplyError(m_socket, m_reply, QNetworkReply::UnknownNetworkError); + return false; + } else if (readPointer == 0 || currentReadSize == 0) { + // nothing to read currently, break the loop + break; + } else { + qint64 currentWriteSize = m_socket->write(readPointer, currentReadSize); + if (currentWriteSize == -1 || currentWriteSize != currentReadSize) { + // socket broke down + m_connection->d_func()->emitReplyError(m_socket, m_reply, QNetworkReply::UnknownNetworkError); + return false; + } else { + m_channel->written += currentWriteSize; + uploadByteDevice->advanceReadPointer(currentWriteSize); + + emit m_reply->dataSendProgress(m_channel->written, m_channel->bytesTotal); + + if (m_channel->written == m_channel->bytesTotal) { + // make sure this function is called once again + m_channel->state = QHttpNetworkConnectionChannel::WaitingState; + sendRequest(); + break; + } + } + } + } + break; + } + + case QHttpNetworkConnectionChannel::WaitingState: + { + QNonContiguousByteDevice* uploadByteDevice = m_channel->request.uploadByteDevice(); + if (uploadByteDevice) { + QObject::disconnect(uploadByteDevice, SIGNAL(readyRead()), m_channel, SLOT(_q_uploadDataReadyRead())); + } + + // HTTP pipelining + //m_connection->d_func()->fillPipeline(m_socket); + //m_socket->flush(); + + // ensure we try to receive a reply in all cases, even if _q_readyRead_ hat not been called + // this is needed if the sends an reply before we have finished sending the request. In that + // case receiveReply had been called before but ignored the server reply + if (m_socket->bytesAvailable()) + QMetaObject::invokeMethod(m_channel, "_q_receiveReply", Qt::QueuedConnection); + break; + } + case QHttpNetworkConnectionChannel::ReadingState: + // ignore _q_bytesWritten in these states + // fall through + default: + break; + } + return true; +} + +QT_END_NAMESPACE + +#endif // QT_NO_HTTP diff --git a/src/network/access/qhttpprotocolhandler_p.h b/src/network/access/qhttpprotocolhandler_p.h new file mode 100644 index 0000000000..2bbc044b6c --- /dev/null +++ b/src/network/access/qhttpprotocolhandler_p.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 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, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QHTTPPROTOCOLHANDLER_H +#define QHTTPPROTOCOLHANDLER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include + +#ifndef QT_NO_HTTP + +QT_BEGIN_NAMESPACE + +class QHttpProtocolHandler : public QAbstractProtocolHandler { +public: + QHttpProtocolHandler(QHttpNetworkConnectionChannel *channel); + +private: + virtual void _q_receiveReply() Q_DECL_OVERRIDE; + virtual void _q_readyRead() Q_DECL_OVERRIDE; + virtual bool sendRequest() Q_DECL_OVERRIDE; +}; + +QT_END_NAMESPACE + +#endif // QT_NO_HTTP + +#endif -- cgit v1.2.3