diff options
23 files changed, 2888 insertions, 86 deletions
diff --git a/src/network/access/access.pri b/src/network/access/access.pri index 3017417da8..e829d52cbe 100644 --- a/src/network/access/access.pri +++ b/src/network/access/access.pri @@ -9,6 +9,7 @@ HEADERS += \ access/qhttpnetworkconnectionchannel_p.h \ access/qabstractprotocolhandler_p.h \ access/qhttpprotocolhandler_p.h \ + access/qspdyprotocolhandler_p.h \ access/qnetworkaccessauthenticationmanager_p.h \ access/qnetworkaccessmanager.h \ access/qnetworkaccessmanager_p.h \ @@ -47,6 +48,7 @@ SOURCES += \ access/qhttpnetworkconnectionchannel.cpp \ access/qabstractprotocolhandler.cpp \ access/qhttpprotocolhandler.cpp \ + access/qspdyprotocolhandler.cpp \ access/qnetworkaccessauthenticationmanager.cpp \ access/qnetworkaccessmanager.cpp \ access/qnetworkaccesscache.cpp \ diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 66c97f7485..6d568220e2 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -68,7 +68,7 @@ QT_BEGIN_NAMESPACE -const int QHttpNetworkConnectionPrivate::defaultChannelCount = 6; +const int QHttpNetworkConnectionPrivate::defaultHttpChannelCount = 6; // The pipeline length. So there will be 4 requests in flight. const int QHttpNetworkConnectionPrivate::defaultPipelineLength = 3; @@ -77,20 +77,29 @@ const int QHttpNetworkConnectionPrivate::defaultPipelineLength = 3; const int QHttpNetworkConnectionPrivate::defaultRePipelineLength = 2; -QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(const QString &hostName, quint16 port, bool encrypt) +QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(const QString &hostName, + quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType type) : state(RunningState), networkLayerState(Unknown), - hostName(hostName), port(port), encrypt(encrypt), delayIpv4(true), - channelCount(defaultChannelCount) + hostName(hostName), port(port), encrypt(encrypt), delayIpv4(true) +#ifndef QT_NO_SSL +, channelCount((type == QHttpNetworkConnection::ConnectionTypeSPDY) ? 1 : defaultHttpChannelCount) +#else +, channelCount(defaultHttpChannelCount) +#endif // QT_NO_SSL #ifndef QT_NO_NETWORKPROXY , networkProxy(QNetworkProxy::NoProxy) #endif , preConnectRequests(0) + , connectionType(type) { channels = new QHttpNetworkConnectionChannel[channelCount]; } -QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(quint16 channelCount, const QString &hostName, quint16 port, bool encrypt) +QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(quint16 channelCount, const QString &hostName, + quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType type) : state(RunningState), networkLayerState(Unknown), hostName(hostName), port(port), encrypt(encrypt), delayIpv4(true), channelCount(channelCount) @@ -98,6 +107,7 @@ QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(quint16 channelCoun , networkProxy(QNetworkProxy::NoProxy) #endif , preConnectRequests(0) + , connectionType(type) { channels = new QHttpNetworkConnectionChannel[channelCount]; } @@ -546,15 +556,24 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor if (request.isPreConnect()) preConnectRequests++; - switch (request.priority()) { - case QHttpNetworkRequest::HighPriority: - highPriorityQueue.prepend(pair); - break; - case QHttpNetworkRequest::NormalPriority: - case QHttpNetworkRequest::LowPriority: - lowPriorityQueue.prepend(pair); - break; + if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP) { + switch (request.priority()) { + case QHttpNetworkRequest::HighPriority: + highPriorityQueue.prepend(pair); + break; + case QHttpNetworkRequest::NormalPriority: + case QHttpNetworkRequest::LowPriority: + lowPriorityQueue.prepend(pair); + break; + } } +#ifndef QT_NO_SSL + else { // SPDY + if (!pair.second->d_func()->requestIsPrepared) + prepareRequest(pair); + channels[0].spdyRequestsToSend.insertMulti(request.priority(), pair); + } +#endif // QT_NO_SSL // For Happy Eyeballs the networkLayerState is set to Unknown // untill we have started the first connection attempt. So no @@ -900,17 +919,39 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() // dequeue new ones - // return fast if there is nothing to do - if (highPriorityQueue.isEmpty() && lowPriorityQueue.isEmpty()) - return; - // try to get a free AND connected socket - for (int i = 0; i < channelCount; ++i) { - if (channels[i].socket) { - if (!channels[i].reply && !channels[i].isSocketBusy() && channels[i].socket->state() == QAbstractSocket::ConnectedState) { - if (dequeueRequest(channels[i].socket)) - channels[i].sendRequest(); + switch (connectionType) { + case QHttpNetworkConnection::ConnectionTypeHTTP: { + // return fast if there is nothing to do + if (highPriorityQueue.isEmpty() && lowPriorityQueue.isEmpty()) + return; + + // try to get a free AND connected socket + for (int i = 0; i < channelCount; ++i) { + if (channels[i].socket) { + if (!channels[i].reply && !channels[i].isSocketBusy() && channels[i].socket->state() == QAbstractSocket::ConnectedState) { + if (dequeueRequest(channels[i].socket)) + channels[i].sendRequest(); + } } } + break; + } + case QHttpNetworkConnection::ConnectionTypeSPDY: { +#ifndef QT_NO_SSL + if (channels[0].spdyRequestsToSend.isEmpty()) + return; + + if (networkLayerState == IPv4) + channels[0].networkLayerPreference = QAbstractSocket::IPv4Protocol; + else if (networkLayerState == IPv6) + channels[0].networkLayerPreference = QAbstractSocket::IPv6Protocol; + channels[0].ensureConnection(); + if (channels[0].socket && channels[0].socket->state() == QAbstractSocket::ConnectedState + && !channels[0].pendingEncrypt) + channels[0].sendRequest(); +#endif // QT_NO_SSL + break; + } } // try to push more into all sockets @@ -1059,7 +1100,19 @@ void QHttpNetworkConnectionPrivate::_q_hostLookupFinished(QHostInfo info) if (dequeueRequest(channels[0].socket)) { emitReplyError(channels[0].socket, channels[0].reply, QNetworkReply::HostNotFoundError); networkLayerState = QHttpNetworkConnectionPrivate::Unknown; - } else { + } +#ifndef QT_NO_SSL + else if (connectionType == QHttpNetworkConnection::ConnectionTypeSPDY) { + QList<HttpMessagePair> spdyPairs = channels[0].spdyRequestsToSend.values(); + for (int a = 0; a < spdyPairs.count(); ++a) { + // emit error for all replies + QHttpNetworkReply *currentReply = spdyPairs.at(a).second; + Q_ASSERT(currentReply); + emitReplyError(channels[0].socket, currentReply, QNetworkReply::HostNotFoundError); + } + } +#endif // QT_NO_SSL + else { // Should not happen qWarning() << "QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not dequeu request"; networkLayerState = QHttpNetworkConnectionPrivate::Unknown; @@ -1127,31 +1180,41 @@ void QHttpNetworkConnectionPrivate::_q_connectDelayedChannel() } #ifndef QT_NO_BEARERMANAGEMENT -QHttpNetworkConnection::QHttpNetworkConnection(const QString &hostName, quint16 port, bool encrypt, QObject *parent, QSharedPointer<QNetworkSession> networkSession) - : QObject(*(new QHttpNetworkConnectionPrivate(hostName, port, encrypt)), parent) +QHttpNetworkConnection::QHttpNetworkConnection(const QString &hostName, quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType connectionType, + QObject *parent, QSharedPointer<QNetworkSession> networkSession) + : QObject(*(new QHttpNetworkConnectionPrivate(hostName, port, encrypt, connectionType)), parent) { Q_D(QHttpNetworkConnection); d->networkSession = networkSession; d->init(); } -QHttpNetworkConnection::QHttpNetworkConnection(quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt, QObject *parent, QSharedPointer<QNetworkSession> networkSession) - : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt)), parent) +QHttpNetworkConnection::QHttpNetworkConnection(quint16 connectionCount, const QString &hostName, + quint16 port, bool encrypt, QObject *parent, + QSharedPointer<QNetworkSession> networkSession, + QHttpNetworkConnection::ConnectionType connectionType) + : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt, + connectionType)), parent) { Q_D(QHttpNetworkConnection); d->networkSession = networkSession; d->init(); } #else -QHttpNetworkConnection::QHttpNetworkConnection(const QString &hostName, quint16 port, bool encrypt, QObject *parent) - : QObject(*(new QHttpNetworkConnectionPrivate(hostName, port, encrypt)), parent) +QHttpNetworkConnection::QHttpNetworkConnection(const QString &hostName, quint16 port, bool encrypt, QObject *parent, + QHttpNetworkConnection::ConnectionType connectionType) + : QObject(*(new QHttpNetworkConnectionPrivate(hostName, port, encrypt , connectionType)), parent) { Q_D(QHttpNetworkConnection); d->init(); } -QHttpNetworkConnection::QHttpNetworkConnection(quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt, QObject *parent) - : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt)), parent) +QHttpNetworkConnection::QHttpNetworkConnection(quint16 connectionCount, const QString &hostName, + quint16 port, bool encrypt, QObject *parent, + QHttpNetworkConnection::ConnectionType connectionType) + : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt, + connectionType)), parent) { Q_D(QHttpNetworkConnection); d->init(); @@ -1225,6 +1288,17 @@ QNetworkProxy QHttpNetworkConnection::transparentProxy() const } #endif +QHttpNetworkConnection::ConnectionType QHttpNetworkConnection::connectionType() +{ + Q_D(QHttpNetworkConnection); + return d->connectionType; +} + +void QHttpNetworkConnection::setConnectionType(ConnectionType type) +{ + Q_D(QHttpNetworkConnection); + d->connectionType = type; +} // SSL support below #ifndef QT_NO_SSL @@ -1299,7 +1373,23 @@ void QHttpNetworkConnectionPrivate::emitProxyAuthenticationRequired(const QHttpN // Also pause the connection because socket notifiers may fire while an user // dialog is displaying pauseConnection(); - emit chan->reply->proxyAuthenticationRequired(proxy, auth); + QHttpNetworkReply *reply; +#ifndef QT_NO_SSL + if (connectionType == QHttpNetworkConnection::ConnectionTypeSPDY) { + // we choose the reply to emit the proxyAuth signal from somewhat arbitrarily, + // but that does not matter because the signal will ultimately be emitted + // by the QNetworkAccessManager. + Q_ASSERT(chan->spdyRequestsToSend.count() > 0); + reply = chan->spdyRequestsToSend.values().first().second; + } else { // HTTP +#endif // QT_NO_SSL + reply = chan->reply; +#ifndef QT_NO_SSL + } +#endif // QT_NO_SSL + + Q_ASSERT(reply); + emit reply->proxyAuthenticationRequired(proxy, auth); resumeConnection(); int i = indexOf(chan->socket); copyCredentials(i, auth, true); diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index 526326c3fd..9d4257e217 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -94,12 +94,27 @@ class Q_AUTOTEST_EXPORT QHttpNetworkConnection : public QObject Q_OBJECT public: + enum ConnectionType { + ConnectionTypeHTTP, + ConnectionTypeSPDY + }; + #ifndef QT_NO_BEARERMANAGEMENT - explicit QHttpNetworkConnection(const QString &hostName, quint16 port = 80, bool encrypt = false, QObject *parent = 0, QSharedPointer<QNetworkSession> networkSession = QSharedPointer<QNetworkSession>()); - QHttpNetworkConnection(quint16 channelCount, const QString &hostName, quint16 port = 80, bool encrypt = false, QObject *parent = 0, QSharedPointer<QNetworkSession> networkSession = QSharedPointer<QNetworkSession>()); + explicit QHttpNetworkConnection(const QString &hostName, quint16 port = 80, bool encrypt = false, + ConnectionType connectionType = ConnectionTypeHTTP, + QObject *parent = 0, QSharedPointer<QNetworkSession> networkSession + = QSharedPointer<QNetworkSession>()); + QHttpNetworkConnection(quint16 channelCount, const QString &hostName, quint16 port = 80, + bool encrypt = false, QObject *parent = 0, + QSharedPointer<QNetworkSession> networkSession = QSharedPointer<QNetworkSession>(), + ConnectionType connectionType = ConnectionTypeHTTP); #else - explicit QHttpNetworkConnection(const QString &hostName, quint16 port = 80, bool encrypt = false, QObject *parent = 0); - QHttpNetworkConnection(quint16 channelCount, const QString &hostName, quint16 port = 80, bool encrypt = false, QObject *parent = 0); + explicit QHttpNetworkConnection(const QString &hostName, quint16 port = 80, bool encrypt = false, + QObject *parent = 0, + ConnectionType connectionType = ConnectionTypeHTTP); + QHttpNetworkConnection(quint16 channelCount, const QString &hostName, quint16 port = 80, + bool encrypt = false, QObject *parent = 0, + ConnectionType connectionType = ConnectionTypeHTTP); #endif ~QHttpNetworkConnection(); @@ -123,6 +138,9 @@ public: QHttpNetworkConnectionChannel *channels() const; + ConnectionType connectionType(); + void setConnectionType(ConnectionType type); + #ifndef QT_NO_SSL void setSslConfiguration(const QSslConfiguration &config); void ignoreSslErrors(int channel = -1); @@ -140,6 +158,7 @@ private: friend class QHttpNetworkReplyPrivate; friend class QHttpNetworkConnectionChannel; friend class QHttpProtocolHandler; + friend class QSpdyProtocolHandler; Q_PRIVATE_SLOT(d_func(), void _q_startNextRequest()) Q_PRIVATE_SLOT(d_func(), void _q_hostLookupFinished(QHostInfo)) @@ -155,7 +174,7 @@ class QHttpNetworkConnectionPrivate : public QObjectPrivate { Q_DECLARE_PUBLIC(QHttpNetworkConnection) public: - static const int defaultChannelCount; + static const int defaultHttpChannelCount; static const int defaultPipelineLength; static const int defaultRePipelineLength; @@ -172,8 +191,10 @@ public: IPv4or6 }; - QHttpNetworkConnectionPrivate(const QString &hostName, quint16 port, bool encrypt); - QHttpNetworkConnectionPrivate(quint16 channelCount, const QString &hostName, quint16 port, bool encrypt); + QHttpNetworkConnectionPrivate(const QString &hostName, quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType type); + QHttpNetworkConnectionPrivate(quint16 channelCount, const QString &hostName, quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType type); ~QHttpNetworkConnectionPrivate(); void init(); @@ -245,6 +266,8 @@ public: int preConnectRequests; + QHttpNetworkConnection::ConnectionType connectionType; + #ifndef QT_NO_SSL QSharedPointer<QSslContext> sslContext; #endif diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 6f06c18732..fb40958178 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -50,6 +50,7 @@ #ifndef QT_NO_HTTP #include <private/qhttpprotocolhandler_p.h> +#include <private/qspdyprotocolhandler_p.h> #ifndef QT_NO_SSL # include <QtNetwork/qsslkey.h> @@ -166,9 +167,12 @@ void QHttpNetworkConnectionChannel::init() if (!sslConfiguration.isNull()) sslSocket->setSslConfiguration(sslConfiguration); + } else { +#endif // QT_NO_SSL + protocolHandler.reset(new QHttpProtocolHandler(this)); +#ifndef QT_NO_SSL } #endif - protocolHandler.reset(new QHttpProtocolHandler(this)); #ifndef QT_NO_NETWORKPROXY if (proxy.type() != QNetworkProxy::NoProxy) @@ -879,6 +883,18 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket if (protocolHandler) protocolHandler->setReply(0); } +#ifndef QT_NO_SSL + else if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { + QList<HttpMessagePair> spdyPairs = spdyRequestsToSend.values(); + for (int a = 0; a < spdyPairs.count(); ++a) { + // emit error for all replies + QHttpNetworkReply *currentReply = spdyPairs.at(a).second; + Q_ASSERT(currentReply); + emit currentReply->finishedWithError(errorCode, errorString); + } + } +#endif // QT_NO_SSL + // send the next request QMetaObject::invokeMethod(that, "_q_startNextRequest", Qt::QueuedConnection); @@ -889,11 +905,19 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket #ifndef QT_NO_NETWORKPROXY void QHttpNetworkConnectionChannel::_q_proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator* auth) { - // Need to dequeue the request before we can emit the error. - if (!reply) - connection->d_func()->dequeueRequest(socket); - if (reply) +#ifndef QT_NO_SSL + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { connection->d_func()->emitProxyAuthenticationRequired(this, proxy, auth); + } else { // HTTP +#endif // QT_NO_SSL + // Need to dequeue the request before we can emit the error. + if (!reply) + connection->d_func()->dequeueRequest(socket); + if (reply) + connection->d_func()->emitProxyAuthenticationRequired(this, proxy, auth); +#ifndef QT_NO_SSL + } +#endif // QT_NO_SSL } #endif @@ -905,16 +929,85 @@ void QHttpNetworkConnectionChannel::_q_uploadDataReadyRead() #ifndef QT_NO_SSL void QHttpNetworkConnectionChannel::_q_encrypted() { + QSslSocket *sslSocket = qobject_cast<QSslSocket *>(socket); + Q_ASSERT(sslSocket); + + if (!protocolHandler) { + switch (sslSocket->sslConfiguration().nextProtocolNegotiationStatus()) { + case QSslConfiguration::NextProtocolNegotiationNegotiated: { + QByteArray nextProtocol = sslSocket->sslConfiguration().nextNegotiatedProtocol(); + if (nextProtocol == QSslConfiguration::NextProtocolHttp1_1) { + // fall through to create a QHttpProtocolHandler + } else if (nextProtocol == QSslConfiguration::NextProtocolSpdy3_0) { + protocolHandler.reset(new QSpdyProtocolHandler(this)); + connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeSPDY); + // no need to re-queue requests, if SPDY was enabled on the request it + // has gone to the SPDY queue already + break; + } else { + emitFinishedWithError(QNetworkReply::SslHandshakeFailedError, + "detected unknown Next Protocol Negotiation protocol"); + break; + } + } + case QSslConfiguration::NextProtocolNegotiationNone: + protocolHandler.reset(new QHttpProtocolHandler(this)); + connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP); + // re-queue requests from SPDY queue to HTTP queue, if any + requeueSpdyRequests(); + break; + case QSslConfiguration::NextProtocolNegotiationUnsupported: + emitFinishedWithError(QNetworkReply::SslHandshakeFailedError, + "chosen Next Protocol Negotiation value unsupported"); + break; + default: + emitFinishedWithError(QNetworkReply::SslHandshakeFailedError, + "detected unknown Next Protocol Negotiation protocol"); + } + } + if (!socket) return; // ### error state = QHttpNetworkConnectionChannel::IdleState; pendingEncrypt = false; - if (!reply) - connection->d_func()->dequeueRequest(socket); - if (reply) - emit reply->encrypted(); + + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeSPDY) { + // we call setSpdyWasUsed(true) on the replies in the SPDY handler when the request is sent + if (spdyRequestsToSend.count() > 0) + // wait for data from the server first (e.g. initial window, max concurrent requests) + QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection); + } else { // HTTP + if (!reply) + connection->d_func()->dequeueRequest(socket); + if (reply) { + reply->setSpdyWasUsed(false); + emit reply->encrypted(); + } + if (reply) + sendRequest(); + } +} + +void QHttpNetworkConnectionChannel::requeueSpdyRequests() +{ + QList<HttpMessagePair> spdyPairs = spdyRequestsToSend.values(); + for (int a = 0; a < spdyPairs.count(); ++a) { + connection->d_func()->requeueRequest(spdyPairs.at(a)); + } + spdyRequestsToSend.clear(); +} + +void QHttpNetworkConnectionChannel::emitFinishedWithError(QNetworkReply::NetworkError error, + const char *message) +{ if (reply) - sendRequest(); + emit reply->finishedWithError(error, QHttpNetworkConnectionChannel::tr(message)); + QList<HttpMessagePair> spdyPairs = spdyRequestsToSend.values(); + for (int a = 0; a < spdyPairs.count(); ++a) { + QHttpNetworkReply *currentReply = spdyPairs.at(a).second; + Q_ASSERT(currentReply); + emit currentReply->finishedWithError(error, QHttpNetworkConnectionChannel::tr(message)); + } } void QHttpNetworkConnectionChannel::_q_sslErrors(const QList<QSslError> &errors) @@ -927,8 +1020,21 @@ void QHttpNetworkConnectionChannel::_q_sslErrors(const QList<QSslError> &errors) connection->d_func()->pauseConnection(); if (pendingEncrypt && !reply) connection->d_func()->dequeueRequest(socket); - if (reply) - emit reply->sslErrors(errors); + if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP) { + if (reply) + emit reply->sslErrors(errors); + } +#ifndef QT_NO_SSL + else { // SPDY + QList<HttpMessagePair> spdyPairs = spdyRequestsToSend.values(); + for (int a = 0; a < spdyPairs.count(); ++a) { + // emit SSL errors for all replies + QHttpNetworkReply *currentReply = spdyPairs.at(a).second; + Q_ASSERT(currentReply); + emit currentReply->sslErrors(errors); + } + } +#endif // QT_NO_SSL connection->d_func()->resumeConnection(); } diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h index 7230eb2543..450b6fc419 100644 --- a/src/network/access/qhttpnetworkconnectionchannel_p.h +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h @@ -104,8 +104,8 @@ public: bool ssl; bool isInitialized; ChannelState state; - QHttpNetworkRequest request; // current request - QHttpNetworkReply *reply; // current reply for this request + QHttpNetworkRequest request; // current request, only used for HTTP + QHttpNetworkReply *reply; // current reply for this request, only used for HTTP qint64 written; qint64 bytesTotal; bool resendCurrent; @@ -123,9 +123,13 @@ public: bool ignoreAllSslErrors; QList<QSslError> ignoreSslErrorsList; QSslConfiguration sslConfiguration; + QMultiMap<int, HttpMessagePair> spdyRequestsToSend; // sorted by priority void ignoreSslErrors(); void ignoreSslErrors(const QList<QSslError> &errors); void setSslConfiguration(const QSslConfiguration &config); + void requeueSpdyRequests(); // when we wanted SPDY but got HTTP + // to emit the signal for all in-flight replies: + void emitFinishedWithError(QNetworkReply::NetworkError error, const char *message); #endif #ifndef QT_NO_BEARERMANAGEMENT QSharedPointer<QNetworkSession> networkSession; diff --git a/src/network/access/qhttpnetworkreply.cpp b/src/network/access/qhttpnetworkreply.cpp index 1b9e1f5a53..2a7e6ed638 100644 --- a/src/network/access/qhttpnetworkreply.cpp +++ b/src/network/access/qhttpnetworkreply.cpp @@ -265,6 +265,16 @@ bool QHttpNetworkReply::isPipeliningUsed() const return d_func()->pipeliningUsed; } +bool QHttpNetworkReply::isSpdyUsed() const +{ + return d_func()->spdyUsed; +} + +void QHttpNetworkReply::setSpdyWasUsed(bool spdy) +{ + d_func()->spdyUsed = spdy; +} + QHttpNetworkConnection* QHttpNetworkReply::connection() { return d_func()->connection; @@ -281,9 +291,15 @@ QHttpNetworkReplyPrivate::QHttpNetworkReplyPrivate(const QUrl &newUrl) connectionCloseEnabled(true), forceConnectionCloseEnabled(false), lastChunkRead(false), - currentChunkSize(0), currentChunkRead(0), readBufferMaxSize(0), connection(0), + currentChunkSize(0), currentChunkRead(0), readBufferMaxSize(0), + windowSizeDownload(65536), // 64K initial window size according to SPDY standard + windowSizeUpload(65536), // 64K initial window size according to SPDY standard + currentlyReceivedDataInWindow(0), + currentlyUploadedDataInWindow(0), + totallyUploadedData(0), + connection(0), autoDecompress(false), responseData(), requestIsPrepared(false) - ,pipeliningUsed(false), downstreamLimited(false) + ,pipeliningUsed(false), spdyUsed(false), downstreamLimited(false) ,userProvidedDownloadBuffer(0) #ifndef QT_NO_COMPRESS ,inflateStrm(0) @@ -550,15 +566,7 @@ qint64 QHttpNetworkReplyPrivate::readHeader(QAbstractSocket *socket) // allocate inflate state if (!inflateStrm) inflateStrm = new z_stream; - inflateStrm->zalloc = Z_NULL; - inflateStrm->zfree = Z_NULL; - inflateStrm->opaque = Z_NULL; - inflateStrm->avail_in = 0; - inflateStrm->next_in = Z_NULL; - // "windowBits can also be greater than 15 for optional gzip decoding. - // Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" - // http://www.zlib.net/manual.html - int ret = inflateInit2(inflateStrm, MAX_WBITS+32); + int ret = initializeInflateStream(); if (ret != Z_OK) return -1; } @@ -703,8 +711,28 @@ qint64 QHttpNetworkReplyPrivate::readBody(QAbstractSocket *socket, QByteDataBuff } #ifndef QT_NO_COMPRESS +int QHttpNetworkReplyPrivate::initializeInflateStream() +{ + inflateStrm->zalloc = Z_NULL; + inflateStrm->zfree = Z_NULL; + inflateStrm->opaque = Z_NULL; + inflateStrm->avail_in = 0; + inflateStrm->next_in = Z_NULL; + // "windowBits can also be greater than 15 for optional gzip decoding. + // Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" + // http://www.zlib.net/manual.html + int ret = inflateInit2(inflateStrm, MAX_WBITS+32); + Q_ASSERT(ret == Z_OK); + return ret; +} + qint64 QHttpNetworkReplyPrivate::uncompressBodyData(QByteDataBuffer *in, QByteDataBuffer *out) { + if (!inflateStrm) { // happens when called from the SPDY protocol handler + inflateStrm = new z_stream; + initializeInflateStream(); + } + if (!inflateStrm) return -1; diff --git a/src/network/access/qhttpnetworkreply_p.h b/src/network/access/qhttpnetworkreply_p.h index 583c3e426f..e15ace0072 100644 --- a/src/network/access/qhttpnetworkreply_p.h +++ b/src/network/access/qhttpnetworkreply_p.h @@ -132,6 +132,8 @@ public: bool isFinished() const; bool isPipeliningUsed() const; + bool isSpdyUsed() const; + void setSpdyWasUsed(bool spdy); QHttpNetworkConnection* connection(); @@ -165,6 +167,7 @@ private: friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; friend class QHttpProtocolHandler; + friend class QSpdyProtocolHandler; }; @@ -205,7 +208,11 @@ public: ReadingStatusState, ReadingHeaderState, ReadingDataState, - AllDoneState + AllDoneState, + SPDYSYNSent, + SPDYUploading, + SPDYHalfClosed, + SPDYClosed } state; QHttpNetworkRequest request; @@ -226,6 +233,11 @@ public: qint64 currentChunkSize; qint64 currentChunkRead; qint64 readBufferMaxSize; + qint32 windowSizeDownload; // only for SPDY + qint32 windowSizeUpload; // only for SPDY + qint32 currentlyReceivedDataInWindow; // only for SPDY + qint32 currentlyUploadedDataInWindow; // only for SPDY + qint64 totallyUploadedData; // only for SPDY QPointer<QHttpNetworkConnection> connection; QPointer<QHttpNetworkConnectionChannel> connectionChannel; @@ -236,12 +248,14 @@ public: bool requestIsPrepared; bool pipeliningUsed; + bool spdyUsed; bool downstreamLimited; char* userProvidedDownloadBuffer; #ifndef QT_NO_COMPRESS z_stream_s *inflateStrm; + int initializeInflateStream(); qint64 uncompressBodyData(QByteDataBuffer *in, QByteDataBuffer *out); #endif }; diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 3786f9b992..6e9f1216c3 100644 --- a/src/network/access/qhttpnetworkrequest.cpp +++ b/src/network/access/qhttpnetworkrequest.cpp @@ -49,8 +49,8 @@ QT_BEGIN_NAMESPACE QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(QHttpNetworkRequest::Operation op, QHttpNetworkRequest::Priority pri, const QUrl &newUrl) : QHttpNetworkHeaderPrivate(newUrl), operation(op), priority(pri), uploadByteDevice(0), - autoDecompress(false), pipeliningAllowed(false), withCredentials(true), - preConnect(false) + autoDecompress(false), pipeliningAllowed(false), spdyAllowed(false), + withCredentials(true), preConnect(false) { } @@ -62,6 +62,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest uploadByteDevice = other.uploadByteDevice; autoDecompress = other.autoDecompress; pipeliningAllowed = other.pipeliningAllowed; + spdyAllowed = other.spdyAllowed; customVerb = other.customVerb; withCredentials = other.withCredentials; ssl = other.ssl; @@ -80,6 +81,7 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot && (uploadByteDevice == other.uploadByteDevice) && (autoDecompress == other.autoDecompress) && (pipeliningAllowed == other.pipeliningAllowed) + && (spdyAllowed == other.spdyAllowed) // we do not clear the customVerb in setOperation && (operation != QHttpNetworkRequest::Custom || (customVerb == other.customVerb)) && (withCredentials == other.withCredentials) @@ -299,6 +301,16 @@ void QHttpNetworkRequest::setPipeliningAllowed(bool b) d->pipeliningAllowed = b; } +bool QHttpNetworkRequest::isSPDYAllowed() const +{ + return d->spdyAllowed; +} + +void QHttpNetworkRequest::setSPDYAllowed(bool b) +{ + d->spdyAllowed = b; +} + bool QHttpNetworkRequest::withCredentials() const { return d->withCredentials; diff --git a/src/network/access/qhttpnetworkrequest_p.h b/src/network/access/qhttpnetworkrequest_p.h index f224f7329d..09847d715c 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -114,6 +114,9 @@ public: bool isPipeliningAllowed() const; void setPipeliningAllowed(bool b); + bool isSPDYAllowed() const; + void setSPDYAllowed(bool b); + bool withCredentials() const; void setWithCredentials(bool b); @@ -135,6 +138,7 @@ private: friend class QHttpNetworkConnectionPrivate; friend class QHttpNetworkConnectionChannel; friend class QHttpProtocolHandler; + friend class QSpdyProtocolHandler; }; class QHttpNetworkRequestPrivate : public QHttpNetworkHeaderPrivate @@ -154,6 +158,7 @@ public: mutable QNonContiguousByteDevice* uploadByteDevice; bool autoDecompress; bool pipeliningAllowed; + bool spdyAllowed; bool withCredentials; bool ssl; bool preConnect; diff --git a/src/network/access/qhttpthreaddelegate.cpp b/src/network/access/qhttpthreaddelegate.cpp index b7e8bb3f72..e03dcb8ead 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -180,11 +180,15 @@ class QNetworkAccessCachedHttpConnection: public QHttpNetworkConnection, // Q_OBJECT public: #ifdef QT_NO_BEARERMANAGEMENT - QNetworkAccessCachedHttpConnection(const QString &hostName, quint16 port, bool encrypt) - : QHttpNetworkConnection(hostName, port, encrypt) + QNetworkAccessCachedHttpConnection(const QString &hostName, quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType connectionType) + : QHttpNetworkConnection(hostName, port, encrypt, connectionType) #else - QNetworkAccessCachedHttpConnection(const QString &hostName, quint16 port, bool encrypt, QSharedPointer<QNetworkSession> networkSession) - : QHttpNetworkConnection(hostName, port, encrypt, /*parent=*/0, networkSession) + QNetworkAccessCachedHttpConnection(const QString &hostName, quint16 port, bool encrypt, + QHttpNetworkConnection::ConnectionType connectionType, + QSharedPointer<QNetworkSession> networkSession) + : QHttpNetworkConnection(hostName, port, encrypt, connectionType, /*parent=*/0, + networkSession) #endif { setExpires(true); @@ -230,6 +234,7 @@ QHttpThreadDelegate::QHttpThreadDelegate(QObject *parent) : , synchronous(false) , incomingStatusCode(0) , isPipeliningUsed(false) + , isSpdyUsed(false) , incomingContentLength(-1) , incomingErrorCode(QNetworkReply::NoError) , downloadBuffer(0) @@ -281,6 +286,19 @@ void QHttpThreadDelegate::startRequest() QUrl urlCopy = httpRequest.url(); urlCopy.setPort(urlCopy.port(ssl ? 443 : 80)); + QHttpNetworkConnection::ConnectionType connectionType + = QHttpNetworkConnection::ConnectionTypeHTTP; +#ifndef QT_NO_SSL + if (httpRequest.isSPDYAllowed() && ssl) { + connectionType = QHttpNetworkConnection::ConnectionTypeSPDY; + urlCopy.setScheme(QStringLiteral("spdy")); // to differentiate SPDY requests from HTTPS requests + QList<QByteArray> nextProtocols; + nextProtocols << QSslConfiguration::NextProtocolSpdy3_0 + << QSslConfiguration::NextProtocolHttp1_1; + incomingSslConfiguration.setAllowedNextProtocols(nextProtocols); + } +#endif // QT_NO_SSL + #ifndef QT_NO_NETWORKPROXY if (transparentProxy.type() != QNetworkProxy::NoProxy) cacheKey = makeCacheKey(urlCopy, &transparentProxy); @@ -297,9 +315,12 @@ void QHttpThreadDelegate::startRequest() // no entry in cache; create an object // the http object is actually a QHttpNetworkConnection #ifdef QT_NO_BEARERMANAGEMENT - httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl); + httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl, + connectionType); #else - httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl, networkSession); + httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl, + connectionType, + networkSession); #endif #ifndef QT_NO_SSL // Set the QSslConfiguration from this QNetworkRequest. @@ -575,13 +596,15 @@ void QHttpThreadDelegate::headerChangedSlot() incomingReasonPhrase = httpReply->reasonPhrase(); isPipeliningUsed = httpReply->isPipeliningUsed(); incomingContentLength = httpReply->contentLength(); + isSpdyUsed = httpReply->isSpdyUsed(); emit downloadMetaData(incomingHeaders, incomingStatusCode, incomingReasonPhrase, isPipeliningUsed, downloadBuffer, - incomingContentLength); + incomingContentLength, + isSpdyUsed); } void QHttpThreadDelegate::synchronousHeaderChangedSlot() @@ -597,6 +620,7 @@ void QHttpThreadDelegate::synchronousHeaderChangedSlot() incomingStatusCode = httpReply->statusCode(); incomingReasonPhrase = httpReply->reasonPhrase(); isPipeliningUsed = httpReply->isPipeliningUsed(); + isSpdyUsed = httpReply->isSpdyUsed(); incomingContentLength = httpReply->contentLength(); } diff --git a/src/network/access/qhttpthreaddelegate_p.h b/src/network/access/qhttpthreaddelegate_p.h index d9ef1a0a55..6e6d3b5797 100644 --- a/src/network/access/qhttpthreaddelegate_p.h +++ b/src/network/access/qhttpthreaddelegate_p.h @@ -111,6 +111,7 @@ public: int incomingStatusCode; QString incomingReasonPhrase; bool isPipeliningUsed; + bool isSpdyUsed; qint64 incomingContentLength; QNetworkReply::NetworkError incomingErrorCode; QString incomingErrorDetail; @@ -139,7 +140,8 @@ signals: void sslErrors(const QList<QSslError> &, bool *, QList<QSslError> *); void sslConfigurationChanged(const QSslConfiguration); #endif - void downloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64); + void downloadMetaData(QList<QPair<QByteArray,QByteArray> >, int, QString, bool, + QSharedPointer<char>, qint64, bool); void downloadProgress(qint64, qint64); void downloadData(QByteArray); void error(QNetworkReply::NetworkError, const QString); diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp index c89419091f..473acc5f22 100644 --- a/src/network/access/qnetworkaccessmanager.cpp +++ b/src/network/access/qnetworkaccessmanager.cpp @@ -977,6 +977,12 @@ QNetworkAccessManager::NetworkAccessibility QNetworkAccessManager::networkAccess \a sslConfiguration. This function is useful to complete the TCP and SSL handshake to a host before the HTTPS request is made, resulting in a lower network latency. + \note Preconnecting a SPDY connection can be done by calling setAllowedNextProtocols() + on \a sslConfiguration with QSslConfiguration::NextProtocolSpdy3_0 contained in + the list of allowed protocols. When using SPDY, one single connection per host is + enough, i.e. calling this method multiple times per host will not result in faster + network transactions. + \note This function has no possibility to report errors. \sa connectToHost(), get(), post(), put(), deleteResource() @@ -991,6 +997,13 @@ void QNetworkAccessManager::connectToHostEncrypted(const QString &hostName, quin QNetworkRequest request(url); if (sslConfiguration != QSslConfiguration::defaultConfiguration()) request.setSslConfiguration(sslConfiguration); + + // There is no way to enable SPDY via a request, so we need to check + // the ssl configuration whether SPDY is allowed here. + if (sslConfiguration.allowedNextProtocols().contains( + QSslConfiguration::NextProtocolSpdy3_0)) + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + get(request); } #endif diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index 29d23bfd8f..de4c8d0964 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -752,6 +752,9 @@ void QNetworkReplyHttpImplPrivate::postRequest() if (request.attribute(QNetworkRequest::HttpPipeliningAllowedAttribute).toBool() == true) httpRequest.setPipeliningAllowed(true); + if (request.attribute(QNetworkRequest::SpdyAllowedAttribute).toBool() == true) + httpRequest.setSPDYAllowed(true); + if (static_cast<QNetworkRequest::LoadControl> (request.attribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual) @@ -811,8 +814,12 @@ void QNetworkReplyHttpImplPrivate::postRequest() QObject::connect(delegate, SIGNAL(downloadFinished()), q, SLOT(replyFinished()), Qt::QueuedConnection); - QObject::connect(delegate, SIGNAL(downloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64)), - q, SLOT(replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64)), + QObject::connect(delegate, SIGNAL(downloadMetaData(QList<QPair<QByteArray,QByteArray> >, + int, QString, bool, + QSharedPointer<char>, qint64, bool)), + q, SLOT(replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >, + int, QString, bool, + QSharedPointer<char>, qint64, bool)), Qt::QueuedConnection); QObject::connect(delegate, SIGNAL(downloadProgress(qint64,qint64)), q, SLOT(replyDownloadProgressSlot(qint64,qint64)), @@ -907,7 +914,8 @@ void QNetworkReplyHttpImplPrivate::postRequest() delegate->incomingReasonPhrase, delegate->isPipeliningUsed, QSharedPointer<char>(), - delegate->incomingContentLength); + delegate->incomingContentLength, + delegate->isSpdyUsed); replyDownloadData(delegate->synchronousDownloadData); httpError(delegate->incomingErrorCode, delegate->incomingErrorDetail); } else { @@ -917,7 +925,8 @@ void QNetworkReplyHttpImplPrivate::postRequest() delegate->incomingReasonPhrase, delegate->isPipeliningUsed, QSharedPointer<char>(), - delegate->incomingContentLength); + delegate->incomingContentLength, + delegate->isSpdyUsed); replyDownloadData(delegate->synchronousDownloadData); } @@ -1074,7 +1083,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData (QList<QPair<QByteArray,QByteArray> > hm, int sc,QString rp,bool pu, QSharedPointer<char> db, - qint64 contentLength) + qint64 contentLength, bool spdyWasUsed) { Q_Q(QNetworkReplyHttpImpl); Q_UNUSED(contentLength); @@ -1091,6 +1100,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData } q->setAttribute(QNetworkRequest::HttpPipeliningWasUsedAttribute, pu); + q->setAttribute(QNetworkRequest::SpdyWasUsedAttribute, spdyWasUsed); // reconstruct the HTTP header QList<QPair<QByteArray, QByteArray> > headerMap = hm; diff --git a/src/network/access/qnetworkreplyhttpimpl_p.h b/src/network/access/qnetworkreplyhttpimpl_p.h index 15cc0ec476..aa2d6f0ec9 100644 --- a/src/network/access/qnetworkreplyhttpimpl_p.h +++ b/src/network/access/qnetworkreplyhttpimpl_p.h @@ -111,7 +111,9 @@ public: // From reply Q_PRIVATE_SLOT(d_func(), void replyDownloadData(QByteArray)) Q_PRIVATE_SLOT(d_func(), void replyFinished()) - Q_PRIVATE_SLOT(d_func(), void replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64)) + Q_PRIVATE_SLOT(d_func(), void replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >, + int, QString, bool, QSharedPointer<char>, + qint64, bool)) Q_PRIVATE_SLOT(d_func(), void replyDownloadProgressSlot(qint64,qint64)) Q_PRIVATE_SLOT(d_func(), void httpAuthenticationRequired(const QHttpNetworkRequest &, QAuthenticator *)) Q_PRIVATE_SLOT(d_func(), void httpError(QNetworkReply::NetworkError, const QString &)) @@ -276,7 +278,8 @@ public: // From HTTP thread: void replyDownloadData(QByteArray); void replyFinished(); - void replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >,int,QString,bool,QSharedPointer<char>,qint64); + void replyDownloadMetaData(QList<QPair<QByteArray,QByteArray> >, int, QString, bool, + QSharedPointer<char>, qint64, bool); void replyDownloadProgressSlot(qint64,qint64); void httpAuthenticationRequired(const QHttpNetworkRequest &request, QAuthenticator *auth); void httpError(QNetworkReply::NetworkError error, const QString &errorString); diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index 65fd693771..aa1102f9bf 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -246,6 +246,17 @@ QT_BEGIN_NAMESPACE The QNetworkSession ConnectInBackground property will be set according to this attribute. + \value SpdyAllowedAttribute + Requests only, type: QMetaType::Bool (default: false) + Indicates whether the QNetworkAccessManager code is + allowed to use SPDY with this request. This applies only + to SSL requests, and depends on the server supporting SPDY. + + \value SpdyWasUsedAttribute + Replies only, type: QMetaType::Bool + Indicates whether SPDY was used for receiving + this reply. + \value User Special type. Additional information can be passed in QVariants with types ranging from User to UserMax. The default diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index 1512c6dadd..27b02a89ef 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -86,6 +86,8 @@ public: DownloadBufferAttribute, // internal SynchronousRequestAttribute, // internal BackgroundRequestAttribute, + SpdyAllowedAttribute, + SpdyWasUsedAttribute, User = 1000, UserMax = 32767 diff --git a/src/network/access/qspdyprotocolhandler.cpp b/src/network/access/qspdyprotocolhandler.cpp new file mode 100644 index 0000000000..f12b9535fe --- /dev/null +++ b/src/network/access/qspdyprotocolhandler.cpp @@ -0,0 +1,1272 @@ +/**************************************************************************** +** +** 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 <private/qspdyprotocolhandler_p.h> +#include <private/qnoncontiguousbytedevice_p.h> +#include <private/qhttpnetworkconnectionchannel_p.h> +#include <QtCore/QtEndian> + +#if !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +QT_BEGIN_NAMESPACE + +static const char spdyDictionary[] = { + 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // ....opti + 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // ons....h + 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // ead....p + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // ost....p + 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // ut....de + 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // lete.... + 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // trace... + 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // .accept. + 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep + 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t-charse + 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t....acc + 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ept-enco + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // ding.... + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // accept-l + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // anguage. + 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // ...accep + 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t-ranges + 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // ....age. + 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // ...allow + 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // ....auth + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // orizatio + 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n....cac + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // he-contr + 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // ol....co + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // nnection + 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // ....cont + 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // ent-base + 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // ....cont + 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // ent-enco + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // ding.... + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // content- + 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // language + 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // ....cont + 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // ent-leng + 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // th....co + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // ntent-lo + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // cation.. + 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten + 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t-md5... + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // .content + 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // -range.. + 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // ..conten + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t-type.. + 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // ..date.. + 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // ..etag.. + 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // ..expect + 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // ....expi + 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // res....f + 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // rom....h + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // ost....i + 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f-match. + 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // ...if-mo + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // dified-s + 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // ince.... + 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // if-none- + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // match... + 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // .if-rang + 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e....if- + 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // unmodifi + 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // ed-since + 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // ....last + 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // -modifie + 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d....loc + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // ation... + 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // .max-for + 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // wards... + 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // .pragma. + 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // ...proxy + 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // -authent + 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // icate... + 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // .proxy-a + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // uthoriza + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // tion.... + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // range... + 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // .referer + 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // ....retr + 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y-after. + 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // ...serve + 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r....te. + 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // ...trail + 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // er....tr + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // ansfer-e + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // ncoding. + 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // ...upgra + 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // de....us + 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // er-agent + 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // ....vary + 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // ....via. + 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // ...warni + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // ng....ww + 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w-authen + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // ticate.. + 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // ..method + 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // ....get. + 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // ...statu + 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s....200 + 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // .OK....v + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // ersion.. + 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // ..HTTP.1 + 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // .1....ur + 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l....pub + 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // lic....s + 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // et-cooki + 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e....kee + 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p-alive. + 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // ...origi + 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n1001012 + 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 01202205 + 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 20630030 + 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 23033043 + 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 05306307 + 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 40240540 + 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 64074084 + 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 09410411 + 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 41241341 + 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 44154164 + 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 17502504 + 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 505203.N + 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // on-Autho + 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // ritative + 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // .Informa + 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // tion204. + 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // No.Conte + 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // nt301.Mo + 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // ved.Perm + 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // anently4 + 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 00.Bad.R + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // equest40 + 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1.Unauth + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // orized40 + 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3.Forbid + 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // den404.N + 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // ot.Found + 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 500.Inte + 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // rnal.Ser + 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // ver.Erro + 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r501.Not + 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // .Impleme + 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // nted503. + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // Service. + 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // Unavaila + 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // bleJan.F + 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // eb.Mar.A + 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // pr.May.J + 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // un.Jul.A + 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // ug.Sept. + 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // Oct.Nov. + 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // Dec.00.0 + 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0.00.Mon + 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // ..Tue..W + 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // ed..Thu. + 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // .Fri..Sa + 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t..Sun.. + 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // GMTchunk + 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // ed.text. + 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // html.ima + 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // ge.png.i + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // mage.jpg + 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // .image.g + 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // if.appli + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x + 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // ml.appli + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // cation.x + 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // html.xml + 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // .text.pl + 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // ain.text + 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // .javascr + 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // ipt.publ + 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // icprivat + 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // emax-age + 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // .gzip.de + 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // flate.sd + 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // chcharse + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t.utf-8c + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // harset.i + 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // so-8859- + 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1.utf-.. + 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // .enq.0. +}; + +// uncomment to debug +//static void printHex(const QByteArray &ba) +//{ +// QByteArray hex; +// QByteArray clearText; +// for (int a = 0; a < ba.count(); ++a) { +// QByteArray currentHexChar = QByteArray(1, ba.at(a)).toHex().rightJustified(2, ' '); +// QByteArray currentChar; +// if (ba.at(a) >= 32 && ba.at(a) < 126) { // if ASCII, print the letter +// currentChar = QByteArray(1, ba.at(a)); +// } else { +// currentChar = " "; +// } +// clearText.append(currentChar.rightJustified(2, ' ')); +// hex.append(currentHexChar); +// hex.append(' '); +// clearText.append(' '); +// } +// int chunkSize = 102; // 12 == 4 bytes per line +// for (int a = 0; a < hex.count(); a += chunkSize) { +// qDebug() << hex.mid(a, chunkSize); +// qDebug() << clearText.mid(a, chunkSize); +// } +//} + +QSpdyProtocolHandler::QSpdyProtocolHandler(QHttpNetworkConnectionChannel *channel) + : QObject(0), QAbstractProtocolHandler(channel), + m_nextStreamID(-1), + m_maxConcurrentStreams(100), // 100 is recommended in the SPDY RFC + m_initialWindowSize(0), + m_waitingForCompleteStream(false) +{ + m_inflateStream.zalloc = Z_NULL; + m_inflateStream.zfree = Z_NULL; + m_inflateStream.opaque = Z_NULL; + int zlibRet = inflateInit(&m_inflateStream); + Q_ASSERT(zlibRet == Z_OK); + + m_deflateStream.zalloc = Z_NULL; + m_deflateStream.zfree = Z_NULL; + m_deflateStream.opaque = Z_NULL; + + // Do actually not compress (i.e. compression level = 0) + // when sending the headers because of the CRIME attack + zlibRet = deflateInit(&m_deflateStream, /* compression level = */ 0); + Q_ASSERT(zlibRet == Z_OK); +} + +QSpdyProtocolHandler::~QSpdyProtocolHandler() +{ + deflateEnd(&m_deflateStream); + deflateEnd(&m_inflateStream); +} + +bool QSpdyProtocolHandler::sendRequest() +{ + Q_ASSERT(!m_reply); + + int maxPossibleRequests = m_maxConcurrentStreams - m_inFlightStreams.count(); + Q_ASSERT(maxPossibleRequests >= 0); + if (maxPossibleRequests == 0) + return true; // return early if max concurrent requests are exceeded + + m_channel->state = QHttpNetworkConnectionChannel::WritingState; + + // requests will be ordered by priority (see QMultiMap doc) + QList<HttpMessagePair> requests = m_channel->spdyRequestsToSend.values(); + QList<int> priorities = m_channel->spdyRequestsToSend.keys(); + + int requestsToSend = qMin(requests.count(), maxPossibleRequests); + + for (int a = 0; a < requestsToSend; ++a) { + HttpMessagePair currentPair = requests.at(a); + QHttpNetworkRequest currentRequest = requests.at(a).first; + QHttpNetworkReply *currentReply = requests.at(a).second; + + currentReply->setSpdyWasUsed(true); + qint32 streamID = generateNextStreamID(); + + currentReply->setRequest(currentRequest); + currentReply->d_func()->connection = m_connection; + currentReply->d_func()->connectionChannel = m_channel; + m_inFlightStreams.insert(streamID, currentPair); + + sendSYN_STREAM(currentPair, streamID, /* associatedToStreamID = */ 0); + int requestsRemoved = m_channel->spdyRequestsToSend.remove( + priorities.at(a), currentPair); + Q_ASSERT(requestsRemoved == 1); + Q_UNUSED(requestsRemoved); // silence -Wunused-variable + } + m_channel->state = QHttpNetworkConnectionChannel::IdleState; + return true; +} + +void QSpdyProtocolHandler::_q_receiveReply() +{ + Q_ASSERT(m_socket); + + // 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<QHttpNetworkConnection*>(m_connection)) { + return; + } + + if (bytesAvailable() < 8) + return; // cannot read frame headers, wait for more data + + char frameHeadersRaw[8]; + if (!readNextChunk(8, frameHeadersRaw)) + return; // this should not happen, we just checked + + const QByteArray frameHeaders(frameHeadersRaw, 8); // ### try without memcpy + if (frameHeadersRaw[0] & 0x80) { + handleControlFrame(frameHeaders); + } else { + handleDataFrame(frameHeaders); + } + + // after handling the current frame, check whether there is more data waiting + if (m_socket->bytesAvailable() > 0) + QMetaObject::invokeMethod(m_channel, "_q_receiveReply", Qt::QueuedConnection); +} + +void QSpdyProtocolHandler::_q_readyRead() +{ + _q_receiveReply(); +} + +static qint16 twoBytesToInt(const char *bytes) +{ + return qFromBigEndian<qint16>(reinterpret_cast<const uchar *>(bytes)); +} + +static qint32 threeBytesToInt(const char *bytes) +{ + return qFromBigEndian<qint32>(reinterpret_cast<const uchar *>(bytes)) >> 8; +} + +static qint32 fourBytesToInt(const char *bytes) +{ + return qFromBigEndian<qint32>(reinterpret_cast<const uchar *>(bytes)); +} + +static void appendIntToThreeBytes(char *output, qint32 number) +{ + qToBigEndian<qint16>(number, reinterpret_cast<uchar *>(output + 1)); + qToBigEndian<qint8>(number >> 16, reinterpret_cast<uchar *>(output)); +} + +static void appendIntToFourBytes(char *output, qint32 number) +{ + qToBigEndian<qint32>(number, reinterpret_cast<uchar *>(output)); +} + +static QByteArray intToFourBytes(qint32 number) // ### try to use appendIntToFourBytes where possible +{ + uchar data[4]; + qToBigEndian<qint32>(number, data); + QByteArray ret(reinterpret_cast<char *>(data), 4); + return ret; +} + +static QByteArray intToThreeBytes(qint32 number) +{ + uchar data[4]; + qToBigEndian<qint32>(number << 8, data); + QByteArray ret(reinterpret_cast<char *>(data), 3); + return ret; +} + +static qint32 getStreamID(const char *bytes) +{ + // eliminate most significant bit; it might be 0 or 1 depending on whether + // we are dealing with a control or data frame + return fourBytesToInt(bytes) & 0x3fffffff; +} + +static QByteArray headerField(const QByteArray &name, const QByteArray &value) +{ + QByteArray ret; + ret.reserve(name.count() + value.count() + 8); // 4 byte for length each + ret.append(intToFourBytes(name.count())); + ret.append(name); + ret.append(intToFourBytes(value.count())); + ret.append(value); + return ret; +} + +bool QSpdyProtocolHandler::uncompressHeader(const QByteArray &input, QByteArray *output) +{ + const size_t chunkSize = 1024; + char outputRaw[chunkSize]; + // input bytes will not be changed by zlib, so it is safe to const_cast here + m_inflateStream.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(input.constData())); + m_inflateStream.avail_in = input.count(); + m_inflateStream.total_in = input.count(); + int zlibRet; + + do { + m_inflateStream.next_out = reinterpret_cast<Bytef *>(outputRaw); + m_inflateStream.avail_out = chunkSize; + zlibRet = inflate(&m_inflateStream, Z_SYNC_FLUSH); + if (zlibRet == Z_NEED_DICT) { + zlibRet = inflateSetDictionary(&m_inflateStream, + reinterpret_cast<const Bytef*>(spdyDictionary), + /* dictionaryLength = */ 1423); + Q_ASSERT(zlibRet == Z_OK); + continue; + } + switch (zlibRet) { + case Z_BUF_ERROR: { + if (m_inflateStream.avail_in == 0) { + int outputSize = chunkSize - m_inflateStream.avail_out; + output->append(outputRaw, outputSize); + m_inflateStream.avail_out = chunkSize; + } + break; + } + case Z_OK: { + int outputSize = chunkSize - m_inflateStream.avail_out; + output->append(outputRaw, outputSize); + break; + } + default: { + qWarning() << Q_FUNC_INFO << "got unexpected zlib return value:" << zlibRet; + return false; + } + } + } while (m_inflateStream.avail_in > 0 && zlibRet != Z_STREAM_END); + + Q_ASSERT(m_inflateStream.avail_in == 0); + return true; +} + +QByteArray QSpdyProtocolHandler::composeHeader(const QHttpNetworkRequest &request) +{ + QByteArray uncompressedHeader; + uncompressedHeader.reserve(300); // rough estimate + + // calculate additional headers first, because we need to know the size + // ### do not partially copy the list, but restrict the set header fields + // in QHttpNetworkConnection + QList<QPair<QByteArray, QByteArray> > additionalHeaders; + for (int a = 0; a < request.header().count(); ++a) { + QByteArray key = request.header().at(a).first; + if (key == "Connection" || key == "Host" || key == "Keep-Alive" + || key == "Proxy-Connection" || key == "Transfer-Encoding") + continue; // those headers are not valid (section 3.2.1) + additionalHeaders.append(request.header().at(a)); + } + + qint32 numberOfHeaderPairs = 5 + additionalHeaders.count(); // 5 mandatory below + the additional ones + uncompressedHeader.append(intToFourBytes(numberOfHeaderPairs)); + + // mandatory header fields: + + uncompressedHeader.append(headerField(":method", request.methodName())); +#ifndef QT_NO_NETWORKPROXY + bool useProxy = m_connection->d_func()->networkProxy.type() != QNetworkProxy::NoProxy; + uncompressedHeader.append(headerField(":path", request.uri(useProxy))); +#else + uncompressedHeader.append(headerField(":path", request.uri(false))); +#endif + uncompressedHeader.append(headerField(":version", "HTTP/1.1")); + + QHostAddress add; // ### unify with the host parsing from QHttpNetworkConnection + QByteArray host; + QString hostName = m_connection->hostName(); + if (add.setAddress(hostName)) { + if (add.protocol() == QAbstractSocket::IPv6Protocol) + host = "[" + hostName.toLatin1() + "]"; //format the ipv6 in the standard way + else + host = hostName.toLatin1(); + + } else { + host = QUrl::toAce(hostName); + } + + int port = request.url().port(); + if (port != -1) { + host += ':'; + host += QByteArray::number(port); + } + uncompressedHeader.append(headerField(":host", host)); + + uncompressedHeader.append(headerField(":scheme", request.url().scheme().toLatin1())); + + // end of mandatory header fields + + // now add the additional headers + for (int a = 0; a < additionalHeaders.count(); ++a) { + uncompressedHeader.append(headerField(additionalHeaders.at(a).first.toLower(), + additionalHeaders.at(a).second)); + } + + m_deflateStream.total_in = uncompressedHeader.count(); + m_deflateStream.avail_in = uncompressedHeader.count(); + m_deflateStream.next_in = reinterpret_cast<unsigned char *>(uncompressedHeader.data()); + int outputBytes = uncompressedHeader.count() + 30; // 30 bytes of compression header overhead + m_deflateStream.avail_out = outputBytes; + unsigned char *out = new unsigned char[outputBytes]; + m_deflateStream.next_out = out; + int availOutBefore = m_deflateStream.avail_out; + int zlibRet = deflate(&m_deflateStream, Z_SYNC_FLUSH); // do everything in one go since we use no compression + int compressedHeaderSize = availOutBefore - m_deflateStream.avail_out; + Q_ASSERT(zlibRet == Z_OK); // otherwise, we need to allocate more outputBytes + Q_UNUSED(zlibRet); // silence -Wunused-variable + Q_ASSERT(m_deflateStream.avail_in == 0); + QByteArray compressedHeader(reinterpret_cast<char *>(out), compressedHeaderSize); + delete[] out; + + return compressedHeader; +} + +quint64 QSpdyProtocolHandler::bytesAvailable() const +{ + Q_ASSERT(m_socket); + return m_spdyBuffer.byteAmount() + m_socket->bytesAvailable(); +} + +bool QSpdyProtocolHandler::readNextChunk(qint64 length, char *sink) +{ + qint64 expectedReadBytes = length; + qint64 requiredBytesFromBuffer = 0; + + if (m_waitingForCompleteStream) { + requiredBytesFromBuffer = qMin(length, m_spdyBuffer.byteAmount()); + // ### if next chunk from buffer bigger than what we want to read, + // we have to call read() (which memcpy's). Otherwise, we can just + // read the next chunk without memcpy'ing. + qint64 bytesReadFromBuffer = m_spdyBuffer.read(sink, requiredBytesFromBuffer); + Q_ASSERT(bytesReadFromBuffer == requiredBytesFromBuffer); + if (length <= bytesReadFromBuffer) { + return true; // buffer > required size -> no need to read from socket + } + expectedReadBytes -= requiredBytesFromBuffer; + } + qint64 readBytes = m_socket->read(sink + requiredBytesFromBuffer, expectedReadBytes); + + if (readBytes < expectedReadBytes) { + m_waitingForCompleteStream = true; + // ### this is inefficient, we should not put back so much data into the buffer + QByteArray temp(sink, requiredBytesFromBuffer + readBytes); + m_spdyBuffer.append(temp); + return false; + } else { + return true; // buffer must be cleared by calling function + } +} + +void QSpdyProtocolHandler::sendControlFrame(FrameType type, + ControlFrameFlags flags, + const char *data, + quint32 length) +{ + // frame type and stream ID + char header[8]; + header[0] = 0x80; // leftmost bit == 1 -> is a control frame + header[1] = 0x03; // 3 bit == version 3 + header[2] = 0; + switch (type) { + case FrameType_CREDENTIAL: { + qWarning("sending SPDY CREDENTIAL frame is not yet implemented"); // QTBUG-36188 + return; + } + default: + header[3] = type; + } + + // flags + header[4] = 0; + if (flags & ControlFrame_FLAG_FIN || length == 0) { + Q_ASSERT(type == FrameType_SYN_STREAM || type == FrameType_SYN_REPLY + || type == FrameType_HEADERS || length == 0); + header[4] |= ControlFrame_FLAG_FIN; + } + if (flags & ControlFrame_FLAG_UNIDIRECTIONAL) { + Q_ASSERT(type == FrameType_SYN_STREAM); + header[4] |= ControlFrame_FLAG_UNIDIRECTIONAL; + } + + // length + appendIntToThreeBytes(header + 5, length); + + qint64 written = m_socket->write(header, 8); + Q_ASSERT(written == 8); + written = m_socket->write(data, length); + Q_ASSERT(written == length); +} + +void QSpdyProtocolHandler::sendSYN_STREAM(HttpMessagePair messagePair, + qint32 streamID, qint32 associatedToStreamID) +{ + QHttpNetworkRequest request = messagePair.first; + QHttpNetworkReply *reply = messagePair.second; + + ControlFrameFlags flags = 0; + + if (!request.uploadByteDevice()) { + // no upload -> this is the last frame, send the FIN flag + flags |= ControlFrame_FLAG_FIN; + reply->d_func()->state = QHttpNetworkReplyPrivate::SPDYHalfClosed; + } else { + reply->d_func()->state = QHttpNetworkReplyPrivate::SPDYUploading; + + // hack: set the stream ID on the device directly, so when we get + // the signal for uploading we know which stream we are sending on + request.uploadByteDevice()->setProperty("SPDYStreamID", streamID); + + QObject::connect(request.uploadByteDevice(), SIGNAL(readyRead()), this, + SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection); + } + + QByteArray namesAndValues = composeHeader(request); + quint32 length = namesAndValues.count() + 10; // 10 == 4 for Stream-ID + 4 for Associated-To-Stream-ID + // + 2 for Priority, Unused and Slot + + QByteArray wireData; + wireData.reserve(length); + wireData.append(intToFourBytes(streamID)); + wireData.append(intToFourBytes(associatedToStreamID)); + + // priority (3 bits) / unused (5 bits) / slot (8 bits) + char prioAndSlot[2]; + switch (request.priority()) { + case QHttpNetworkRequest::HighPriority: + prioAndSlot[0] = 0x00; // == prio 0 (highest) + break; + case QHttpNetworkRequest::NormalPriority: + prioAndSlot[0] = 0x80; // == prio 4 + break; + case QHttpNetworkRequest::LowPriority: + prioAndSlot[0] = 0xe0; // == prio 7 (lowest) + break; + } + prioAndSlot[1] = 0x00; // slot in client certificates (not supported currently) + wireData.append(prioAndSlot, 2); + + wireData.append(namesAndValues); + + sendControlFrame(FrameType_SYN_STREAM, flags, wireData.constData(), length); + + if (reply->d_func()->state == QHttpNetworkReplyPrivate::SPDYUploading) + uploadData(streamID); +} + +void QSpdyProtocolHandler::sendRST_STREAM(qint32 streamID, RST_STREAM_STATUS_CODE statusCode) +{ + char wireData[8]; + appendIntToFourBytes(wireData, streamID); + appendIntToFourBytes(wireData + 4, statusCode); + sendControlFrame(FrameType_RST_STREAM, /* flags = */ 0, wireData, /* length = */ 8); +} + +void QSpdyProtocolHandler::sendPING(quint32 pingID) +{ + char rawData[4]; + appendIntToFourBytes(rawData, pingID); + sendControlFrame(FrameType_PING, /* flags = */ 0, rawData, /* length = */ 4); +} + +bool QSpdyProtocolHandler::uploadData(qint32 streamID) +{ + // we only rely on SPDY flow control here and don't care about TCP buffers + + HttpMessagePair messagePair = m_inFlightStreams.value(streamID); + QHttpNetworkRequest request = messagePair.first; + QHttpNetworkReply *reply = messagePair.second; + Q_ASSERT(reply); + QHttpNetworkReplyPrivate *replyPrivate = reply->d_func(); + Q_ASSERT(replyPrivate); + + qint32 dataLeftInWindow = replyPrivate->windowSizeUpload + - replyPrivate->currentlyUploadedDataInWindow; + + while (dataLeftInWindow > 0 && !request.uploadByteDevice()->atEnd()) { + + // get pointer to upload data + qint64 currentReadSize = 0; + const char *readPointer = request.uploadByteDevice()->readPointer(dataLeftInWindow, + currentReadSize); + + if (currentReadSize == -1) { + // premature eof happened + m_connection->d_func()->emitReplyError(m_socket, reply, + QNetworkReply::UnknownNetworkError); + return false; + } else if (readPointer == 0 || currentReadSize == 0) { + // nothing to read currently, break the loop + break; + } else { + DataFrameFlags flags = 0; + // we will send the FIN flag later if appropriate + qint64 currentWriteSize = sendDataFrame(streamID, flags, currentReadSize, readPointer); + if (currentWriteSize == -1 || currentWriteSize != currentReadSize) { + // socket broke down + m_connection->d_func()->emitReplyError(m_socket, reply, + QNetworkReply::UnknownNetworkError); + return false; + } else { + replyPrivate->currentlyUploadedDataInWindow += currentWriteSize; + replyPrivate->totallyUploadedData += currentWriteSize; + dataLeftInWindow = replyPrivate->windowSizeUpload + - replyPrivate->currentlyUploadedDataInWindow; + request.uploadByteDevice()->advanceReadPointer(currentWriteSize); + + emit reply->dataSendProgress(replyPrivate->totallyUploadedData, + request.contentLength()); + } + } + } + if (replyPrivate->totallyUploadedData == request.contentLength()) { + DataFrameFlags finFlag = DataFrame_FLAG_FIN; + qint64 writeSize = sendDataFrame(streamID, finFlag, 0, 0); + Q_ASSERT(writeSize == 0); + Q_UNUSED(writeSize); // silence -Wunused-variable + replyPrivate->state = QHttpNetworkReplyPrivate::SPDYHalfClosed; + // ### this will not work if the content length is not known, but + // then again many servers will fail in this case anyhow according + // to the SPDY RFC + } + return true; +} + +void QSpdyProtocolHandler::_q_uploadDataReadyRead() +{ + QNonContiguousByteDevice *device = qobject_cast<QNonContiguousByteDevice *>(sender()); + Q_ASSERT(device); + qint32 streamID = device->property("SPDYStreamID").toInt(); + Q_ASSERT(streamID > 0); + uploadData(streamID); +} + +void QSpdyProtocolHandler::sendWINDOW_UPDATE(qint32 streamID, quint32 deltaWindowSize) +{ + char windowUpdateData[8]; + appendIntToFourBytes(windowUpdateData, streamID); + appendIntToFourBytes(windowUpdateData + 4, deltaWindowSize); + + sendControlFrame(FrameType_WINDOW_UPDATE, /* flags = */ 0, windowUpdateData, /* length = */ 8); +} + +qint64 QSpdyProtocolHandler::sendDataFrame(qint32 streamID, DataFrameFlags flags, + quint32 length, const char *data) +{ + QByteArray wireData; + wireData.reserve(8); + + wireData.append(intToFourBytes(streamID)); + wireData.append(flags); + wireData.append(intToThreeBytes(length)); + + Q_ASSERT(m_socket); + m_socket->write(wireData); + + if (data) { + qint64 ret = m_socket->write(data, length); + return ret; + } else { + return 0; // nothing to write, e.g. FIN flag + } +} + +void QSpdyProtocolHandler::handleControlFrame(const QByteArray &frameHeaders) // ### make it char * +{ + Q_ASSERT(frameHeaders.count() >= 8); + qint16 version = twoBytesToInt(frameHeaders.constData()); + version &= 0x3fff; // eliminate most significant bit to determine version + Q_ASSERT(version == 3); + + qint16 type = twoBytesToInt(frameHeaders.constData() + 2); + + char flags = frameHeaders.at(4); + qint32 length = threeBytesToInt(frameHeaders.constData() + 5); + Q_ASSERT(length > 0); + + QByteArray frameData; + frameData.resize(length); + if (!readNextChunk(length, frameData.data())) { + // put back the frame headers to the buffer + m_spdyBuffer.prepend(frameHeaders); + return; // we couldn't read the whole frame and need to wait + } else { + m_spdyBuffer.clear(); + m_waitingForCompleteStream = false; + } + + switch (type) { + case FrameType_SYN_STREAM: { + handleSYN_STREAM(flags, length, frameData); + break; + } + case FrameType_SYN_REPLY: { + handleSYN_REPLY(flags, length, frameData); + break; + } + case FrameType_RST_STREAM: { + handleRST_STREAM(flags, length, frameData); + break; + } + case FrameType_SETTINGS: { + handleSETTINGS(flags, length, frameData); + break; + } + case FrameType_PING: { + handlePING(flags, length, frameData); + break; + } + case FrameType_GOAWAY: { + handleGOAWAY(flags, length, frameData); + break; + } + case FrameType_HEADERS: { + handleHEADERS(flags, length, frameData); + break; + } + case FrameType_WINDOW_UPDATE: { + handleWINDOW_UPDATE(flags, length, frameData); + break; + } + default: + qWarning() << Q_FUNC_INFO << "cannot handle frame of type" << type; + } +} + +void QSpdyProtocolHandler::handleSYN_STREAM(char /*flags*/, quint32 /*length*/, + const QByteArray &frameData) +{ + // not implemented; will be implemented when servers start using it + // we just tell the server that we do not accept that + + qint32 streamID = getStreamID(frameData.constData()); + + sendRST_STREAM(streamID, RST_STREAM_REFUSED_STREAM); +} + +void QSpdyProtocolHandler::handleSYN_REPLY(char flags, quint32 /*length*/, const QByteArray &frameData) +{ + parseHttpHeaders(flags, frameData); +} + +void QSpdyProtocolHandler::parseHttpHeaders(char flags, const QByteArray &frameData) +{ + qint32 streamID = getStreamID(frameData.constData()); + + flags &= 0x3f; + bool flag_fin = flags & 0x01; + + QByteArray headerValuePairs = frameData.mid(4); + + HttpMessagePair pair = m_inFlightStreams.value(streamID); + QHttpNetworkReply *httpReply = pair.second; + Q_ASSERT(httpReply != 0); + + QByteArray uncompressedHeader; + if (!uncompressHeader(headerValuePairs, &uncompressedHeader)) { + qWarning() << Q_FUNC_INFO << "error reading header from SYN_REPLY message"; + return; + } + + qint32 headerCount = fourBytesToInt(uncompressedHeader.constData()); + qint32 readPointer = 4; + for (qint32 a = 0; a < headerCount; ++a) { + qint32 count = fourBytesToInt(uncompressedHeader.constData() + readPointer); + readPointer += 4; + QByteArray name = uncompressedHeader.mid(readPointer, count); + readPointer += count; + count = fourBytesToInt(uncompressedHeader.constData() + readPointer); + readPointer += 4; + QByteArray value = uncompressedHeader.mid(readPointer, count); + readPointer += count; + if (name == ":status") { + httpReply->setStatusCode(value.left(3).toInt()); + httpReply->d_func()->reasonPhrase = QString::fromLatin1(value.mid(4)); + } else if (name == ":version") { + int majorVersion = value.at(5) - 48; + int minorVersion = value.at(7) - 48; + httpReply->d_func()->majorVersion = majorVersion; + httpReply->d_func()->minorVersion = minorVersion; + } else if (name == "content-length") { + httpReply->setContentLength(value.toLongLong()); + } else { + httpReply->setHeaderField(name, value); + } + } + emit httpReply->headerChanged(); + + if (flag_fin) { + switch (httpReply->d_func()->state) { + case QHttpNetworkReplyPrivate::SPDYSYNSent: + httpReply->d_func()->state = QHttpNetworkReplyPrivate::SPDYHalfClosed; + break; + case QHttpNetworkReplyPrivate::SPDYHalfClosed: + replyFinished(httpReply, streamID); + break; + case QHttpNetworkReplyPrivate::SPDYClosed: { + sendRST_STREAM(streamID, RST_STREAM_PROTOCOL_ERROR); + replyFinishedWithError(httpReply, streamID, QNetworkReply::ProtocolFailure, + "server sent SYN_REPLY on an already closed stream"); + break; + } + default: + qWarning() << Q_FUNC_INFO << "got data frame in unknown state"; + } + } +} + +void QSpdyProtocolHandler::handleRST_STREAM(char /*flags*/, quint32 length, + const QByteArray &frameData) +{ + // flags are ignored + + Q_ASSERT(length == 8); + Q_UNUSED(length); // silence -Wunused-parameter + qint32 streamID = getStreamID(frameData.constData()); + QHttpNetworkReply *httpReply = m_inFlightStreams.value(streamID).second; + Q_ASSERT(httpReply); + + qint32 statusCodeInt = fourBytesToInt(frameData.constData() + 4); + RST_STREAM_STATUS_CODE statusCode = static_cast<RST_STREAM_STATUS_CODE>(statusCodeInt); + QNetworkReply::NetworkError errorCode; + QByteArray errorMessage; + + switch (statusCode) { + case RST_STREAM_PROTOCOL_ERROR: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "SPDY protocol error"; + break; + case RST_STREAM_INVALID_STREAM: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "SPDY stream is not active"; + break; + case RST_STREAM_REFUSED_STREAM: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "SPDY stream was refused"; + break; + case RST_STREAM_UNSUPPORTED_VERSION: + errorCode = QNetworkReply::ProtocolUnknownError; + errorMessage = "SPDY version is unknown to the server"; + break; + case RST_STREAM_CANCEL: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "SPDY stream is no longer needed"; + break; + case RST_STREAM_INTERNAL_ERROR: + errorCode = QNetworkReply::InternalServerError; + errorMessage = "Internal server error"; + break; + case RST_STREAM_FLOW_CONTROL_ERROR: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "peer violated the flow control protocol"; + break; + case RST_STREAM_STREAM_IN_USE: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "server received a SYN_REPLY for an already open stream"; + break; + case RST_STREAM_STREAM_ALREADY_CLOSED: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "server received data or a SYN_REPLY for an already half-closed stream"; + break; + case RST_STREAM_INVALID_CREDENTIALS: + errorCode = QNetworkReply::ContentAccessDenied; + errorMessage = "server received invalid credentials"; + break; + case RST_STREAM_FRAME_TOO_LARGE: + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "server cannot process the frame because it is too large"; + break; + default: + qWarning() << Q_FUNC_INFO << "could not understand servers RST_STREAM status code"; + errorCode = QNetworkReply::ProtocolFailure; + errorMessage = "got SPDY RST_STREAM message with unknown error code"; + } + replyFinishedWithError(httpReply, streamID, errorCode, errorMessage.constData()); +} + +void QSpdyProtocolHandler::handleSETTINGS(char flags, quint32 /*length*/, const QByteArray &frameData) +{ + Q_ASSERT(frameData.count() > 0); + + SETTINGS_Flags settingsFlags = static_cast<SETTINGS_Flags>(flags); + if (settingsFlags & FLAG_SETTINGS_CLEAR_SETTINGS) { + // ### clear all persistent settings; since we do not persist settings + // as of now, we don't need to clear anything either + } + + qint32 numberOfEntries = fourBytesToInt(frameData.constData()); + Q_ASSERT(numberOfEntries > 0); + for (int a = 0, frameDataIndex = 4; a < numberOfEntries; ++a, frameDataIndex += 8) { + SETTINGS_ID_Flag idFlag = static_cast<SETTINGS_ID_Flag>(frameData[frameDataIndex]); + if (idFlag & FLAG_SETTINGS_PERSIST_VALUE) { + // ### we SHOULD persist the settings here according to the RFC, but we don't have to, + // so implement that later + } // the other value is only sent by us, but not received + + quint32 uniqueID = static_cast<SETTINGS_ID>( + threeBytesToInt(frameData.constData() + frameDataIndex + 1)); + quint32 value = fourBytesToInt(frameData.constData() + frameDataIndex + 4); + switch (uniqueID) { + case SETTINGS_UPLOAD_BANDWIDTH: { + // ignored for now, just an estimated informative value + break; + } + case SETTINGS_DOWNLOAD_BANDWIDTH: { + // ignored for now, just an estimated informative value + break; + } + case SETTINGS_ROUND_TRIP_TIME: { + // ignored for now, just an estimated informative value + break; + } + case SETTINGS_MAX_CONCURRENT_STREAMS: { + m_maxConcurrentStreams = value; + break; + } + case SETTINGS_CURRENT_CWND: { + // ignored for now, just an informative value + break; + } + case SETTINGS_DOWNLOAD_RETRANS_RATE: { + // ignored for now, just an estimated informative value + break; + } + case SETTINGS_INITIAL_WINDOW_SIZE: { + m_initialWindowSize = value; + break; + } + case SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE: { + // client certificates are not supported + break; + } + default: + qWarning() << Q_FUNC_INFO << "found unknown settings value" << value; + } + } +} + +void QSpdyProtocolHandler::handlePING(char /*flags*/, quint32 length, const QByteArray &frameData) +{ + // flags are ignored + + Q_ASSERT(length == 4); + Q_UNUSED(length); // silence -Wunused-parameter + quint32 pingID = fourBytesToInt(frameData.constData()); + + // odd numbered IDs must be ignored + if ((pingID & 1) == 0) // is even? + sendPING(pingID); +} + +void QSpdyProtocolHandler::handleGOAWAY(char /*flags*/, quint32 /*length*/, + const QByteArray &frameData) +{ + // flags are ignored + + qint32 statusCode = static_cast<GOAWAY_STATUS>(fourBytesToInt(frameData.constData() + 4)); + QNetworkReply::NetworkError errorCode; + switch (statusCode) { + case GOAWAY_OK: { + errorCode = QNetworkReply::NoError; + break; + } + case GOAWAY_PROTOCOL_ERROR: { + errorCode = QNetworkReply::ProtocolFailure; + break; + } + case GOAWAY_INTERNAL_ERROR: { + errorCode = QNetworkReply::InternalServerError; + break; + } + default: + qWarning() << Q_FUNC_INFO << "unexpected status code" << statusCode; + errorCode = QNetworkReply::ProtocolUnknownError; + } + + qint32 lastGoodStreamID = getStreamID(frameData.constData()); + + // emit errors for all replies after the last good stream ID + Q_ASSERT(m_connection); + for (qint32 currentStreamID = lastGoodStreamID + 2; currentStreamID <= m_nextStreamID; + ++currentStreamID) { + QHttpNetworkReply *reply = m_inFlightStreams.value(currentStreamID).second; + Q_ASSERT(reply); + m_connection->d_func()->emitReplyError(m_socket, reply, errorCode); + } + // ### we could make sure a new session is initiated anyhow +} + +void QSpdyProtocolHandler::handleHEADERS(char flags, quint32 /*length*/, + const QByteArray &frameData) +{ + parseHttpHeaders(flags, frameData); +} + +void QSpdyProtocolHandler::handleWINDOW_UPDATE(char /*flags*/, quint32 /*length*/, + const QByteArray &frameData) +{ + qint32 streamID = getStreamID(frameData.constData()); + qint32 deltaWindowSize = fourBytesToInt(frameData.constData() + 4); + + QHttpNetworkReply *reply = m_inFlightStreams.value(streamID).second; + Q_ASSERT(reply); + QHttpNetworkReplyPrivate *replyPrivate = reply->d_func(); + Q_ASSERT(replyPrivate); + + replyPrivate->currentlyUploadedDataInWindow = replyPrivate->windowSizeUpload - deltaWindowSize; + uploadData(streamID); // we hopefully can continue to upload +} + + +void QSpdyProtocolHandler::handleDataFrame(const QByteArray &frameHeaders) +{ + Q_ASSERT(frameHeaders.count() >= 8); + + qint32 streamID = getStreamID(frameHeaders.constData()); + unsigned char flags = static_cast<unsigned char>(frameHeaders.at(4)); + flags &= 0x3f; + bool flag_fin = flags & 0x01; + bool flag_compress = flags & 0x02; + qint32 length = threeBytesToInt(frameHeaders.constData() + 5); + + QByteArray data; + data.resize(length); + if (!readNextChunk(length, data.data())) { + // put back the frame headers to the buffer + m_spdyBuffer.prepend(frameHeaders); + return; // we couldn't read the whole frame and need to wait + } else { + m_spdyBuffer.clear(); + m_waitingForCompleteStream = false; + } + + HttpMessagePair pair = m_inFlightStreams.value(streamID); + QHttpNetworkRequest httpRequest = pair.first; + QHttpNetworkReply *httpReply = pair.second; + Q_ASSERT(httpReply != 0); + + QHttpNetworkReplyPrivate *replyPrivate = httpReply->d_func(); + + // check whether we need to send WINDOW_UPDATE (i.e. tell the sender it can send more) + replyPrivate->currentlyReceivedDataInWindow += length; + qint32 dataLeftInWindow = replyPrivate->windowSizeDownload - replyPrivate->currentlyReceivedDataInWindow; + + if (replyPrivate->currentlyReceivedDataInWindow > 0 + && dataLeftInWindow < replyPrivate->windowSizeDownload / 2) { + + // socket read buffer size is 64K actually, hard coded in the channel + // We can read way more than 64K per socket, because the window size + // here is per stream. + if (replyPrivate->windowSizeDownload >= m_socket->readBufferSize()) { + replyPrivate->windowSizeDownload = m_socket->readBufferSize(); + } else { + replyPrivate->windowSizeDownload *= 1.5; + } + QMetaObject::invokeMethod(this, "sendWINDOW_UPDATE", Qt::QueuedConnection, + Q_ARG(qint32, streamID), + Q_ARG(quint32, replyPrivate->windowSizeDownload)); + // setting the current data count to 0 is a race condition, + // because we call sendWINDOW_UPDATE through the event loop. + // But then again, the whole situation is a race condition because + // we don't know when the packet will arrive at the server; so + // this is most likely good enough here. + replyPrivate->currentlyReceivedDataInWindow = 0; + } + + httpReply->d_func()->compressedData.append(data); + + + replyPrivate->totalProgress += length; + + if (httpRequest.d->autoDecompress && httpReply->d_func()->isCompressed()) { + QByteDataBuffer inDataBuffer; // ### should we introduce one in the http reply? + inDataBuffer.append(data); + qint64 compressedCount = httpReply->d_func()->uncompressBodyData(&inDataBuffer, + &replyPrivate->responseData); + Q_ASSERT(compressedCount >= 0); + Q_UNUSED(compressedCount); // silence -Wunused-variable + } else { + replyPrivate->responseData.append(data); + } + + if (replyPrivate->shouldEmitSignals()) { + emit httpReply->readyRead(); + emit httpReply->dataReadProgress(replyPrivate->totalProgress, replyPrivate->bodyLength); + } + + if (flag_compress) { + qWarning() << Q_FUNC_INFO << "SPDY level compression is not supported"; + } + + if (flag_fin) { + switch (httpReply->d_func()->state) { + case QHttpNetworkReplyPrivate::SPDYSYNSent: + httpReply->d_func()->state = QHttpNetworkReplyPrivate::SPDYHalfClosed; + // ### send FIN ourselves? + break; + case QHttpNetworkReplyPrivate::SPDYHalfClosed: + replyFinished(httpReply, streamID); + break; + case QHttpNetworkReplyPrivate::SPDYClosed: { + sendRST_STREAM(streamID, RST_STREAM_PROTOCOL_ERROR); + replyFinishedWithError(httpReply, streamID, QNetworkReply::ProtocolFailure, + "server sent data on an already closed stream"); + break; + } + default: + qWarning() << Q_FUNC_INFO << "got data frame in unknown state"; + } + } +} + +void QSpdyProtocolHandler::replyFinished(QHttpNetworkReply *httpReply, qint32 streamID) +{ + httpReply->d_func()->state = QHttpNetworkReplyPrivate::SPDYClosed; + int streamsRemoved = m_inFlightStreams.remove(streamID); + Q_ASSERT(streamsRemoved == 1); + Q_UNUSED(streamsRemoved); // silence -Wunused-variable + emit httpReply->finished(); +} + +void QSpdyProtocolHandler::replyFinishedWithError(QHttpNetworkReply *httpReply, qint32 streamID, + QNetworkReply::NetworkError errorCode, const char *errorMessage) +{ + httpReply->d_func()->state = QHttpNetworkReplyPrivate::SPDYClosed; + int streamsRemoved = m_inFlightStreams.remove(streamID); + Q_ASSERT(streamsRemoved == 1); + Q_UNUSED(streamsRemoved); // silence -Wunused-variable + emit httpReply->finishedWithError(errorCode, QSpdyProtocolHandler::tr(errorMessage)); +} + +qint32 QSpdyProtocolHandler::generateNextStreamID() +{ + // stream IDs initiated by the client must be odd + m_nextStreamID += 2; + return m_nextStreamID; +} + +QT_END_NAMESPACE + +#endif // !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) diff --git a/src/network/access/qspdyprotocolhandler_p.h b/src/network/access/qspdyprotocolhandler_p.h new file mode 100644 index 0000000000..8cbfbdda86 --- /dev/null +++ b/src/network/access/qspdyprotocolhandler_p.h @@ -0,0 +1,228 @@ +/**************************************************************************** +** +** 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 QSPDYPROTOCOLHANDLER_H +#define QSPDYPROTOCOLHANDLER_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 <private/qabstractprotocolhandler_p.h> +#include <QtNetwork/qnetworkreply.h> +#include <private/qbytedata_p.h> + +#include <zlib.h> + +#if !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +QT_BEGIN_NAMESPACE + +class QHttpNetworkRequest; + +#ifndef HttpMessagePair +typedef QPair<QHttpNetworkRequest, QHttpNetworkReply*> HttpMessagePair; +#endif + +class QSpdyProtocolHandler : public QObject, public QAbstractProtocolHandler { + Q_OBJECT +public: + QSpdyProtocolHandler(QHttpNetworkConnectionChannel *channel); + ~QSpdyProtocolHandler(); + + enum DataFrameFlag { + DataFrame_FLAG_FIN = 0x01, + DataFrame_FLAG_COMPRESS = 0x02 + }; + + Q_DECLARE_FLAGS(DataFrameFlags, DataFrameFlag) + + enum ControlFrameFlag { + ControlFrame_FLAG_FIN = 0x01, + ControlFrame_FLAG_UNIDIRECTIONAL = 0x02 + }; + + Q_DECLARE_FLAGS(ControlFrameFlags, ControlFrameFlag) + + enum SETTINGS_Flag { + FLAG_SETTINGS_CLEAR_SETTINGS = 0x01 + }; + + Q_DECLARE_FLAGS(SETTINGS_Flags, SETTINGS_Flag) + + enum SETTINGS_ID_Flag { + FLAG_SETTINGS_PERSIST_VALUE = 0x01, + FLAG_SETTINGS_PERSISTED = 0x02 + }; + + Q_DECLARE_FLAGS(SETTINGS_ID_Flags, SETTINGS_ID_Flag) + + virtual void _q_receiveReply() Q_DECL_OVERRIDE; + virtual void _q_readyRead() Q_DECL_OVERRIDE; + virtual bool sendRequest() Q_DECL_OVERRIDE; + +private slots: + void _q_uploadDataReadyRead(); + +private: + + enum FrameType { + FrameType_SYN_STREAM = 1, + FrameType_SYN_REPLY = 2, + FrameType_RST_STREAM = 3, + FrameType_SETTINGS = 4, + FrameType_PING = 6, + FrameType_GOAWAY = 7, + FrameType_HEADERS = 8, + FrameType_WINDOW_UPDATE = 9, + FrameType_CREDENTIAL // has a special type + }; + + enum StatusCode { + StatusCode_PROTOCOL_ERROR = 1, + StatusCode_INVALID_STREAM = 2, + StatusCode_REFUSED_STREAM = 3, + StatusCode_UNSUPPORTED_VERSION = 4, + StatusCode_CANCEL = 5, + StatusCode_INTERNAL_ERROR = 6, + StatusCode_FLOW_CONTROL_ERROR = 7, + StatusCode_STREAM_IN_USE = 8, + StatusCode_STREAM_ALREADY_CLOSED = 9, + StatusCode_INVALID_CREDENTIALS = 10, + StatusCode_FRAME_TOO_LARGE = 11 + }; + + enum SETTINGS_ID { + SETTINGS_UPLOAD_BANDWIDTH = 1, + SETTINGS_DOWNLOAD_BANDWIDTH = 2, + SETTINGS_ROUND_TRIP_TIME = 3, + SETTINGS_MAX_CONCURRENT_STREAMS = 4, + SETTINGS_CURRENT_CWND = 5, + SETTINGS_DOWNLOAD_RETRANS_RATE = 6, + SETTINGS_INITIAL_WINDOW_SIZE = 7, + SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE = 8 + }; + + enum GOAWAY_STATUS { + GOAWAY_OK = 0, + GOAWAY_PROTOCOL_ERROR = 1, + GOAWAY_INTERNAL_ERROR = 11 + }; + + enum RST_STREAM_STATUS_CODE { + RST_STREAM_PROTOCOL_ERROR = 1, + RST_STREAM_INVALID_STREAM = 2, + RST_STREAM_REFUSED_STREAM = 3, + RST_STREAM_UNSUPPORTED_VERSION = 4, + RST_STREAM_CANCEL = 5, + RST_STREAM_INTERNAL_ERROR = 6, + RST_STREAM_FLOW_CONTROL_ERROR = 7, + RST_STREAM_STREAM_IN_USE = 8, + RST_STREAM_STREAM_ALREADY_CLOSED = 9, + RST_STREAM_INVALID_CREDENTIALS = 10, + RST_STREAM_FRAME_TOO_LARGE = 11 + }; + + quint64 bytesAvailable() const; + bool readNextChunk(qint64 length, char *sink); + + void sendControlFrame(FrameType type, ControlFrameFlags flags, const char *data, quint32 length); + + void sendSYN_STREAM(HttpMessagePair pair, qint32 streamID, + qint32 associatedToStreamID); + void sendRST_STREAM(qint32 streamID, RST_STREAM_STATUS_CODE statusCode); + void sendPING(quint32 pingID); + + bool uploadData(qint32 streamID); + Q_INVOKABLE void sendWINDOW_UPDATE(qint32 streamID, quint32 deltaWindowSize); + + qint64 sendDataFrame(qint32 streamID, DataFrameFlags flags, quint32 length, + const char *data); + + QByteArray composeHeader(const QHttpNetworkRequest &request); + bool uncompressHeader(const QByteArray &input, QByteArray *output); + + void handleControlFrame(const QByteArray &frameHeaders); + void handleDataFrame(const QByteArray &frameHeaders); + + void handleSYN_STREAM(char, quint32, const QByteArray &frameData); + void handleSYN_REPLY(char flags, quint32, const QByteArray &frameData); + void handleRST_STREAM(char flags, quint32 length, const QByteArray &frameData); + void handleSETTINGS(char flags, quint32 length, const QByteArray &frameData); + void handlePING(char, quint32 length, const QByteArray &frameData); + void handleGOAWAY(char flags, quint32, const QByteArray &frameData); + void handleHEADERS(char flags, quint32, const QByteArray &frameData); + void handleWINDOW_UPDATE(char, quint32, const QByteArray &frameData); + + qint32 generateNextStreamID(); + void parseHttpHeaders(char flags, const QByteArray &frameData); + + void replyFinished(QHttpNetworkReply *httpReply, qint32 streamID); + void replyFinishedWithError(QHttpNetworkReply *httpReply, qint32 streamID, + QNetworkReply::NetworkError errorCode, const char *errorMessage); + + qint32 m_nextStreamID; + QHash<quint32, HttpMessagePair> m_inFlightStreams; + qint32 m_maxConcurrentStreams; + quint32 m_initialWindowSize; + QByteDataBuffer m_spdyBuffer; + bool m_waitingForCompleteStream; + z_stream m_deflateStream; + z_stream m_inflateStream; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QSpdyProtocolHandler::DataFrameFlags) +Q_DECLARE_OPERATORS_FOR_FLAGS(QSpdyProtocolHandler::ControlFrameFlags) +Q_DECLARE_OPERATORS_FOR_FLAGS(QSpdyProtocolHandler::SETTINGS_Flags) +Q_DECLARE_OPERATORS_FOR_FLAGS(QSpdyProtocolHandler::SETTINGS_ID_Flags) + +QT_END_NAMESPACE + +#endif // !defined(QT_NO_HTTP) && !defined(QT_NO_SSL) + +#endif // QSPDYPROTOCOLHANDLER_H diff --git a/tests/auto/network/access/access.pro b/tests/auto/network/access/access.pro index 3139f19f7b..bc76190e30 100644 --- a/tests/auto/network/access/access.pro +++ b/tests/auto/network/access/access.pro @@ -7,6 +7,7 @@ SUBDIRS=\ qnetworkrequest \ qhttpnetworkconnection \ qnetworkreply \ + spdy \ qnetworkcachemetadata \ qftp \ qhttpnetworkreply \ diff --git a/tests/auto/network/access/spdy/spdy.pro b/tests/auto/network/access/spdy/spdy.pro new file mode 100644 index 0000000000..6bfc6d84e0 --- /dev/null +++ b/tests/auto/network/access/spdy/spdy.pro @@ -0,0 +1,7 @@ +CONFIG += testcase +CONFIG += parallel_test +TARGET = tst_spdy +SOURCES += tst_spdy.cpp + +QT = core core-private network network-private testlib +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0 diff --git a/tests/auto/network/access/spdy/tst_spdy.cpp b/tests/auto/network/access/spdy/tst_spdy.cpp new file mode 100644 index 0000000000..d2a220bad4 --- /dev/null +++ b/tests/auto/network/access/spdy/tst_spdy.cpp @@ -0,0 +1,690 @@ +/**************************************************************************** +** +** Copyright (C) 2014 BlackBerry Limited. All rights reserved. +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite 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 <QtTest/QtTest> +#include <QtNetwork/QNetworkAccessManager> +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QHttpPart> +#include <QtNetwork/QHttpMultiPart> +#include <QtNetwork/QNetworkProxy> +#include <QtNetwork/QAuthenticator> +#ifdef QT_BUILD_INTERNAL +#include <QtNetwork/private/qsslsocket_openssl_p.h> +#endif // QT_BUILD_INTERNAL + +#include "../../../network-settings.h" + +Q_DECLARE_METATYPE(QAuthenticator*) + +class tst_Spdy: public QObject +{ + Q_OBJECT + +public: + tst_Spdy(); + ~tst_Spdy(); + +private Q_SLOTS: + void settingsAndNegotiation_data(); + void settingsAndNegotiation(); + void download_data(); + void download(); + void headerFields(); + void upload_data(); + void upload(); + void errors_data(); + void errors(); + void multipleRequests_data(); + void multipleRequests(); + +private: + QNetworkAccessManager m_manager; + int m_multipleRequestsCount; + int m_multipleRepliesFinishedCount; + +protected Q_SLOTS: + void proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *authenticator); + void multipleRequestsFinishedSlot(); +}; + +tst_Spdy::tst_Spdy() +{ +#if defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) && OPENSSL_VERSION_NUMBER >= 0x1000100fL && !defined(OPENSSL_NO_TLSEXT) && !defined(OPENSSL_NO_NEXTPROTONEG) + qRegisterMetaType<QNetworkReply *>(); // for QSignalSpy + qRegisterMetaType<QAuthenticator *>(); + + connect(&m_manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), + this, SLOT(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *))); +#else + QSKIP("Qt built withouth OpenSSL, or the OpenSSL version is too old"); +#endif // defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) ... +} + +tst_Spdy::~tst_Spdy() +{ +} + +void tst_Spdy::settingsAndNegotiation_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::addColumn<bool>("setAttribute"); + QTest::addColumn<bool>("enabled"); + QTest::addColumn<QByteArray>("expectedProtocol"); + QTest::addColumn<QByteArray>("expectedContent"); + + QTest::newRow("default-settings") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/cgi-bin/echo.cgi?1") + << false << false << QByteArray() + << QByteArray("1"); + + QTest::newRow("http-url") << QUrl("http://" + QtNetworkSettings::serverName() + + "/qtest/cgi-bin/echo.cgi?1") + << true << true << QByteArray() + << QByteArray("1"); + + QTest::newRow("spdy-disabled") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/cgi-bin/echo.cgi?1") + << true << false << QByteArray() + << QByteArray("1"); + +#ifndef QT_NO_OPENSSL + QTest::newRow("spdy-enabled") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/cgi-bin/echo.cgi?1") + << true << true << QByteArray(QSslConfiguration::NextProtocolSpdy3_0) + << QByteArray("1"); +#endif // QT_NO_OPENSSL +} + +void tst_Spdy::settingsAndNegotiation() +{ + QFETCH(QUrl, url); + QFETCH(bool, setAttribute); + QFETCH(bool, enabled); + + QNetworkRequest request(url); + + if (setAttribute) { + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, QVariant(enabled)); + } + + QNetworkReply *reply = m_manager.get(request); + reply->ignoreSslErrors(); + QSignalSpy metaDataChangedSpy(reply, SIGNAL(metaDataChanged())); + QSignalSpy readyReadSpy(reply, SIGNAL(readyRead())); + QSignalSpy finishedSpy(reply, SIGNAL(finished())); + + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QFETCH(QByteArray, expectedProtocol); + +#ifndef QT_NO_OPENSSL + bool expectedSpdyUsed = (expectedProtocol == QSslConfiguration::NextProtocolSpdy3_0) + ? true : false; + QCOMPARE(reply->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), expectedSpdyUsed); +#endif // QT_NO_OPENSSL + + QCOMPARE(metaDataChangedSpy.count(), 1); + QCOMPARE(finishedSpy.count(), 1); + + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QCOMPARE(statusCode, 200); + + QByteArray content = reply->readAll(); + + QFETCH(QByteArray, expectedContent); + QCOMPARE(expectedContent, content); + +#ifndef QT_NO_OPENSSL + QSslConfiguration::NextProtocolNegotiationStatus expectedStatus = + (expectedProtocol.isEmpty()) + ? QSslConfiguration::NextProtocolNegotiationNone + : QSslConfiguration::NextProtocolNegotiationNegotiated; + QCOMPARE(reply->sslConfiguration().nextProtocolNegotiationStatus(), + expectedStatus); + + QCOMPARE(reply->sslConfiguration().nextNegotiatedProtocol(), expectedProtocol); +#endif // QT_NO_OPENSSL +} + +void tst_Spdy::proxyAuthenticationRequired(const QNetworkProxy &/*proxy*/, + QAuthenticator *authenticator) +{ + authenticator->setUser("qsockstest"); + authenticator->setPassword("password"); +} + +void tst_Spdy::download_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::addColumn<QString>("fileName"); + QTest::addColumn<QNetworkProxy>("proxy"); + + QTest::newRow("mediumfile") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/rfc3252.txt") + << QFINDTESTDATA("../qnetworkreply/rfc3252.txt") + << QNetworkProxy(); + + QHostInfo hostInfo = QHostInfo::fromName(QtNetworkSettings::serverName()); + QString proxyserver = hostInfo.addresses().first().toString(); + + QTest::newRow("mediumfile-http-proxy") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/rfc3252.txt") + << QFINDTESTDATA("../qnetworkreply/rfc3252.txt") + << QNetworkProxy(QNetworkProxy::HttpProxy, proxyserver, 3128); + + QTest::newRow("mediumfile-http-proxy-auth") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/rfc3252.txt") + << QFINDTESTDATA("../qnetworkreply/rfc3252.txt") + << QNetworkProxy(QNetworkProxy::HttpProxy, + proxyserver, 3129); + + QTest::newRow("mediumfile-socks-proxy") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/rfc3252.txt") + << QFINDTESTDATA("../qnetworkreply/rfc3252.txt") + << QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyserver, 1080); + + QTest::newRow("mediumfile-socks-proxy-auth") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/rfc3252.txt") + << QFINDTESTDATA("../qnetworkreply/rfc3252.txt") + << QNetworkProxy(QNetworkProxy::Socks5Proxy, + proxyserver, 1081); + + QTest::newRow("bigfile") << QUrl("https://" + QtNetworkSettings::serverName() + + "/qtest/bigfile") + << QFINDTESTDATA("../qnetworkreply/bigfile") + << QNetworkProxy(); +} + +void tst_Spdy::download() +{ + QFETCH(QUrl, url); + QFETCH(QString, fileName); + QFETCH(QNetworkProxy, proxy); + + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + + if (proxy.type() != QNetworkProxy::DefaultProxy) { + m_manager.setProxy(proxy); + } + QNetworkReply *reply = m_manager.get(request); + reply->ignoreSslErrors(); + QSignalSpy metaDataChangedSpy(reply, SIGNAL(metaDataChanged())); + QSignalSpy downloadProgressSpy(reply, SIGNAL(downloadProgress(qint64, qint64))); + QSignalSpy readyReadSpy(reply, SIGNAL(readyRead())); + QSignalSpy finishedSpy(reply, SIGNAL(finished())); + + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + QSignalSpy proxyAuthRequiredSpy(&m_manager, SIGNAL( + proxyAuthenticationRequired(const QNetworkProxy &, + QAuthenticator *))); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(finishedManagerSpy.count(), 1); + QCOMPARE(metaDataChangedSpy.count(), 1); + QCOMPARE(finishedSpy.count(), 1); + QVERIFY(downloadProgressSpy.count() > 0); + QVERIFY(readyReadSpy.count() > 0); + + QVERIFY(proxyAuthRequiredSpy.count() <= 1); + + QCOMPARE(reply->error(), QNetworkReply::NoError); + QCOMPARE(reply->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), true); + QCOMPARE(reply->attribute(QNetworkRequest::ConnectionEncryptedAttribute).toBool(), true); + QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + QFile file(fileName); + QVERIFY(file.open(QIODevice::ReadOnly)); + + qint64 contentLength = reply->header(QNetworkRequest::ContentLengthHeader).toLongLong(); + qint64 expectedContentLength = file.bytesAvailable(); + QCOMPARE(contentLength, expectedContentLength); + + QByteArray expectedContent = file.readAll(); + QByteArray content = reply->readAll(); + QCOMPARE(content, expectedContent); + + reply->deleteLater(); + m_manager.setProxy(QNetworkProxy()); // reset +} + +void tst_Spdy::headerFields() +{ + QUrl url(QUrl("https://" + QtNetworkSettings::serverName())); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + + QNetworkReply *reply = m_manager.get(request); + reply->ignoreSslErrors(); + + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(reply->rawHeader("Content-Type"), QByteArray("text/html")); + QVERIFY(reply->rawHeader("Content-Length").toInt() > 0); + QVERIFY(reply->rawHeader("server").contains("Apache")); + + QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray(), QByteArray("text/html")); + QVERIFY(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong() > 0); + QVERIFY(reply->header(QNetworkRequest::LastModifiedHeader).toDateTime().isValid()); + QVERIFY(reply->header(QNetworkRequest::ServerHeader).toByteArray().contains("Apache")); +} + +static inline QByteArray md5sum(const QByteArray &data) +{ + return QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex().append('\n'); +} + +void tst_Spdy::upload_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::addColumn<QByteArray>("data"); + QTest::addColumn<QByteArray>("uploadMethod"); + QTest::addColumn<QObject *>("uploadObject"); + QTest::addColumn<QByteArray>("md5sum"); + QTest::addColumn<QNetworkProxy>("proxy"); + + + // 1. test uploading of byte arrays + + QUrl md5Url("https://" + QtNetworkSettings::serverName() + "/qtest/cgi-bin/md5sum.cgi"); + + QByteArray data; + data = ""; + QObject *dummyObject = 0; + QTest::newRow("empty") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = "This is a normal message."; + QTest::newRow("generic") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = "This is a message to show that Qt rocks!\r\n\n"; + QTest::newRow("small") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = QByteArray("abcd\0\1\2\abcd",12); + QTest::newRow("with-nul") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = QByteArray(4097, '\4'); + QTest::newRow("4k+1") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data)<< QNetworkProxy(); + + QHostInfo hostInfo = QHostInfo::fromName(QtNetworkSettings::serverName()); + QString proxyserver = hostInfo.addresses().first().toString(); + + QTest::newRow("4k+1-with-http-proxy") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) + << QNetworkProxy(QNetworkProxy::HttpProxy, proxyserver, 3128); + + QTest::newRow("4k+1-with-http-proxy-auth") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) + << QNetworkProxy(QNetworkProxy::HttpProxy, + proxyserver, 3129); + + QTest::newRow("4k+1-with-socks-proxy") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) + << QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyserver, 1080); + + QTest::newRow("4k+1-with-socks-proxy-auth") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) + << QNetworkProxy(QNetworkProxy::Socks5Proxy, + proxyserver, 1081); + + data = QByteArray(128*1024+1, '\177'); + QTest::newRow("128k+1") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = QByteArray(128*1024+1, '\177'); + QTest::newRow("128k+1-put") << md5Url << data << QByteArray("PUT") << dummyObject + << md5sum(data) << QNetworkProxy(); + + data = QByteArray(2*1024*1024+1, '\177'); + QTest::newRow("2MB+1") << md5Url << data << QByteArray("POST") << dummyObject + << md5sum(data) << QNetworkProxy(); + + + // 2. test uploading of files + + QFile *file = new QFile(QFINDTESTDATA("../qnetworkreply/rfc3252.txt")); + file->open(QIODevice::ReadOnly); + QTest::newRow("file-26K") << md5Url << QByteArray() << QByteArray("POST") + << static_cast<QObject *>(file) + << QByteArray("b3e32ac459b99d3f59318f3ac31e4bee\n") << QNetworkProxy(); + + QFile *file2 = new QFile(QFINDTESTDATA("../qnetworkreply/image1.jpg")); + file2->open(QIODevice::ReadOnly); + QTest::newRow("file-1MB") << md5Url << QByteArray() << QByteArray("POST") + << static_cast<QObject *>(file2) + << QByteArray("87ef3bb319b004ba9e5e9c9fa713776e\n") << QNetworkProxy(); + + + // 3. test uploading of multipart + + QUrl multiPartUrl("https://" + QtNetworkSettings::serverName() + "/qtest/cgi-bin/multipart.cgi"); + + QHttpPart imagePart31; + imagePart31.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + imagePart31.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"testImage1\"")); + imagePart31.setRawHeader("Content-Location", "http://my.test.location.tld"); + imagePart31.setRawHeader("Content-ID", "my@id.tld"); + QFile *file31 = new QFile(QFINDTESTDATA("../qnetworkreply/image1.jpg")); + file31->open(QIODevice::ReadOnly); + imagePart31.setBodyDevice(file31); + QHttpMultiPart *imageMultiPart3 = new QHttpMultiPart(QHttpMultiPart::FormDataType); + imageMultiPart3->append(imagePart31); + file31->setParent(imageMultiPart3); + QHttpPart imagePart32; + imagePart32.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + imagePart32.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"testImage2\"")); + QFile *file32 = new QFile(QFINDTESTDATA("../qnetworkreply/image2.jpg")); + file32->open(QIODevice::ReadOnly); + imagePart32.setBodyDevice(file31); // check that resetting works + imagePart32.setBodyDevice(file32); + imageMultiPart3->append(imagePart32); + file32->setParent(imageMultiPart3); + QHttpPart imagePart33; + imagePart33.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + imagePart33.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"testImage3\"")); + QFile *file33 = new QFile(QFINDTESTDATA("../qnetworkreply/image3.jpg")); + file33->open(QIODevice::ReadOnly); + imagePart33.setBodyDevice(file33); + imageMultiPart3->append(imagePart33); + file33->setParent(imageMultiPart3); + QByteArray expectedData = "content type: multipart/form-data; boundary=\"" + + imageMultiPart3->boundary(); + expectedData.append("\"\nkey: testImage1, value: 87ef3bb319b004ba9e5e9c9fa713776e\n" + "key: testImage2, value: 483761b893f7fb1bd2414344cd1f3dfb\n" + "key: testImage3, value: ab0eb6fd4fcf8b4436254870b4513033\n"); + + QTest::newRow("multipart-3images") << multiPartUrl << QByteArray() << QByteArray("POST") + << static_cast<QObject *>(imageMultiPart3) << expectedData + << QNetworkProxy(); +} + +void tst_Spdy::upload() +{ + QFETCH(QUrl, url); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + + QFETCH(QByteArray, data); + QFETCH(QByteArray, uploadMethod); + QFETCH(QObject *, uploadObject); + QFETCH(QNetworkProxy, proxy); + + if (proxy.type() != QNetworkProxy::DefaultProxy) { + m_manager.setProxy(proxy); + } + + QNetworkReply *reply; + QHttpMultiPart *multiPart = 0; + + if (uploadObject) { + // upload via device + if (QIODevice *device = qobject_cast<QIODevice *>(uploadObject)) { + reply = m_manager.post(request, device); + } else if ((multiPart = qobject_cast<QHttpMultiPart *>(uploadObject))) { + reply = m_manager.post(request, multiPart); + } else { + QFAIL("got unknown upload device"); + } + } else { + // upload via byte array + if (uploadMethod == "PUT") { + reply = m_manager.put(request, data); + } else { + reply = m_manager.post(request, data); + } + } + + reply->ignoreSslErrors(); + QSignalSpy metaDataChangedSpy(reply, SIGNAL(metaDataChanged())); + QSignalSpy uploadProgressSpy(reply, SIGNAL(uploadProgress(qint64, qint64))); + QSignalSpy readyReadSpy(reply, SIGNAL(readyRead())); + QSignalSpy finishedSpy(reply, SIGNAL(finished())); + + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + + QTestEventLoop::instance().enterLoop(20); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(finishedManagerSpy.count(), 1); + QCOMPARE(metaDataChangedSpy.count(), 1); + QCOMPARE(finishedSpy.count(), 1); + QVERIFY(uploadProgressSpy.count() > 0); + QVERIFY(readyReadSpy.count() > 0); + + QCOMPARE(reply->error(), QNetworkReply::NoError); + QCOMPARE(reply->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), true); + QCOMPARE(reply->attribute(QNetworkRequest::ConnectionEncryptedAttribute).toBool(), true); + QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + qint64 contentLength = reply->header(QNetworkRequest::ContentLengthHeader).toLongLong(); + if (!multiPart) // script to test multiparts does not return a content length + QCOMPARE(contentLength, 33); // 33 bytes for md5 sums (including new line) + + QFETCH(QByteArray, md5sum); + QByteArray content = reply->readAll(); + QCOMPARE(content, md5sum); + + reply->deleteLater(); + if (uploadObject) + uploadObject->deleteLater(); + + m_manager.setProxy(QNetworkProxy()); // reset +} + +void tst_Spdy::errors_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::addColumn<QNetworkProxy>("proxy"); + QTest::addColumn<bool>("ignoreSslErrors"); + QTest::addColumn<int>("expectedReplyError"); + + QTest::newRow("http-404") << QUrl("https://" + QtNetworkSettings::serverName() + "/non-existent-url") + << QNetworkProxy() << true << int(QNetworkReply::ContentNotFoundError); + + QTest::newRow("ssl-errors") << QUrl("https://" + QtNetworkSettings::serverName()) + << QNetworkProxy() << false << int(QNetworkReply::SslHandshakeFailedError); + + QTest::newRow("host-not-found") << QUrl("https://this-host-does-not.exist") + << QNetworkProxy() + << true << int(QNetworkReply::HostNotFoundError); + + QTest::newRow("proxy-not-found") << QUrl("https://" + QtNetworkSettings::serverName()) + << QNetworkProxy(QNetworkProxy::HttpProxy, + "https://this-host-does-not.exist", 3128) + << true << int(QNetworkReply::HostNotFoundError); + + QHostInfo hostInfo = QHostInfo::fromName(QtNetworkSettings::serverName()); + QString proxyserver = hostInfo.addresses().first().toString(); + + QTest::newRow("proxy-unavailable") << QUrl("https://" + QtNetworkSettings::serverName()) + << QNetworkProxy(QNetworkProxy::HttpProxy, proxyserver, 10) + << true << int(QNetworkReply::UnknownNetworkError); + + QTest::newRow("no-proxy-credentials") << QUrl("https://" + QtNetworkSettings::serverName()) + << QNetworkProxy(QNetworkProxy::HttpProxy, proxyserver, 3129) + << true << int(QNetworkReply::ProxyAuthenticationRequiredError); +} + +void tst_Spdy::errors() +{ + QFETCH(QUrl, url); + QFETCH(QNetworkProxy, proxy); + QFETCH(bool, ignoreSslErrors); + QFETCH(int, expectedReplyError); + + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + + disconnect(&m_manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), + 0, 0); + if (proxy.type() != QNetworkProxy::DefaultProxy) { + m_manager.setProxy(proxy); + } + QNetworkReply *reply = m_manager.get(request); + if (ignoreSslErrors) + reply->ignoreSslErrors(); + QSignalSpy finishedSpy(reply, SIGNAL(finished())); + QSignalSpy errorSpy(reply, SIGNAL(error(QNetworkReply::NetworkError))); + + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(errorSpy.count(), 1); + + QCOMPARE(reply->error(), static_cast<QNetworkReply::NetworkError>(expectedReplyError)); + + m_manager.setProxy(QNetworkProxy()); // reset + m_manager.clearAccessCache(); // e.g. to get an SSL error we need a new connection + connect(&m_manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), + this, SLOT(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)), + Qt::UniqueConnection); // reset +} + +void tst_Spdy::multipleRequests_data() +{ + QTest::addColumn<QList<QUrl> >("urls"); + + QString baseUrl = "https://" + QtNetworkSettings::serverName() + "/qtest/cgi-bin/echo.cgi?"; + QList<QUrl> urls; + for (int a = 1; a <= 50; ++a) + urls.append(QUrl(baseUrl + QLatin1String(QByteArray::number(a)))); + + QTest::newRow("one-request") << urls.mid(0, 1); + QTest::newRow("two-requests") << urls.mid(0, 2); + QTest::newRow("ten-requests") << urls.mid(0, 10); + QTest::newRow("twenty-requests") << urls.mid(0, 20); + QTest::newRow("fifty-requests") << urls; +} + +void tst_Spdy::multipleRequestsFinishedSlot() +{ + m_multipleRepliesFinishedCount++; + if (m_multipleRepliesFinishedCount == m_multipleRequestsCount) + QTestEventLoop::instance().exitLoop(); +} + +void tst_Spdy::multipleRequests() +{ + QFETCH(QList<QUrl>, urls); + m_multipleRequestsCount = urls.count(); + m_multipleRepliesFinishedCount = 0; + + QList<QNetworkReply *> replies; + QList<QSignalSpy *> metaDataChangedSpies; + QList<QSignalSpy *> readyReadSpies; + QList<QSignalSpy *> finishedSpies; + + foreach (const QUrl &url, urls) { + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + QNetworkReply *reply = m_manager.get(request); + replies.append(reply); + reply->ignoreSslErrors(); + QObject::connect(reply, SIGNAL(finished()), this, SLOT(multipleRequestsFinishedSlot())); + QSignalSpy *metaDataChangedSpy = new QSignalSpy(reply, SIGNAL(metaDataChanged())); + metaDataChangedSpies << metaDataChangedSpy; + QSignalSpy *readyReadSpy = new QSignalSpy(reply, SIGNAL(readyRead())); + readyReadSpies << readyReadSpy; + QSignalSpy *finishedSpy = new QSignalSpy(reply, SIGNAL(finished())); + finishedSpies << finishedSpy; + } + + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(finishedManagerSpy.count(), m_multipleRequestsCount); + + for (int a = 0; a < replies.count(); ++a) { + +#ifndef QT_NO_OPENSSL + QCOMPARE(replies.at(a)->sslConfiguration().nextProtocolNegotiationStatus(), + QSslConfiguration::NextProtocolNegotiationNegotiated); + QCOMPARE(replies.at(a)->sslConfiguration().nextNegotiatedProtocol(), + QByteArray(QSslConfiguration::NextProtocolSpdy3_0)); +#endif // QT_NO_OPENSSL + + QCOMPARE(replies.at(a)->error(), QNetworkReply::NoError); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), true); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::ConnectionEncryptedAttribute).toBool(), true); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + // using the echo script, a request to "echo.cgi?1" will return a body of "1" + QByteArray expectedContent = replies.at(a)->url().query().toUtf8(); + QByteArray content = replies.at(a)->readAll(); + QCOMPARE(expectedContent, content); + + QCOMPARE(metaDataChangedSpies.at(a)->count(), 1); + metaDataChangedSpies.at(a)->deleteLater(); + + QCOMPARE(finishedSpies.at(a)->count(), 1); + finishedSpies.at(a)->deleteLater(); + + QVERIFY(readyReadSpies.at(a)->count() > 0); + readyReadSpies.at(a)->deleteLater(); + + replies.at(a)->deleteLater(); + } +} + +QTEST_MAIN(tst_Spdy) + +#include "tst_spdy.moc" diff --git a/tests/benchmarks/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/benchmarks/network/access/qnetworkreply/tst_qnetworkreply.cpp index 55376d5a79..3b8565a0ec 100644 --- a/tests/benchmarks/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/benchmarks/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -52,6 +52,7 @@ #ifdef QT_BUILD_INTERNAL #include <QtNetwork/private/qhostinfo_p.h> +#include <QtNetwork/private/qsslsocket_openssl_p.h> #endif Q_DECLARE_METATYPE(QSharedPointer<char>) @@ -552,10 +553,16 @@ void tst_qnetworkreply::echoPerformance() void tst_qnetworkreply::preConnectEncrypted() { + QFETCH(int, sleepTime); + QFETCH(QSslConfiguration, sslConfiguration); + bool spdyEnabled = !sslConfiguration.isNull(); + QString hostName = QLatin1String("www.google.com"); QNetworkAccessManager manager; QNetworkRequest request(QUrl("https://" + hostName)); + if (spdyEnabled) + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); // make sure we have a full request including // DNS lookup, TCP and SSL handshakes @@ -581,8 +588,12 @@ void tst_qnetworkreply::preConnectEncrypted() manager.clearAccessCache(); // now try to make the connection beforehand - QFETCH(int, sleepTime); - manager.connectToHostEncrypted(hostName); + if (spdyEnabled) { + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + manager.connectToHostEncrypted(hostName, 443, sslConfiguration); + } else { + manager.connectToHostEncrypted(hostName); + } QTestEventLoop::instance().enterLoopMSecs(sleepTime); // now make another request and hopefully use the existing connection @@ -590,18 +601,42 @@ void tst_qnetworkreply::preConnectEncrypted() QNetworkReply *preConnectReply = normalResult.first; QVERIFY(!QTestEventLoop::instance().timeout()); QVERIFY(preConnectReply->error() == QNetworkReply::NoError); + bool spdyWasUsed = preConnectReply->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(); + QCOMPARE(spdyEnabled, spdyWasUsed); qint64 preConnectElapsed = preConnectResult.second; qDebug() << request.url().toString() << "full request:" << normalElapsed << "ms, pre-connect request:" << preConnectElapsed << "ms, difference:" << (normalElapsed - preConnectElapsed) << "ms"; } + #endif // !QT_NO_SSL void tst_qnetworkreply::preConnectEncrypted_data() { +#ifndef QT_NO_OPENSSL QTest::addColumn<int>("sleepTime"); - QTest::newRow("2secs") << 2000; // to start a new request after preconnecting is done - QTest::newRow("100ms") << 100; // to start a new request while preconnecting is in-flight + QTest::addColumn<QSslConfiguration>("sslConfiguration"); + + // start a new normal request after preconnecting is done + QTest::newRow("HTTPS-2secs") << 2000 << QSslConfiguration(); + + // start a new normal request while preconnecting is in-flight + QTest::newRow("HTTPS-100ms") << 100 << QSslConfiguration(); + + QSslConfiguration spdySslConf = QSslConfiguration::defaultConfiguration(); + QList<QByteArray> nextProtocols = QList<QByteArray>() + << QSslConfiguration::NextProtocolSpdy3_0 + << QSslConfiguration::NextProtocolHttp1_1; + spdySslConf.setAllowedNextProtocols(nextProtocols); + +#if defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) && OPENSSL_VERSION_NUMBER >= 0x1000100fL && !defined(OPENSSL_NO_TLSEXT) && !defined(OPENSSL_NO_NEXTPROTONEG) + // start a new SPDY request while preconnecting is done + QTest::newRow("SPDY-2secs") << 2000 << spdySslConf; + + // start a new SPDY request while preconnecting is in-flight + QTest::newRow("SPDY-100ms") << 100 << spdySslConf; +#endif // defined (QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) ... +#endif // QT_NO_OPENSSL } void tst_qnetworkreply::downloadPerformance() diff --git a/tests/manual/qnetworkreply/main.cpp b/tests/manual/qnetworkreply/main.cpp index fe667e92a0..ff96f2598d 100644 --- a/tests/manual/qnetworkreply/main.cpp +++ b/tests/manual/qnetworkreply/main.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 test suite of the Qt Toolkit. @@ -53,6 +54,7 @@ #if defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) #include "private/qsslsocket_p.h" +#include <QtNetwork/private/qsslsocket_openssl_p.h> #endif #define BANDWIDTH_LIMIT_BYTES (1024*100) @@ -68,8 +70,16 @@ private slots: void setSslConfiguration_data(); void setSslConfiguration(); void uploadToFacebook(); + void spdy_data(); + void spdy(); + void spdyMultipleRequestsPerHost(); + +protected slots: + void spdyReplyFinished(); // only used by spdyMultipleRequestsPerHost test + private: QHttpMultiPart *createFacebookMultiPart(const QByteArray &accessToken); + QNetworkAccessManager m_manager; }; QNetworkReply *reply; @@ -104,6 +114,7 @@ protected: void tst_qnetworkreply::initTestCase() { + qRegisterMetaType<QNetworkReply *>(); // for QSignalSpy QVERIFY(QtNetworkSettings::verifyTestNetworkSettings()); } @@ -284,6 +295,215 @@ void tst_qnetworkreply::uploadToFacebook() } } +void tst_qnetworkreply::spdy_data() +{ + QTest::addColumn<QString>("host"); + QTest::addColumn<bool>("setAttribute"); + QTest::addColumn<bool>("enabled"); + QTest::addColumn<QByteArray>("expectedProtocol"); + + QList<QString> hosts = QList<QString>() + << QStringLiteral("www.google.com") // sends SPDY and 30x redirect + << QStringLiteral("www.google.de") // sends SPDY and 200 OK + << QStringLiteral("mail.google.com") // sends SPDY and 200 OK + << QStringLiteral("www.youtube.com") // sends SPDY and 200 OK + << QStringLiteral("www.dropbox.com") // no SPDY, but NPN which selects HTTP + << QStringLiteral("www.facebook.com") // sends SPDY and 200 OK + << QStringLiteral("graph.facebook.com") // sends SPDY and 200 OK + << QStringLiteral("www.twitter.com") // sends SPDY and 30x redirect + << QStringLiteral("twitter.com") // sends SPDY and 200 OK + << QStringLiteral("api.twitter.com"); // sends SPDY and 200 OK + + foreach (const QString &host, hosts) { + QByteArray tag = host.toLocal8Bit(); + tag.append("-not-used"); + QTest::newRow(tag) + << QStringLiteral("https://") + host + << false + << false + << QByteArray(); + + tag = host.toLocal8Bit(); + tag.append("-disabled"); + QTest::newRow(tag) + << QStringLiteral("https://") + host + << true + << false + << QByteArray(); + + if (host != QStringLiteral("api.twitter.com")) { // they don't offer an API over HTTP + tag = host.toLocal8Bit(); + tag.append("-no-https-url"); + QTest::newRow(tag) + << QStringLiteral("http://") + host + << true + << true + << QByteArray(); + } + +#ifndef QT_NO_OPENSSL + tag = host.toLocal8Bit(); + tag.append("-enabled"); + QTest::newRow(tag) + << QStringLiteral("https://") + host + << true + << true + << (host == QStringLiteral("www.dropbox.com") + ? QByteArray(QSslConfiguration::NextProtocolHttp1_1) + : QByteArray(QSslConfiguration::NextProtocolSpdy3_0)); +#endif // QT_NO_OPENSSL + } +} + +void tst_qnetworkreply::spdy() +{ +#if defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) && OPENSSL_VERSION_NUMBER >= 0x1000100fL && !defined(OPENSSL_NO_TLSEXT) && !defined(OPENSSL_NO_NEXTPROTONEG) + + m_manager.clearAccessCache(); + + QFETCH(QString, host); + QUrl url(host); + QNetworkRequest request(url); + + QFETCH(bool, setAttribute); + QFETCH(bool, enabled); + if (setAttribute) { + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, QVariant(enabled)); + } + + QNetworkReply *reply = m_manager.get(request); + QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + + QSignalSpy metaDataChangedSpy(reply, SIGNAL(metaDataChanged())); + QSignalSpy readyReadSpy(reply, SIGNAL(readyRead())); + QSignalSpy finishedSpy(reply, SIGNAL(finished())); + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QFETCH(QByteArray, expectedProtocol); + + bool expectedSpdyUsed = (expectedProtocol == QSslConfiguration::NextProtocolSpdy3_0) + ? true : false; + QCOMPARE(reply->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), expectedSpdyUsed); + + QCOMPARE(metaDataChangedSpy.count(), 1); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedManagerSpy.count(), 1); + + QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl(); + QByteArray content = reply->readAll(); + + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QVERIFY(statusCode >= 200 && statusCode < 500); + if (statusCode == 200 || statusCode >= 400) { + QVERIFY(readyReadSpy.count() > 0); + QVERIFY(!content.isEmpty()); + } else if (statusCode >= 300 && statusCode < 400) { + QVERIFY(!redirectUrl.isEmpty()); + } + + QSslConfiguration::NextProtocolNegotiationStatus expectedStatus = + expectedProtocol.isNull() ? QSslConfiguration::NextProtocolNegotiationNone + : QSslConfiguration::NextProtocolNegotiationNegotiated; + QCOMPARE(reply->sslConfiguration().nextProtocolNegotiationStatus(), + expectedStatus); + + QCOMPARE(reply->sslConfiguration().nextNegotiatedProtocol(), expectedProtocol); +#else + QSKIP("Qt built withouth OpenSSL, or the OpenSSL version is too old"); +#endif // defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) ... +} + +void tst_qnetworkreply::spdyReplyFinished() +{ + static int finishedCount = 0; + finishedCount++; + + if (finishedCount == 12) + QTestEventLoop::instance().exitLoop(); +} + +void tst_qnetworkreply::spdyMultipleRequestsPerHost() +{ +#if defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) && OPENSSL_VERSION_NUMBER >= 0x1000100fL && !defined(OPENSSL_NO_TLSEXT) && !defined(OPENSSL_NO_NEXTPROTONEG) + + QList<QNetworkRequest> requests; + requests + << QNetworkRequest(QUrl("https://www.facebook.com")) + << QNetworkRequest(QUrl("https://www.facebook.com/images/fb_icon_325x325.png")) + + << QNetworkRequest(QUrl("https://www.google.de")) + << QNetworkRequest(QUrl("https://www.google.de/preferences?hl=de")) + << QNetworkRequest(QUrl("https://www.google.de/intl/de/policies/?fg=1")) + << QNetworkRequest(QUrl("https://www.google.de/intl/de/about.html?fg=1")) + << QNetworkRequest(QUrl("https://www.google.de/services/?fg=1")) + << QNetworkRequest(QUrl("https://www.google.de/intl/de/ads/?fg=1")) + + << QNetworkRequest(QUrl("https://i1.ytimg.com/li/tnHdj3df7iM/default.jpg")) + << QNetworkRequest(QUrl("https://i1.ytimg.com/li/7Dr1BKwqctY/default.jpg")) + << QNetworkRequest(QUrl("https://i1.ytimg.com/li/hfZhJdhTqX8/default.jpg")) + << QNetworkRequest(QUrl("https://i1.ytimg.com/vi/14Nprh8163I/hqdefault.jpg")) + ; + QList<QNetworkReply *> replies; + QList<QSignalSpy *> metaDataChangedSpies; + QList<QSignalSpy *> readyReadSpies; + QList<QSignalSpy *> finishedSpies; + + QSignalSpy finishedManagerSpy(&m_manager, SIGNAL(finished(QNetworkReply*))); + + foreach (QNetworkRequest request, requests) { + request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); + QNetworkReply *reply = m_manager.get(request); + QObject::connect(reply, SIGNAL(finished()), this, SLOT(spdyReplyFinished())); + replies << reply; + QSignalSpy *metaDataChangedSpy = new QSignalSpy(reply, SIGNAL(metaDataChanged())); + metaDataChangedSpies << metaDataChangedSpy; + QSignalSpy *readyReadSpy = new QSignalSpy(reply, SIGNAL(readyRead())); + readyReadSpies << readyReadSpy; + QSignalSpy *finishedSpy = new QSignalSpy(reply, SIGNAL(finished())); + finishedSpies << finishedSpy; + } + + QCOMPARE(requests.count(), replies.count()); + + QTestEventLoop::instance().enterLoop(15); + QVERIFY(!QTestEventLoop::instance().timeout()); + + QCOMPARE(finishedManagerSpy.count(), requests.count()); + + for (int a = 0; a < replies.count(); ++a) { + + QCOMPARE(replies.at(a)->sslConfiguration().nextProtocolNegotiationStatus(), + QSslConfiguration::NextProtocolNegotiationNegotiated); + QCOMPARE(replies.at(a)->sslConfiguration().nextNegotiatedProtocol(), + QByteArray(QSslConfiguration::NextProtocolSpdy3_0)); + + QCOMPARE(replies.at(a)->error(), QNetworkReply::NoError); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::SpdyWasUsedAttribute).toBool(), true); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::ConnectionEncryptedAttribute).toBool(), true); + QCOMPARE(replies.at(a)->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + QByteArray content = replies.at(a)->readAll(); + QVERIFY(content.count() > 0); + + QCOMPARE(metaDataChangedSpies.at(a)->count(), 1); + metaDataChangedSpies.at(a)->deleteLater(); + + QCOMPARE(finishedSpies.at(a)->count(), 1); + finishedSpies.at(a)->deleteLater(); + + QVERIFY(readyReadSpies.at(a)->count() > 0); + readyReadSpies.at(a)->deleteLater(); + + replies.at(a)->deleteLater(); + } +#else + QSKIP("Qt built withouth OpenSSL, or the OpenSSL version is too old"); +#endif // defined(QT_BUILD_INTERNAL) && !defined(QT_NO_SSL) ... +} + QTEST_MAIN(tst_qnetworkreply) #include "main.moc" |