qt5base-lts/tests/manual/qnetworkreply/main.cpp
Peter Hartmann 1de244ea65 network: add support for the SPDY protocol
Currently the only supported SPDY version is 3.0.

The feature needs to be enabled explicitly via
QNetworkRequest::SpdyAllowedAttribute. Whether SPDY actually was used
can be determined via QNetworkRequest::SpdyWasUsedAttribute from a
QNetworkReply once it has been started (i.e. after the encrypted()
signal has been received). Whether SPDY can be used will be
determined during the SSL handshake through the TLS NPN extension
(see separate commit).

The following things from SPDY have not been enabled currently:
* server push is not implemented, it has never been seen in the wild;
  in that case we just reject a stream pushed by the server, which is
  legit.
* settings are not persisted across SPDY sessions. In practice this
  means that the server sends a small message upon session start
  telling us e.g. the number of concurrent connections.
* SSL client certificates are not supported.

Task-number: QTBUG-18714

[ChangeLog][QtNetwork] Added support for the SPDY protocol (version
3.0).

Change-Id: I81bbe0495c24ed84e9cf8af3a9dbd63ca1e93d0d
Reviewed-by: Richard J. Moore <rich@kde.org>
2014-02-19 21:44:15 +01:00

510 lines
20 KiB
C++

/****************************************************************************
**
** 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.
**
** $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$
**
****************************************************************************/
// This file contains benchmarks for QNetworkReply functions.
#include <QDebug>
#include <qtest.h>
#include <QtTest/QtTest>
#include <QtNetwork/qnetworkreply.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtNetwork/qnetworkaccessmanager.h>
#include <QtNetwork/qsslconfiguration.h>
#include <QtNetwork/qhttpmultipart.h>
#include <QtCore/QJsonDocument>
#include "../../auto/network-settings.h"
#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)
#define TIME_ESTIMATION_SECONDS (97)
class tst_qnetworkreply : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void limiting_data();
void limiting();
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;
class HttpReceiver : public QObject
{
Q_OBJECT
public slots:
void finishedSlot() {
quint64 bytesPerSec = (reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()) / (stopwatch.elapsed() / 1000.0);
qDebug() << "Finished HTTP(S) request with" << bytesPerSec << "bytes/sec";
QVERIFY(bytesPerSec < BANDWIDTH_LIMIT_BYTES*1.05);
QVERIFY(bytesPerSec > BANDWIDTH_LIMIT_BYTES*0.95);
timer->stop();
QTestEventLoop::instance().exitLoop();
}
void readyReadSlot() {
}
void timeoutSlot() {
reply->read(BANDWIDTH_LIMIT_BYTES).size();
}
void startTimer() {
stopwatch.start();
timer = new QTimer(this);
QObject::connect(timer, SIGNAL(timeout()), this, SLOT(timeoutSlot()));
timer->start(1000);
}
protected:
QTimer *timer;
QTime stopwatch;
};
void tst_qnetworkreply::initTestCase()
{
qRegisterMetaType<QNetworkReply *>(); // for QSignalSpy
QVERIFY(QtNetworkSettings::verifyTestNetworkSettings());
}
void tst_qnetworkreply::limiting_data()
{
QTest::addColumn<QUrl>("url");
QTest::newRow("HTTP") << QUrl("http://" + QtNetworkSettings::serverName() + "/mediumfile");
#ifndef QT_NO_SSL
QTest::newRow("HTTP+SSL") << QUrl("https://" + QtNetworkSettings::serverName() + "/mediumfile");
#endif
}
void tst_qnetworkreply::limiting()
{
HttpReceiver receiver;
QNetworkAccessManager manager;
QFETCH(QUrl, url);
QNetworkRequest req (url);
qDebug() << "Starting. This will take a while (around" << TIME_ESTIMATION_SECONDS << "sec).";
qDebug() << "Please check the actual bandwidth usage with a network monitor, e.g. the KDE";
qDebug() << "network plasma widget. It should be around" << BANDWIDTH_LIMIT_BYTES << "bytes/sec.";
reply = manager.get(req);
reply->ignoreSslErrors();
reply->setReadBufferSize(BANDWIDTH_LIMIT_BYTES);
QObject::connect(reply, SIGNAL(readyRead()), &receiver, SLOT(readyReadSlot()));
QObject::connect(reply, SIGNAL(finished()), &receiver, SLOT(finishedSlot()));
receiver.startTimer();
// event loop
QTestEventLoop::instance().enterLoop(TIME_ESTIMATION_SECONDS + 20);
QVERIFY(!QTestEventLoop::instance().timeout());
}
void tst_qnetworkreply::setSslConfiguration_data()
{
QTest::addColumn<QUrl>("url");
QTest::addColumn<bool>("works");
QTest::newRow("codereview.qt-project.org") << QUrl("https://codereview.qt-project.org") << true;
QTest::newRow("test-server") << QUrl("https://" + QtNetworkSettings::serverName() + "/") << false;
}
void tst_qnetworkreply::setSslConfiguration()
{
#ifdef QT_NO_SSL
QSKIP("SSL is not enabled.");
#else
QFETCH(QUrl, url);
QNetworkRequest request(url);
QSslConfiguration conf = request.sslConfiguration();
conf.setProtocol(QSsl::TlsV1_0); // TLS 1.0 will be used anyway, just make sure we change the configuration
request.setSslConfiguration(conf);
QNetworkAccessManager manager;
reply = manager.get(request);
QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop()));
QTestEventLoop::instance().enterLoop(15);
QVERIFY(!QTestEventLoop::instance().timeout());
#ifdef QT_BUILD_INTERNAL
QFETCH(bool, works);
bool rootCertLoadingAllowed = QSslSocketPrivate::rootCertOnDemandLoadingSupported();
#if defined(Q_OS_LINUX) || defined (Q_OS_BLACKBERRY)
QCOMPARE(rootCertLoadingAllowed, true);
#elif defined(Q_OS_MAC)
QCOMPARE(rootCertLoadingAllowed, false);
#endif // other platforms: undecided (Windows: depends on the version)
if (works) {
QCOMPARE(reply->error(), QNetworkReply::NoError);
} else {
QCOMPARE(reply->error(), QNetworkReply::SslHandshakeFailedError);
}
#endif
#endif // QT_NO_SSL
}
QHttpMultiPart *tst_qnetworkreply::createFacebookMultiPart(const QByteArray &accessToken)
{
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart accessTokenPart;
accessTokenPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"access_token\""));
accessTokenPart.setBody(accessToken);
multiPart->append(accessTokenPart);
QHttpPart batchPart;
batchPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"batch\""));
batchPart.setBody("["
" {"
" \"attached_files\" : \"image1\","
" \"body\" : \"message=&published=0\","
" \"method\" : \"POST\","
" \"relative_url\" : \"me/photos\""
" },"
" {"
" \"attached_files\" : \"image2\","
" \"body\" : \"message=&published=0\","
" \"method\" : \"POST\","
" \"relative_url\" : \"me/photos\""
" },"
" {"
" \"attached_files\" : \"image3\","
" \"body\" : \"message=&published=0\","
" \"method\" : \"POST\","
" \"relative_url\" : \"me/photos\""
" }"
"]");
multiPart->append(batchPart);
QHttpPart imagePart1;
imagePart1.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpg"));
imagePart1.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image1\"; filename=\"image1.jpg\""));
QFile *file1 = new QFile(QFINDTESTDATA("../../auto/network/access/qnetworkreply/image1.jpg"));
file1->open(QIODevice::ReadOnly);
imagePart1.setBodyDevice(file1);
file1->setParent(multiPart);
multiPart->append(imagePart1);
QHttpPart imagePart2;
imagePart2.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpg"));
imagePart2.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image2\"; filename=\"image2.jpg\""));
QFile *file2 = new QFile(QFINDTESTDATA("../../auto/network/access/qnetworkreply/image2.jpg"));
file2->open(QIODevice::ReadOnly);
imagePart2.setBodyDevice(file2);
file2->setParent(multiPart);
multiPart->append(imagePart2);
QHttpPart imagePart3;
imagePart3.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpg"));
imagePart3.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image3\"; filename=\"image3.jpg\""));
QFile *file3 = new QFile(QFINDTESTDATA("../../auto/network/access/qnetworkreply/image3.jpg"));
file3->open(QIODevice::ReadOnly);
imagePart3.setBodyDevice(file3);
file3->setParent(multiPart);
multiPart->append(imagePart3);
return multiPart;
}
void tst_qnetworkreply::uploadToFacebook()
{
QByteArray accessToken = qgetenv("QT_FACEBOOK_ACCESS_TOKEN");
if (accessToken.isEmpty())
QSKIP("This test requires the QT_FACEBOOK_ACCESS_TOKEN environment variable to be set. "
"Do something like 'export QT_FACEBOOK_ACCESS_TOKEN=MyAccessToken'");
QElapsedTimer timer;
QNetworkAccessManager manager;
QNetworkRequest request(QUrl("https://graph.facebook.com/me/photos"));
QHttpMultiPart *multiPart = createFacebookMultiPart(accessToken);
timer.start();
QNetworkReply *reply = manager.post(request, multiPart);
multiPart->setParent(reply);
QObject::connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop()));
QTestEventLoop::instance().enterLoop(120);
qint64 elapsed = timer.elapsed();
QVERIFY(!QTestEventLoop::instance().timeout());
qDebug() << "reply finished after" << elapsed / 1000.0 << "seconds";
QCOMPARE(reply->error(), QNetworkReply::NoError);
QByteArray content = reply->readAll();
QJsonDocument jsonDocument = QJsonDocument::fromJson(content);
QVERIFY(jsonDocument.isArray());
QJsonArray uploadStatuses = jsonDocument.array();
QVERIFY(uploadStatuses.size() > 0);
for (int a = 0; a < uploadStatuses.size(); a++) {
QJsonValue currentUploadStatus = uploadStatuses.at(a);
QVERIFY(currentUploadStatus.isObject());
QJsonObject statusObject = currentUploadStatus.toObject();
QJsonValue statusCode = statusObject.value(QLatin1String("code"));
QCOMPARE(statusCode.toVariant().toInt(), 200); // 200 OK
}
}
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"