Add a user-controlled auto-redirect policy

With this new policy, after emitting 'redirected', QNetworkReplyHttpImpl
waits for client code to decide if QNAM should follow this redirect or
not. The client can either allow this redirect by emitting 'redirectAllowed'
or abort the reply.

Task-number: QTPM-236
Change-Id: Ia04619f6bd1f0caa477833ae859b24033027b2e1
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
bb10
Timur Pocheptsov 2017-01-02 15:47:15 +01:00
parent 45f80a52c2
commit 231259c3d5
8 changed files with 107 additions and 12 deletions

View File

@ -567,6 +567,8 @@ QUrl QHttpNetworkConnectionPrivate::parseRedirectResponse(QAbstractSocket *socke
return QUrl();
}
break;
case QNetworkRequest::UserVerifiedRedirectsPolicy:
break;
default:
Q_ASSERT(!"Unexpected redirect policy");
}

View File

@ -304,6 +304,20 @@ QNetworkReplyPrivate::QNetworkReplyPrivate()
\sa QNetworkRequest::FollowRedirectsAttribute
*/
/*!
\fn void QNetworkReply::redirectAllowed()
\since 5.9
When client code handling the redirected() signal has verified the new URL,
it emits this signal to allow the redirect to go ahead. This protocol applies
to network requests whose redirects policy is set to
QNetworkRequest::UserVerifiedRedirectsPolicy.
\sa QNetworkRequest::UserVerifiedRedirectsPolicy
QNetworkAccessManager::setRedirectsPolicy(),
QNetworkRequest::RedirectsPolicyAttribute
*/
/*!
\fn void QNetworkReply::metaDataChanged()

View File

@ -163,6 +163,7 @@ Q_SIGNALS:
void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
#endif
void redirected(const QUrl &url);
void redirectAllowed();
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);

View File

@ -824,12 +824,16 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
QObject::connect(delegate, SIGNAL(redirected(QUrl,int,int)),
q, SLOT(onRedirected(QUrl,int,int)),
Qt::QueuedConnection);
QObject::connect(q, SIGNAL(redirectAllowed()), q, SLOT(followRedirect()),
Qt::QueuedConnection);
#ifndef QT_NO_SSL
QObject::connect(delegate, SIGNAL(sslConfigurationChanged(QSslConfiguration)),
q, SLOT(replySslConfigurationChanged(QSslConfiguration)),
Qt::QueuedConnection);
#endif
// Those need to report back, therefire BlockingQueuedConnection
// Those need to report back, therefore BlockingQueuedConnection
QObject::connect(delegate, SIGNAL(authenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
q, SLOT(httpAuthenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
Qt::BlockingQueuedConnection);
@ -1121,19 +1125,26 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt
if (httpRequest.isFollowRedirects()) // update the reply's url as it could've changed
url = redirectUrl;
QNetworkRequest redirectRequest = createRedirectRequest(originalRequest, redirectUrl, maxRedirectsRemaining);
redirectRequest = createRedirectRequest(originalRequest, redirectUrl, maxRedirectsRemaining);
operation = getRedirectOperation(operation, httpStatus);
if (httpRequest.redirectsPolicy() != QNetworkRequest::UserVerifiedRedirectsPolicy)
followRedirect();
emit q->redirected(redirectUrl);
}
void QNetworkReplyHttpImplPrivate::followRedirect()
{
Q_Q(QNetworkReplyHttpImpl);
cookedHeaders.clear();
if (managerPrivate->thread)
managerPrivate->thread->disconnect();
// Recurse
QMetaObject::invokeMethod(q, "start", Qt::QueuedConnection,
Q_ARG(QNetworkRequest, redirectRequest));
emit q->redirected(redirectUrl);
}
void QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode)

View File

@ -136,6 +136,7 @@ public:
Q_PRIVATE_SLOT(d_func(), void _q_cacheSaveDeviceAboutToClose())
Q_PRIVATE_SLOT(d_func(), void _q_metaDataChanged())
Q_PRIVATE_SLOT(d_func(), void onRedirected(const QUrl &, int, int))
Q_PRIVATE_SLOT(d_func(), void followRedirect())
#ifndef QT_NO_SSL
protected:
@ -212,6 +213,7 @@ public:
QSharedPointer<QRingBuffer> outgoingDataBuffer;
void emitReplyUploadProgress(qint64 bytesSent, qint64 bytesTotal); // dup?
void onRedirected(const QUrl &redirectUrl, int httpStatus, int maxRedirectsRemainig);
void followRedirect();
qint64 bytesUploaded;
@ -263,6 +265,7 @@ public:
QList<QSslError> pendingIgnoreSslErrorsList;
#endif
QNetworkRequest redirectRequest;
bool loadFromCacheIfAllowed(QHttpNetworkRequest &httpRequest);
void invalidateCache();

View File

@ -360,6 +360,16 @@ QT_BEGIN_NAMESPACE
Note, http://example.com and http://example.com:80
will fail with this policy (implicit/explicit ports
are considered to be a mismatch).
\value UserVerifiedRedirectsPolicy Client decides whether to follow each
redirect by handling the redirected()
signal, emitting redirectAllowed() on
the QNetworkReply object to allow
the redirect or aborting/finishing it to
reject the redirect. This can be used,
for example, to ask the user whether to
accept the redirect, or to decide
based on some app-specific configuration.
*/
class QNetworkRequestPrivate: public QSharedData, public QNetworkHeadersPrivate

View File

@ -116,7 +116,8 @@ public:
enum RedirectsPolicy {
ManualRedirectsPolicy,
NoLessSafeRedirectsPolicy,
SameOriginRedirectsPolicy
SameOriginRedirectsPolicy,
UserVerifiedRedirectsPolicy
};

View File

@ -134,6 +134,8 @@ class tst_QNetworkReply: public QObject
return s;
}
static const QByteArray httpEmpty200Response;
QEventLoop *loop;
enum RunSimpleRequestReturn { Timeout = 0, Success, Failure };
int returnCode;
@ -480,6 +482,8 @@ private Q_SLOTS:
void ioHttpRedirectPolicy();
void ioHttpRedirectPolicyErrors_data();
void ioHttpRedirectPolicyErrors();
void ioHttpUserVerifiedRedirect_data();
void ioHttpUserVerifiedRedirect();
#ifndef QT_NO_SSL
void putWithServerClosingConnectionImmediately();
#endif
@ -493,6 +497,8 @@ private:
bool notEnoughDataForFastSender;
};
const QByteArray tst_QNetworkReply::httpEmpty200Response =
"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
bool tst_QNetworkReply::seedCreated = false;
#define RUN_REQUEST(call) \
@ -8034,10 +8040,9 @@ void tst_QNetworkReply::putWithRateLimiting()
void tst_QNetworkReply::ioHttpSingleRedirect()
{
QUrl localhost = QUrl("http://localhost");
QByteArray http200Reply = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
// Setup server to which the second server will redirect to
MiniHttpServer server2(http200Reply);
MiniHttpServer server2(httpEmpty200Response);
QUrl redirectUrl = QUrl(localhost);
redirectUrl.setPort(server2.serverPort());
@ -8079,11 +8084,9 @@ void tst_QNetworkReply::ioHttpChangeMaxRedirects()
{
QUrl localhost = QUrl("http://localhost");
QByteArray http200Reply = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
MiniHttpServer server1("");
MiniHttpServer server2("");
MiniHttpServer server3(http200Reply);
MiniHttpServer server3(httpEmpty200Response);
QUrl server2Url(localhost);
server2Url.setPort(server2.serverPort());
@ -8227,7 +8230,7 @@ void tst_QNetworkReply::ioHttpRedirectPolicy()
"http://localhost"));
url.setPort(redirectServer.serverPort());
redirectServer.responses.push_back("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
redirectServer.responses.push_back(httpEmpty200Response);
redirectServer.responses.push_back(tempRedirectReplyStr().arg(QString(url.toEncoded())).toLatin1());
// This is the default one we preserve between tests.
@ -8332,6 +8335,56 @@ void tst_QNetworkReply::ioHttpRedirectPolicyErrors()
QCOMPARE(reply->error(), expectedError);
}
void tst_QNetworkReply::ioHttpUserVerifiedRedirect_data()
{
QTest::addColumn<bool>("followRedirect");
QTest::addColumn<int>("statusCode");
QTest::newRow("allow-redirect") << true << 200;
QTest::newRow("reject-redirect") << false << 307;
}
void tst_QNetworkReply::ioHttpUserVerifiedRedirect()
{
QFETCH(const bool, followRedirect);
QFETCH(const int, statusCode);
// Setup HTTP server.
MiniHttpServer target(httpEmpty200Response, false);
QUrl url("http://localhost");
url.setPort(target.serverPort());
MiniHttpServer redirectServer("", false);
redirectServer.setDataToTransmit(tempRedirectReplyStr().arg(QString(url.toEncoded())).toLatin1());
url.setPort(redirectServer.serverPort());
QCOMPARE(manager.redirectsPolicy(), QNetworkRequest::ManualRedirectsPolicy);
manager.setRedirectsPolicy(QNetworkRequest::UserVerifiedRedirectsPolicy);
QCOMPARE(manager.redirectsPolicy(), QNetworkRequest::UserVerifiedRedirectsPolicy);
QNetworkReplyPtr reply(manager.get(QNetworkRequest(url)));
reply->connect(reply.data(), &QNetworkReply::redirected,
[&](const QUrl &redirectUrl) {
qDebug() << "redirect to:" << redirectUrl;
if (followRedirect) {
qDebug() << "confirmed.";
emit reply->redirectAllowed();
} else{
qDebug() << "rejected.";
emit reply->abort();
}
});
// Before any test failed, reset the policy to default:
manager.setRedirectsPolicy(QNetworkRequest::ManualRedirectsPolicy);
QCOMPARE(manager.redirectsPolicy(), QNetworkRequest::ManualRedirectsPolicy);
QSignalSpy finishedSpy(reply.data(), SIGNAL(finished()));
waitForFinish(reply);
QCOMPARE(finishedSpy.count(), 1);
QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), statusCode);
}
#ifndef QT_NO_SSL
class PutWithServerClosingConnectionImmediatelyHandler: public QObject