Add redirects policy to QNetworkAccessManager

This patch makes it possible to enable/disable redirects on QNAM
level (before it was per-request only). This policy would be applied
to all subsequent requests* created by QNAM.

The policies we support at the moment:
a. Manual - that's what we always had - it's up to a user to handle
   redirects.
b. NoLessSafeRedirectsPolicy - we allow http->http, http->https and
   https->https redirects, but no protocol 'downgrade' (no
   https->http redirects).
c. SameOriginPolicy - we check that protocol/host/port are
   the same.

Updated tst_qnetworkreply.

*We previously were enabling redirect for each request, by
 setting FollowRedirectsAttribute on QNetworkRequest object.
 For backward compatibility this attribute has a higher priority
 (if set) than QNAM's policy (and it will work as NoLessSafeRedirectsPolicy).

[ChangeLog][QtNetwork] Added redirects policy to QNAM

Task-number: QTPM-239
Task-number: QTPM-237
Change-Id: I493d1728254b71b61b5504937e8e01dca5953527
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
bb10
Timur Pocheptsov 2016-12-28 15:27:57 +01:00
parent 27e27966bf
commit cebf1fea4a
10 changed files with 316 additions and 32 deletions

View File

@ -522,17 +522,17 @@ QUrl QHttpNetworkConnectionPrivate::parseRedirectResponse(QAbstractSocket *socke
if (!reply->request().isFollowRedirects())
return QUrl();
QUrl rUrl;
QUrl redirectUrl;
const QList<QPair<QByteArray, QByteArray> > fields = reply->header();
for (const QNetworkReply::RawHeaderPair &header : fields) {
if (header.first.toLower() == "location") {
rUrl = QUrl::fromEncoded(header.second);
redirectUrl = QUrl::fromEncoded(header.second);
break;
}
}
// If the location url is invalid/empty, we emit ProtocolUnknownError
if (!rUrl.isValid()) {
if (!redirectUrl.isValid()) {
emitReplyError(socket, reply, QNetworkReply::ProtocolUnknownError);
return QUrl();
}
@ -544,24 +544,37 @@ QUrl QHttpNetworkConnectionPrivate::parseRedirectResponse(QAbstractSocket *socke
}
// Resolve the URL if it's relative
if (rUrl.isRelative())
rUrl = reply->request().url().resolved(rUrl);
if (redirectUrl.isRelative())
redirectUrl = reply->request().url().resolved(redirectUrl);
// Check redirect url protocol
QString scheme = rUrl.scheme();
if (scheme == QLatin1String("http") || scheme == QLatin1String("https")) {
QString previousUrlScheme = reply->request().url().scheme();
// Check if we're doing an unsecure redirect (https -> http)
if (previousUrlScheme == QLatin1String("https")
&& scheme == QLatin1String("http")) {
emitReplyError(socket, reply, QNetworkReply::InsecureRedirectError);
return QUrl();
const QUrl priorUrl(reply->request().url());
if (redirectUrl.scheme() == QLatin1String("http") || redirectUrl.scheme() == QLatin1String("https")) {
switch (reply->request().redirectsPolicy()) {
case QNetworkRequest::NoLessSafeRedirectsPolicy:
// Check if we're doing an unsecure redirect (https -> http)
if (priorUrl.scheme() == QLatin1String("https")
&& redirectUrl.scheme() == QLatin1String("http")) {
emitReplyError(socket, reply, QNetworkReply::InsecureRedirectError);
return QUrl();
}
break;
case QNetworkRequest::SameOriginRedirectsPolicy:
if (priorUrl.host() != redirectUrl.host()
|| priorUrl.scheme() != redirectUrl.scheme()
|| priorUrl.port() != redirectUrl.port()) {
emitReplyError(socket, reply, QNetworkReply::InsecureRedirectError);
return QUrl();
}
break;
default:
Q_ASSERT(!"Unexpected redirect policy");
}
} else {
emitReplyError(socket, reply, QNetworkReply::ProtocolUnknownError);
return QUrl();
}
return rUrl;
return redirectUrl;
}
void QHttpNetworkConnectionPrivate::createAuthorization(QAbstractSocket *socket, QHttpNetworkRequest &request)

View File

@ -48,7 +48,8 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(QHttpNetworkRequest::Oper
QHttpNetworkRequest::Priority pri, const QUrl &newUrl)
: QHttpNetworkHeaderPrivate(newUrl), operation(op), priority(pri), uploadByteDevice(0),
autoDecompress(false), pipeliningAllowed(false), spdyAllowed(false), http2Allowed(false),
withCredentials(true), preConnect(false), followRedirect(false), redirectCount(0)
withCredentials(true), preConnect(false), redirectCount(0),
redirectsPolicy(QNetworkRequest::ManualRedirectsPolicy)
{
}
@ -65,8 +66,8 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
withCredentials(other.withCredentials),
ssl(other.ssl),
preConnect(other.preConnect),
followRedirect(other.followRedirect),
redirectCount(other.redirectCount)
redirectCount(other.redirectCount),
redirectsPolicy(other.redirectsPolicy)
{
}
@ -88,7 +89,8 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot
&& (operation != QHttpNetworkRequest::Custom || (customVerb == other.customVerb))
&& (withCredentials == other.withCredentials)
&& (ssl == other.ssl)
&& (preConnect == other.preConnect);
&& (preConnect == other.preConnect)
&& (redirectsPolicy == other.redirectsPolicy);
}
QByteArray QHttpNetworkRequest::methodName() const
@ -229,12 +231,17 @@ void QHttpNetworkRequest::setPreConnect(bool preConnect)
bool QHttpNetworkRequest::isFollowRedirects() const
{
return d->followRedirect;
return d->redirectsPolicy != QNetworkRequest::ManualRedirectsPolicy;
}
void QHttpNetworkRequest::setFollowRedirects(bool followRedirect)
void QHttpNetworkRequest::setRedirectsPolicy(QNetworkRequest::RedirectsPolicy policy)
{
d->followRedirect = followRedirect;
d->redirectsPolicy = policy;
}
QNetworkRequest::RedirectsPolicy QHttpNetworkRequest::redirectsPolicy() const
{
return d->redirectsPolicy;
}
int QHttpNetworkRequest::redirectCount() const

View File

@ -55,6 +55,7 @@
#ifndef QT_NO_HTTP
#include <private/qhttpnetworkheader_p.h>
#include <QtNetwork/qnetworkrequest.h>
#include <qmetatype.h>
QT_BEGIN_NAMESPACE
@ -130,7 +131,8 @@ public:
void setPreConnect(bool preConnect);
bool isFollowRedirects() const;
void setFollowRedirects(bool followRedirect);
void setRedirectsPolicy(QNetworkRequest::RedirectsPolicy policy);
QNetworkRequest::RedirectsPolicy redirectsPolicy() const;
int redirectCount() const;
void setRedirectCount(int count);
@ -173,8 +175,8 @@ public:
bool withCredentials;
bool ssl;
bool preConnect;
bool followRedirect;
int redirectCount;
QNetworkRequest::RedirectsPolicy redirectsPolicy;
};

View File

@ -1078,6 +1078,45 @@ void QNetworkAccessManager::connectToHost(const QString &hostName, quint16 port)
get(request);
}
/*!
\since 5.9
Sets the manager's redirects policy to be the \a policy specified. This policy
will affect all subsequent requests created by the manager.
Use this function to enable or disable HTTP redirects on the manager's level.
\note When creating a request QNetworkRequest::RedirectAttributePolicy has
the highest priority, next by priority is QNetworkRequest::FollowRedirectsAttribute.
Finally, the manager's policy has the lowest priority.
For backwards compatibility the default value is QNetworkRequest::ManualRedirectsPolicy.
This may change in the future and some type of auto-redirect policy will become
the default; clients relying on manual redirect handling are encouraged to set
this policy explicitly in their code.
\sa redirectsPolicy(), QNetworkRequest::RedirectsPolicy,
QNetworkRequest::FollowRedirectsAttribute
*/
void QNetworkAccessManager::setRedirectsPolicy(QNetworkRequest::RedirectsPolicy policy)
{
Q_D(QNetworkAccessManager);
d->redirectsPolicy = policy;
}
/*!
\since 5.9
Returns the redirect policy that is used when creating new requests.
\sa setRedirectsPolicy(), QNetworkRequest::RedirectsPolicy
*/
QNetworkRequest::RedirectsPolicy QNetworkAccessManager::redirectsPolicy() const
{
Q_D(const QNetworkAccessManager);
return d->redirectsPolicy;
}
/*!
\since 4.7
@ -1147,9 +1186,9 @@ QNetworkReply *QNetworkAccessManager::sendCustomRequest(const QNetworkRequest &r
/*!
Returns a new QNetworkReply object to handle the operation \a op
and request \a req. The device \a outgoingData is always 0 for Get and
Head requests, but is the value passed to post() and put() in
those operations (the QByteArray variants will pass a QBuffer
and request \a originalReq. The device \a outgoingData is always 0
for Get and Head requests, but is the value passed to post() and
put() in those operations (the QByteArray variants will pass a QBuffer
object).
The default implementation calls QNetworkCookieJar::cookiesForUrl()
@ -1159,11 +1198,20 @@ QNetworkReply *QNetworkAccessManager::sendCustomRequest(const QNetworkRequest &r
The returned object must be in an open state.
*/
QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Operation op,
const QNetworkRequest &req,
const QNetworkRequest &originalReq,
QIODevice *outgoingData)
{
Q_D(QNetworkAccessManager);
QNetworkRequest req(originalReq);
if (req.attribute(QNetworkRequest::RedirectsPolicyAttribute).isNull()
&& req.attribute(QNetworkRequest::FollowRedirectsAttribute).isNull()) {
// We only apply the general manager's policy if:
// - RedirectsPolicyAttribute is not set already on request and
// - no FollowRedirectsAttribute is set.
req.setAttribute(QNetworkRequest::RedirectsPolicyAttribute, redirectsPolicy());
}
bool isLocalFile = req.url().isLocalFile();
QString scheme = req.url().scheme();

View File

@ -41,6 +41,7 @@
#define QNETWORKACCESSMANAGER_H
#include <QtNetwork/qtnetworkglobal.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtCore/QObject>
#ifndef QT_NO_SSL
#include <QtNetwork/QSslConfiguration>
@ -49,7 +50,6 @@
QT_BEGIN_NAMESPACE
class QIODevice;
class QAbstractNetworkCache;
class QAuthenticator;
@ -57,7 +57,6 @@ class QByteArray;
template<typename T> class QList;
class QNetworkCookie;
class QNetworkCookieJar;
class QNetworkRequest;
class QNetworkReply;
class QNetworkProxy;
class QNetworkProxyFactory;
@ -149,6 +148,9 @@ public:
#endif
void connectToHost(const QString &hostName, quint16 port = 80);
void setRedirectsPolicy(QNetworkRequest::RedirectsPolicy policy);
QNetworkRequest::RedirectsPolicy redirectsPolicy() const;
Q_SIGNALS:
#ifndef QT_NO_NETWORKPROXY
void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator);

View File

@ -55,6 +55,7 @@
#include "qnetworkaccessmanager.h"
#include "qnetworkaccesscache_p.h"
#include "qnetworkaccessbackend_p.h"
#include "qnetworkrequest.h"
#include "private/qobject_p.h"
#include "QtNetwork/qnetworkproxy.h"
#include "QtNetwork/qnetworksession.h"
@ -91,6 +92,7 @@ public:
#endif
cookieJarCreated(false),
defaultAccessControl(true),
redirectsPolicy(QNetworkRequest::ManualRedirectsPolicy),
authenticationManager(QSharedPointer<QNetworkAccessAuthenticationManager>::create())
{
#ifndef QT_NO_BEARERMANAGEMENT
@ -193,6 +195,7 @@ public:
bool cookieJarCreated;
bool defaultAccessControl;
QNetworkRequest::RedirectsPolicy redirectsPolicy;
// The cache with authorization data:
QSharedPointer<QNetworkAccessAuthenticationManager> authenticationManager;

View File

@ -655,8 +655,14 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
}
#endif
if (newHttpRequest.attribute(QNetworkRequest::FollowRedirectsAttribute).toBool())
httpRequest.setFollowRedirects(true);
auto redirectsPolicy = QNetworkRequest::ManualRedirectsPolicy;
const QVariant value = newHttpRequest.attribute(QNetworkRequest::RedirectsPolicyAttribute);
if (value.isValid())
redirectsPolicy = value.value<QNetworkRequest::RedirectsPolicy>();
else if (newHttpRequest.attribute(QNetworkRequest::FollowRedirectsAttribute).toBool())
redirectsPolicy = QNetworkRequest::NoLessSafeRedirectsPolicy;
httpRequest.setRedirectsPolicy(redirectsPolicy);
httpRequest.setPriority(convert(newHttpRequest.priority()));

View File

@ -289,6 +289,12 @@ QT_BEGIN_NAMESPACE
marked to be decompressed automatically.
(This value was introduced in 5.9.)
\value RedirectsPolicyAttribute
Requests only, type: QMetaType::Int, should be one of the
QNetworkRequest::RedirectsPolicy values (default: ManualRedirectsPolicy).
This attribute obsoletes FollowRedirectsAttribute.
(This value was introduced in 5.9.)
\value User
Special type. Additional information can be passed in
QVariants with types ranging from User to UserMax. The default
@ -336,6 +342,26 @@ QT_BEGIN_NAMESPACE
\value Manual indicates behaviour has been manually overridden.
*/
/*!
\enum QNetworkRequest::RedirectsPolicy
\since 5.9
Indicates whether the Network Access API should automatically follow a
HTTP redirect response or not.
\value ManualRedirectsPolicy Default value: not following any redirects.
\value NoLessSafeRedirectsPolicy Only "http"->"http", "http" -> "https"
or "https" -> "https" redirects are allowed.
Equivalent to setting the old FollowRedirectsAttribute
to true
\value SameOriginRedirectsPolicy Require the same protocol, host and port.
Note, http://example.com and http://example.com:80
will fail with this policy (implicit/explicit ports
are considered to be a mismatch).
*/
class QNetworkRequestPrivate: public QSharedData, public QNetworkHeadersPrivate
{
public:

View File

@ -48,7 +48,6 @@
QT_BEGIN_NAMESPACE
class QSslConfiguration;
class QNetworkRequestPrivate;
@ -92,6 +91,7 @@ public:
HTTP2AllowedAttribute,
HTTP2WasUsedAttribute,
OriginalContentLengthAttribute,
RedirectsPolicyAttribute,
User = 1000,
UserMax = 32767
@ -113,6 +113,13 @@ public:
LowPriority = 5
};
enum RedirectsPolicy {
ManualRedirectsPolicy,
NoLessSafeRedirectsPolicy,
SameOriginRedirectsPolicy
};
explicit QNetworkRequest(const QUrl &url = QUrl());
QNetworkRequest(const QNetworkRequest &other);
~QNetworkRequest();
@ -169,5 +176,6 @@ Q_DECLARE_SHARED(QNetworkRequest)
QT_END_NAMESPACE
Q_DECLARE_METATYPE(QNetworkRequest)
Q_DECLARE_METATYPE(QNetworkRequest::RedirectsPolicy)
#endif

View File

@ -476,6 +476,10 @@ private Q_SLOTS:
void ioHttpChangeMaxRedirects();
void ioHttpRedirectErrors_data();
void ioHttpRedirectErrors();
void ioHttpRedirectPolicy_data();
void ioHttpRedirectPolicy();
void ioHttpRedirectPolicyErrors_data();
void ioHttpRedirectPolicyErrors();
#ifndef QT_NO_SSL
void putWithServerClosingConnectionImmediately();
#endif
@ -8163,6 +8167,171 @@ void tst_QNetworkReply::ioHttpRedirectErrors()
QCOMPARE(spy.count(), 1);
QCOMPARE(reply->error(), error);
}
struct SameOriginRedirector : MiniHttpServer
{
SameOriginRedirector(const QByteArray &data, bool ssl = false)
: MiniHttpServer(data, ssl)
{ }
std::vector<QByteArray> responses;
void reply() override
{
if (responses.empty()) {
dataToTransmit.clear();
} else {
dataToTransmit = responses.back();
responses.pop_back();
}
MiniHttpServer::reply();
}
};
void tst_QNetworkReply::ioHttpRedirectPolicy_data()
{
QTest::addColumn<QNetworkRequest::RedirectsPolicy>("policy");
QTest::addColumn<bool>("ssl");
QTest::addColumn<int>("redirectCount");
QTest::addColumn<int>("statusCode");
QTest::newRow("manual-nossl") << QNetworkRequest::ManualRedirectsPolicy << false << 0 << 307;
QTest::newRow("manual-ssl") << QNetworkRequest::ManualRedirectsPolicy << true << 0 << 307;
QTest::newRow("nolesssafe-nossl") << QNetworkRequest::NoLessSafeRedirectsPolicy << false << 1 << 200;
QTest::newRow("nolesssafe-ssl") << QNetworkRequest::NoLessSafeRedirectsPolicy << true << 1 << 200;
QTest::newRow("same-origin-nossl") << QNetworkRequest::SameOriginRedirectsPolicy << false << 1 << 200;
QTest::newRow("same-origin-ssl") << QNetworkRequest::SameOriginRedirectsPolicy << true << 1 << 200;
}
void tst_QNetworkReply::ioHttpRedirectPolicy()
{
QFETCH(const QNetworkRequest::RedirectsPolicy, policy);
QFETCH(const bool, ssl);
#ifdef QT_NO_SSL
if (ssl)
QSKIP("SSL is not supported");
#endif
QFETCH(const int, redirectCount);
QFETCH(const int, statusCode);
// Setup HTTP server.
SameOriginRedirector redirectServer("", ssl);
QUrl url(QLatin1String(
#ifndef QT_NO_SSL
ssl ? "https://localhost" :
#endif
"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(tempRedirectReplyStr().arg(QString(url.toEncoded())).toLatin1());
// This is the default one we preserve between tests.
QCOMPARE(manager.redirectsPolicy(), QNetworkRequest::ManualRedirectsPolicy);
manager.setRedirectsPolicy(policy);
QCOMPARE(manager.redirectsPolicy(), policy);
QNetworkReplyPtr reply(manager.get(QNetworkRequest(url)));
if (ssl)
reply->ignoreSslErrors();
// Restore default:
manager.setRedirectsPolicy(QNetworkRequest::ManualRedirectsPolicy);
QSignalSpy redirectSpy(reply.data(), SIGNAL(redirected(QUrl)));
QSignalSpy finishedSpy(reply.data(), SIGNAL(finished()));
QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply));
QCOMPARE(finishedSpy.count(), 1);
QCOMPARE(redirectSpy.count(), redirectCount);
QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), statusCode);
}
void tst_QNetworkReply::ioHttpRedirectPolicyErrors_data()
{
QTest::addColumn<QNetworkRequest::RedirectsPolicy>("policy");
QTest::addColumn<bool>("ssl");
QTest::addColumn<QString>("location");
QTest::addColumn<int>("maxRedirects");
QTest::addColumn<QNetworkReply::NetworkError>("expectedError");
// 1. NoLessSafeRedirectsPolicy
QTest::newRow("nolesssafe-nossl-nossl-too-many") << QNetworkRequest::NoLessSafeRedirectsPolicy
<< false << QString("http://localhost:%1") << 0 << QNetworkReply::TooManyRedirectsError;
QTest::newRow("nolesssafe-ssl-ssl-too-many") << QNetworkRequest::NoLessSafeRedirectsPolicy
<< true << QString("https:/localhost:%1") << 0 << QNetworkReply::TooManyRedirectsError;
QTest::newRow("nolesssafe-ssl-nossl-insecure-redirect") << QNetworkRequest::NoLessSafeRedirectsPolicy
<< true << QString("http://localhost:%1") << 50 << QNetworkReply::InsecureRedirectError;
// 2. SameOriginRedirectsPolicy
QTest::newRow("same-origin-nossl-nossl-too-many") << QNetworkRequest::SameOriginRedirectsPolicy
<< false << QString("http://localhost:%1") << 0 << QNetworkReply::TooManyRedirectsError;
QTest::newRow("same-origin-ssl-ssl-too-many") << QNetworkRequest::SameOriginRedirectsPolicy
<< true << QString("https://localhost:%1") << 0 << QNetworkReply::TooManyRedirectsError;
QTest::newRow("same-origin-https-http-wrong-protocol") << QNetworkRequest::SameOriginRedirectsPolicy
<< true << QString("http://localhost:%1") << 50 << QNetworkReply::InsecureRedirectError;
QTest::newRow("same-origin-http-https-wrong-protocol") << QNetworkRequest::SameOriginRedirectsPolicy
<< false << QString("https://localhost:%1") << 50 << QNetworkReply::InsecureRedirectError;
QTest::newRow("same-origin-http-http-wrong-host") << QNetworkRequest::SameOriginRedirectsPolicy
<< false << QString("http://not-so-localhost:%1") << 50 << QNetworkReply::InsecureRedirectError;
QTest::newRow("same-origin-https-https-wrong-host") << QNetworkRequest::SameOriginRedirectsPolicy
<< true << QString("https://not-so-localhost:%1") << 50 << QNetworkReply::InsecureRedirectError;
QTest::newRow("same-origin-http-http-wrong-port") << QNetworkRequest::SameOriginRedirectsPolicy
<< false << QString("http://localhost/%1") << 50 << QNetworkReply::InsecureRedirectError;
QTest::newRow("same-origin-https-https-wrong-port") << QNetworkRequest::SameOriginRedirectsPolicy
<< true << QString("https://localhost/%1") << 50 << QNetworkReply::InsecureRedirectError;
}
void tst_QNetworkReply::ioHttpRedirectPolicyErrors()
{
QFETCH(const QNetworkRequest::RedirectsPolicy, policy);
// This should never happen:
QVERIFY(policy != QNetworkRequest::ManualRedirectsPolicy);
QFETCH(const bool, ssl);
QFETCH(const QString, location);
QFETCH(const int, maxRedirects);
QFETCH(const QNetworkReply::NetworkError, expectedError);
#ifdef QT_NO_SSL
if (ssl || location.contains("https"))
QSKIP("SSL required to run this test");
#endif
// Setup the server.
MiniHttpServer server("", ssl);
server.setDataToTransmit(tempRedirectReplyStr().arg(location.arg(server.serverPort())).toLatin1());
QUrl url(QLatin1String(
#ifndef QT_NO_SSL
ssl ? "https://localhost" :
#endif
"http://localhost"));
url.setPort(server.serverPort());
QNetworkRequest request(url);
request.setMaximumRedirectsAllowed(maxRedirects);
// We always reset the policy to the default one ('Manual') after any related
// test is finished:
QCOMPARE(manager.redirectsPolicy(), QNetworkRequest::ManualRedirectsPolicy);
manager.setRedirectsPolicy(policy);
QCOMPARE(manager.redirectsPolicy(), policy);
QNetworkReplyPtr reply(manager.get(request));
// Set it back to default:
manager.setRedirectsPolicy(QNetworkRequest::ManualRedirectsPolicy);
if (ssl)
reply->ignoreSslErrors();
QSignalSpy spy(reply.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QCOMPARE(waitForFinish(reply), int(Failure));
QCOMPARE(spy.count(), 1);
QCOMPARE(reply->error(), expectedError);
}
#ifndef QT_NO_SSL
class PutWithServerClosingConnectionImmediatelyHandler: public QObject