QNetworkAccessManager: Support HTTP redirection

This commit adds support for following HTTP redirect responses on a per
request basis.

This behavior is disabled by default. It can be switched on by
setting the QNetworkRequest::FollowRedirectAttribute to true.

2 new error codes have been added to QNetworkReply:

* TooManyRedirectsError: Set when the number of redirects exceed a
given value set by the user (defaults to 50 if not set)

* UnsecureRedirectError: Set when we are redirecting from a 'https'
to 'http' protocol.

Test cases for the following scenarios:
* Single HTTP redirect using local test server
* Changing max-redirects
* Testing all redirect related error scenarios

The next commit will extend this feature at a QNAM level.

Task-number: QTBUG-8232
Change-Id: If9e28ad12bad08bcdc5bc511b1cd59dc9d8150f0
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@theqtcompany.com>
bb10
Mandeep Sandhu 2014-03-17 19:21:25 +05:30 committed by Allan Sandfeld Jensen
parent f7118e7f1b
commit d815de8c26
17 changed files with 539 additions and 57 deletions

View File

@ -506,6 +506,55 @@ bool QHttpNetworkConnectionPrivate::handleAuthenticateChallenge(QAbstractSocket
return false;
}
bool QHttpNetworkConnectionPrivate::parseRedirectResponse(QAbstractSocket *socket, QHttpNetworkReply *reply, QUrl *redirectUrl)
{
if (!reply->request().isFollowRedirects())
return false;
QUrl rUrl;
QList<QPair<QByteArray, QByteArray> > fields = reply->header();
foreach (const QNetworkReply::RawHeaderPair &header, fields) {
if (header.first.toLower() == "location") {
rUrl = QUrl::fromEncoded(header.second);
break;
}
}
// If the location url is invalid/empty, we emit ProtocolUnknownError
if (!rUrl.isValid()) {
emitReplyError(socket, reply, QNetworkReply::ProtocolUnknownError);
return false;
}
// Check if we have exceeded max redirects allowed
if (reply->request().redirectCount() <= 0) {
emitReplyError(socket, reply, QNetworkReply::TooManyRedirectsError);
return false;
}
// Resolve the URL if it's relative
if (rUrl.isRelative())
rUrl = reply->request().url().resolved(rUrl);
// 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 false;
}
} else {
emitReplyError(socket, reply, QNetworkReply::ProtocolUnknownError);
return false;
}
redirectUrl->setUrl(QString(rUrl.toEncoded()));
return true;
}
void QHttpNetworkConnectionPrivate::createAuthorization(QAbstractSocket *socket, QHttpNetworkRequest &request)
{
Q_ASSERT(socket);
@ -802,6 +851,12 @@ QString QHttpNetworkConnectionPrivate::errorDetail(QNetworkReply::NetworkError e
case QNetworkReply::SslHandshakeFailedError:
errorString = QCoreApplication::translate("QHttp", "SSL handshake failed");
break;
case QNetworkReply::TooManyRedirectsError:
errorString = QCoreApplication::translate("QHttp", "Too many redirects");
break;
case QNetworkReply::InsecureRedirectError:
errorString = QCoreApplication::translate("QHttp", "Insecure redirect");
break;
default:
// all other errors are treated as QNetworkReply::UnknownNetworkError
errorString = extraDetail;

View File

@ -250,6 +250,7 @@ public:
void emitReplyError(QAbstractSocket *socket, QHttpNetworkReply *reply, QNetworkReply::NetworkError errorCode);
bool handleAuthenticateChallenge(QAbstractSocket *socket, QHttpNetworkReply *reply, bool isProxy, bool &resend);
bool parseRedirectResponse(QAbstractSocket *socket, QHttpNetworkReply *reply, QUrl *redirectUrl);
#ifndef QT_NO_NETWORKPROXY
QNetworkProxy networkProxy;

View File

@ -492,6 +492,20 @@ void QHttpNetworkConnectionChannel::handleStatus()
bool resend = false;
switch (statusCode) {
case 301:
case 302:
case 303:
case 305:
case 307: {
// Parse the response headers and get the "location" url
QUrl redirectUrl;
if (connection->d_func()->parseRedirectResponse(socket, reply, &redirectUrl))
reply->setRedirectUrl(redirectUrl);
if (qobject_cast<QHttpNetworkConnection *>(connection))
QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
break;
}
case 401: // auth required
case 407: // proxy auth required
if (connection->d_func()->handleAuthenticateChallenge(socket, reply, (statusCode == 407), resend)) {

View File

@ -78,6 +78,23 @@ void QHttpNetworkReply::setUrl(const QUrl &url)
d->url = url;
}
QUrl QHttpNetworkReply::redirectUrl() const
{
return d_func()->redirectUrl;
}
void QHttpNetworkReply::setRedirectUrl(const QUrl &url)
{
Q_D(QHttpNetworkReply);
d->redirectUrl = url;
}
bool QHttpNetworkReply::isHttpRedirect(int statusCode)
{
return (statusCode == 301 || statusCode == 302 || statusCode == 303
|| statusCode == 305 || statusCode == 307);
}
qint64 QHttpNetworkReply::contentLength() const
{
return d_func()->contentLength();
@ -267,6 +284,11 @@ void QHttpNetworkReply::setSpdyWasUsed(bool spdy)
d_func()->spdyUsed = spdy;
}
bool QHttpNetworkReply::isRedirecting() const
{
return d_func()->isRedirecting();
}
QHttpNetworkConnection* QHttpNetworkReply::connection()
{
return d_func()->connection;
@ -910,6 +932,14 @@ qint64 QHttpNetworkReplyPrivate::getChunkSize(QAbstractSocket *socket, qint64 *c
return bytes;
}
bool QHttpNetworkReplyPrivate::isRedirecting() const
{
// We're in the process of redirecting - if the HTTP status code says so and
// followRedirect is switched on
return (QHttpNetworkReply::isHttpRedirect(statusCode)
&& request.isFollowRedirects());
}
bool QHttpNetworkReplyPrivate::shouldEmitSignals()
{
// for 401 & 407 don't emit the data signals. Content along with these

View File

@ -127,8 +127,15 @@ public:
bool isSpdyUsed() const;
void setSpdyWasUsed(bool spdy);
bool isRedirecting() const;
QHttpNetworkConnection* connection();
QUrl redirectUrl() const;
void setRedirectUrl(const QUrl &url);
static bool isHttpRedirect(int statusCode);
#ifndef QT_NO_SSL
QSslConfiguration sslConfiguration() const;
void setSslConfiguration(const QSslConfiguration &config);
@ -153,6 +160,7 @@ Q_SIGNALS:
void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator);
#endif
void authenticationRequired(const QHttpNetworkRequest &request, QAuthenticator *authenticator);
void redirected(const QUrl &url, int httpStatus, int maxRedirectsRemaining);
private:
Q_DECLARE_PRIVATE(QHttpNetworkReply)
friend class QHttpSocketEngine;
@ -185,6 +193,7 @@ public:
qint64 readReplyBodyChunked(QAbstractSocket *in, QByteDataBuffer *out);
qint64 getChunkSize(QAbstractSocket *in, qint64 *chunkSize);
bool isRedirecting() const;
bool shouldEmitSignals();
bool expectContent();
void eraseData();
@ -245,6 +254,7 @@ public:
bool downstreamLimited;
char* userProvidedDownloadBuffer;
QUrl redirectUrl;
#ifndef QT_NO_COMPRESS
z_stream_s *inflateStrm;

View File

@ -42,7 +42,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(QHttpNetworkRequest::Oper
QHttpNetworkRequest::Priority pri, const QUrl &newUrl)
: QHttpNetworkHeaderPrivate(newUrl), operation(op), priority(pri), uploadByteDevice(0),
autoDecompress(false), pipeliningAllowed(false), spdyAllowed(false),
withCredentials(true), preConnect(false)
withCredentials(true), preConnect(false), followRedirect(false), redirectCount(0)
{
}
@ -59,6 +59,8 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest
withCredentials = other.withCredentials;
ssl = other.ssl;
preConnect = other.preConnect;
followRedirect = other.followRedirect;
redirectCount = other.redirectCount;
}
QHttpNetworkRequestPrivate::~QHttpNetworkRequestPrivate()
@ -217,6 +219,26 @@ void QHttpNetworkRequest::setPreConnect(bool preConnect)
d->preConnect = preConnect;
}
bool QHttpNetworkRequest::isFollowRedirects() const
{
return d->followRedirect;
}
void QHttpNetworkRequest::setFollowRedirects(bool followRedirect)
{
d->followRedirect = followRedirect;
}
int QHttpNetworkRequest::redirectCount() const
{
return d->redirectCount;
}
void QHttpNetworkRequest::setRedirectCount(int count)
{
d->redirectCount = count;
}
qint64 QHttpNetworkRequest::contentLength() const
{
return d->contentLength();

View File

@ -118,6 +118,12 @@ public:
bool isPreConnect() const;
void setPreConnect(bool preConnect);
bool isFollowRedirects() const;
void setFollowRedirects(bool followRedirect);
int redirectCount() const;
void setRedirectCount(int count);
void setUploadByteDevice(QNonContiguousByteDevice *bd);
QNonContiguousByteDevice* uploadByteDevice() const;
@ -154,6 +160,8 @@ public:
bool withCredentials;
bool ssl;
bool preConnect;
bool followRedirect;
int redirectCount;
};

View File

@ -496,6 +496,9 @@ void QHttpThreadDelegate::finishedSlot()
emit error(statusCodeFromHttp(httpReply->statusCode(), httpRequest.url()), msg);
}
if (httpRequest.isFollowRedirects() && httpReply->isRedirecting())
emit redirected(httpReply->redirectUrl(), httpReply->statusCode(), httpReply->request().redirectCount() - 1);
emit downloadFinished();
QMetaObject::invokeMethod(httpReply, "deleteLater", Qt::QueuedConnection);

View File

@ -139,6 +139,8 @@ signals:
void downloadData(QByteArray);
void error(QNetworkReply::NetworkError, const QString);
void downloadFinished();
void redirected(const QUrl &url, int httpStatus, int maxRedirectsRemainig);
public slots:
// This are called via QueuedConnection from user thread
void startRequest();

View File

@ -132,6 +132,14 @@ QNetworkReplyPrivate::QNetworkReplyPrivate()
\value BackgroundRequestNotAllowedError the background request
is not currently allowed due to platform policy.
\value TooManyRedirectsError while following redirects, the maximum
limit was reached. The limit is by default set to 50 or as set by
QNetworkRequest::setMaxRedirectsAllowed().
\value InsecureRedirectError while following redirects, the network
access API detected a redirect from a encrypted protocol (https) to an
unencrypted one (http).
\value ProxyConnectionRefusedError the connection to the proxy
server was refused (the proxy server is not accepting requests)
@ -275,6 +283,19 @@ QNetworkReplyPrivate::QNetworkReplyPrivate()
\sa QSslPreSharedKeyAuthenticator
*/
/*!
\fn void QNetworkReply::redirected(const QUrl &url)
\since 5.6
This signal is emitted if the QNetworkRequest::FollowRedirectsAttribute was
set in the request and the server responded with a 3xx status (specifically
301, 302, 303, 305 or 307 status code) with a valid url in the location
header, indicating a HTTP redirect. The \a url parameter contains the new
redirect url as returned by the server in the location header.
\sa QNetworkRequest::FollowRedirectsAttribute
*/
/*!
\fn void QNetworkReply::metaDataChanged()
@ -498,7 +519,7 @@ QNetworkAccessManager *QNetworkReply::manager() const
*/
QNetworkRequest QNetworkReply::request() const
{
return d_func()->request;
return d_func()->originalRequest;
}
/*!
@ -549,9 +570,12 @@ bool QNetworkReply::isRunning() const
/*!
Returns the URL of the content downloaded or uploaded. Note that
the URL may be different from that of the original request.
the URL may be different from that of the original request. If the
QNetworkRequest::FollowRedirectsAttribute was set in the request, then this
function returns the current url that the network API is accessing, i.e the
url emitted in the QNetworkReply::redirected signal.
\sa request(), setUrl(), QNetworkRequest::url()
\sa request(), setUrl(), QNetworkRequest::url(), redirected()
*/
QUrl QNetworkReply::url() const
{
@ -794,7 +818,7 @@ void QNetworkReply::setOperation(QNetworkAccessManager::Operation operation)
void QNetworkReply::setRequest(const QNetworkRequest &request)
{
Q_D(QNetworkReply);
d->request = request;
d->originalRequest = request;
}
/*!

View File

@ -69,6 +69,8 @@ public:
TemporaryNetworkFailureError,
NetworkSessionFailedError,
BackgroundRequestNotAllowedError,
TooManyRedirectsError,
InsecureRedirectError,
UnknownNetworkError = 99,
// proxy errors (101-199):
@ -153,6 +155,7 @@ Q_SIGNALS:
void sslErrors(const QList<QSslError> &errors);
void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
#endif
void redirected(const QUrl &url);
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);

View File

@ -69,6 +69,7 @@ public:
QNetworkReplyPrivate();
QNetworkRequest request;
QNetworkRequest originalRequest;
QUrl url;
QPointer<QNetworkAccessManager> manager;
qint64 readBufferMaxSize;

View File

@ -166,6 +166,7 @@ QNetworkReplyHttpImpl::QNetworkReplyHttpImpl(QNetworkAccessManager* const manage
d->manager = manager;
d->managerPrivate = manager->d_func();
d->request = request;
d->originalRequest = request;
d->operation = operation;
d->outgoingData = outgoingData;
d->url = request.url();
@ -482,7 +483,7 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h
if (!nc)
return false; // no local cache
QNetworkCacheMetaData metaData = nc->metaData(request.url());
QNetworkCacheMetaData metaData = nc->metaData(httpRequest.url());
if (!metaData.isValid())
return false; // not in cache
@ -596,7 +597,7 @@ QHttpNetworkRequest::Priority QNetworkReplyHttpImplPrivate::convert(const QNetwo
}
}
void QNetworkReplyHttpImplPrivate::postRequest()
void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpRequest)
{
Q_Q(QNetworkReplyHttpImpl);
@ -620,8 +621,9 @@ void QNetworkReplyHttpImplPrivate::postRequest()
thread = managerPrivate->httpThread;
}
QUrl url = request.url();
QUrl url = newHttpRequest.url();
httpRequest.setUrl(url);
httpRequest.setRedirectCount(newHttpRequest.maximumRedirectsAllowed());
QString scheme = url.scheme().toLower();
bool ssl = (scheme == QLatin1String("https")
@ -637,7 +639,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
QNetworkProxy transparentProxy, cacheProxy;
// FIXME the proxy stuff should be done in the HTTP thread
foreach (const QNetworkProxy &p, managerPrivate->queryProxy(QNetworkProxyQuery(request.url()))) {
foreach (const QNetworkProxy &p, managerPrivate->queryProxy(QNetworkProxyQuery(newHttpRequest.url()))) {
// use the first proxy that works
// for non-encrypted connections, any transparent or HTTP proxy
// for encrypted, only transparent proxies
@ -668,9 +670,11 @@ void QNetworkReplyHttpImplPrivate::postRequest()
}
#endif
if (newHttpRequest.attribute(QNetworkRequest::FollowRedirectsAttribute).toBool())
httpRequest.setFollowRedirects(true);
bool loadedFromCache = false;
httpRequest.setPriority(convert(request.priority()));
httpRequest.setPriority(convert(newHttpRequest.priority()));
switch (operation) {
case QNetworkAccessManager::GetOperation:
@ -704,7 +708,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
invalidateCache(); // for safety reasons, we don't know what the operation does
httpRequest.setOperation(QHttpNetworkRequest::Custom);
createUploadByteDevice();
httpRequest.setCustomVerb(request.attribute(
httpRequest.setCustomVerb(newHttpRequest.attribute(
QNetworkRequest::CustomVerbAttribute).toByteArray());
break;
@ -716,7 +720,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
return; // no need to send the request! :)
}
QList<QByteArray> headers = request.rawHeaderList();
QList<QByteArray> headers = newHttpRequest.rawHeaderList();
if (resumeOffset != 0) {
if (headers.contains("Range")) {
// Need to adjust resume offset for user specified range
@ -724,7 +728,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
headers.removeOne("Range");
// We've already verified that requestRange starts with "bytes=", see canResume.
QByteArray requestRange = request.rawHeader("Range").mid(6);
QByteArray requestRange = newHttpRequest.rawHeader("Range").mid(6);
int index = requestRange.indexOf('-');
@ -741,16 +745,16 @@ void QNetworkReplyHttpImplPrivate::postRequest()
}
foreach (const QByteArray &header, headers)
httpRequest.setHeaderField(header, request.rawHeader(header));
httpRequest.setHeaderField(header, newHttpRequest.rawHeader(header));
if (request.attribute(QNetworkRequest::HttpPipeliningAllowedAttribute).toBool() == true)
if (newHttpRequest.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,
(newHttpRequest.attribute(QNetworkRequest::AuthenticationReuseAttribute,
QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual)
httpRequest.setWithCredentials(false);
@ -777,7 +781,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
delegate->ssl = ssl;
#ifndef QT_NO_SSL
if (ssl)
delegate->incomingSslConfiguration = request.sslConfiguration();
delegate->incomingSslConfiguration = newHttpRequest.sslConfiguration();
#endif
// Do we use synchronous HTTP?
@ -789,7 +793,7 @@ void QNetworkReplyHttpImplPrivate::postRequest()
if (!synchronous) {
// Tell our zerocopy policy to the delegate
QVariant downloadBufferMaximumSizeAttribute = request.attribute(QNetworkRequest::MaximumDownloadBufferSizeAttribute);
QVariant downloadBufferMaximumSizeAttribute = newHttpRequest.attribute(QNetworkRequest::MaximumDownloadBufferSizeAttribute);
if (downloadBufferMaximumSizeAttribute.isValid()) {
delegate->downloadBufferMaximumSize = downloadBufferMaximumSizeAttribute.toLongLong();
} else {
@ -824,6 +828,9 @@ void QNetworkReplyHttpImplPrivate::postRequest()
QObject::connect(delegate, SIGNAL(error(QNetworkReply::NetworkError,QString)),
q, SLOT(httpError(QNetworkReply::NetworkError,QString)),
Qt::QueuedConnection);
QObject::connect(delegate, SIGNAL(redirected(QUrl,int,int)),
q, SLOT(onRedirected(QUrl,int,int)),
Qt::QueuedConnection);
#ifndef QT_NO_SSL
QObject::connect(delegate, SIGNAL(sslConfigurationChanged(QSslConfiguration)),
q, SLOT(replySslConfigurationChanged(QSslConfiguration)),
@ -950,7 +957,7 @@ void QNetworkReplyHttpImplPrivate::invalidateCache()
{
QAbstractNetworkCache *nc = managerPrivate->networkCache;
if (nc)
nc->remove(request.url());
nc->remove(httpRequest.url());
}
void QNetworkReplyHttpImplPrivate::initCacheSaveDevice()
@ -1030,21 +1037,30 @@ void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d)
for (int i = 0; i < pendingDownloadDataCopy.bufferCount(); i++) {
QByteArray const &item = pendingDownloadDataCopy[i];
// This is going to look a little strange. When downloading data while a
// HTTP redirect is happening (and enabled), we write the redirect
// response to the cache. However, we do not append it to our internal
// buffer as that will contain the response data only for the final
// response
if (cacheSaveDevice)
cacheSaveDevice->write(item.constData(), item.size());
downloadMultiBuffer.append(item);
if (!isHttpRedirectResponse())
downloadMultiBuffer.append(item);
bytesWritten += item.size();
}
pendingDownloadDataCopy.clear();
bytesDownloaded += bytesWritten;
QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader);
if (preMigrationDownloaded != Q_INT64_C(-1))
totalSize = totalSize.toLongLong() + preMigrationDownloaded;
if (isHttpRedirectResponse())
return;
bytesDownloaded += bytesWritten;
emit q->readyRead();
// emit readyRead before downloadProgress incase this will cause events to be
// processed and we get into a recursive call (as in QProgressDialog).
@ -1066,6 +1082,64 @@ void QNetworkReplyHttpImplPrivate::replyFinished()
finished();
}
QNetworkAccessManager::Operation QNetworkReplyHttpImplPrivate::getRedirectOperation(QNetworkAccessManager::Operation currentOp, int httpStatus)
{
// HTTP status code can be used to decide if we can redirect with a GET
// operation or not. See http://www.ietf.org/rfc/rfc2616.txt [Sec 10.3] for
// more details
Q_UNUSED(httpStatus);
switch (currentOp) {
case QNetworkAccessManager::HeadOperation:
return QNetworkAccessManager::HeadOperation;
default:
break;
}
// For now, we're always returning GET for anything other than HEAD
return QNetworkAccessManager::GetOperation;
}
bool QNetworkReplyHttpImplPrivate::isHttpRedirectResponse() const
{
return httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(statusCode);
}
QNetworkRequest QNetworkReplyHttpImplPrivate::createRedirectRequest(const QNetworkRequest &originalRequest,
const QUrl &url,
int maxRedirectsRemaining)
{
QNetworkRequest newRequest(originalRequest);
newRequest.setUrl(url);
newRequest.setMaximumRedirectsAllowed(maxRedirectsRemaining);
return newRequest;
}
void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int httpStatus, int maxRedirectsRemaining)
{
Q_Q(QNetworkReplyHttpImpl);
if (isFinished)
return;
if (httpRequest.isFollowRedirects()) // update the reply's url as it could've changed
url = redirectUrl;
QNetworkRequest redirectRequest = createRedirectRequest(originalRequest, redirectUrl, maxRedirectsRemaining);
operation = getRedirectOperation(operation, httpStatus);
cookedHeaders.clear();
if (managerPrivate->httpThread)
managerPrivate->httpThread->disconnect();
// Recurse
QMetaObject::invokeMethod(q, "start", Qt::QueuedConnection,
Q_ARG(QNetworkRequest, redirectRequest));
emit q->redirected(redirectUrl);
}
void QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode)
{
Q_Q(QNetworkReplyHttpImpl);
@ -1114,7 +1188,16 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData
end = headerMap.constEnd();
for (; it != end; ++it) {
QByteArray value = q->rawHeader(it->first);
// Reset any previous "location" header set in the reply. In case of
// redirects, we don't want to 'append' multiple location header values,
// rather we keep only the latest one
if (it->first.toLower() == "location")
value.clear();
if (!value.isEmpty()) {
// Why are we appending values for headers which are already
// present?
if (qstricmp(it->first.constData(), "set-cookie") == 0)
value += '\n';
else
@ -1128,12 +1211,13 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData
q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reasonPhrase);
// is it a redirection?
checkForRedirect(statusCode);
if (!isHttpRedirectResponse())
checkForRedirect(statusCode);
if (statusCode >= 500 && statusCode < 600) {
QAbstractNetworkCache *nc = managerPrivate->networkCache;
if (nc) {
QNetworkCacheMetaData metaData = nc->metaData(request.url());
QNetworkCacheMetaData metaData = nc->metaData(httpRequest.url());
QNetworkHeadersPrivate cacheHeaders;
cacheHeaders.setAllRawHeaders(metaData.rawHeaders());
QNetworkHeadersPrivate::RawHeadersList::ConstIterator it;
@ -1155,7 +1239,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData
#endif
QAbstractNetworkCache *nc = managerPrivate->networkCache;
if (nc) {
QNetworkCacheMetaData oldMetaData = nc->metaData(request.url());
QNetworkCacheMetaData oldMetaData = nc->metaData(httpRequest.url());
QNetworkCacheMetaData metaData = fetchCacheMetaData(oldMetaData);
if (oldMetaData != metaData)
nc->updateMetaData(metaData);
@ -1202,6 +1286,9 @@ void QNetworkReplyHttpImplPrivate::replyDownloadProgressSlot(qint64 bytesReceive
// FIXME where is it closed?
}
if (isHttpRedirectResponse())
return;
bytesDownloaded = bytesReceived;
downloadBufferCurrentSize = bytesReceived;
@ -1350,6 +1437,8 @@ bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData
if (status < 100)
status = 200; // fake it
statusCode = status;
q->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, status);
q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, attributes.value(QNetworkRequest::HttpReasonPhraseAttribute));
q->setAttribute(QNetworkRequest::SourceIsFromCacheAttribute, true);
@ -1357,10 +1446,16 @@ bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData
QNetworkCacheMetaData::RawHeaderList rawHeaders = metaData.rawHeaders();
QNetworkCacheMetaData::RawHeaderList::ConstIterator it = rawHeaders.constBegin(),
end = rawHeaders.constEnd();
for ( ; it != end; ++it)
QUrl redirectUrl;
for ( ; it != end; ++it) {
if (httpRequest.isFollowRedirects() &&
!qstricmp(it->first.toLower().constData(), "location"))
redirectUrl = QUrl::fromEncoded(it->second);
setRawHeader(it->first, it->second);
}
checkForRedirect(status);
if (!isHttpRedirectResponse())
checkForRedirect(status);
cacheLoadDevice = contents;
q->connect(cacheLoadDevice, SIGNAL(readyRead()), SLOT(_q_cacheLoadReadyRead()));
@ -1377,6 +1472,14 @@ bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData
qDebug() << "Successfully sent cache:" << url << contents->size() << "bytes";
#endif
// Do redirect processing
if (httpRequest.isFollowRedirects() && QHttpNetworkReply::isHttpRedirect(status)) {
QMetaObject::invokeMethod(q, "onRedirected", Qt::QueuedConnection,
Q_ARG(QUrl, redirectUrl),
Q_ARG(int, status),
Q_ARG(int, httpRequest.redirectCount() - 1));
}
// Set the following flag so we can ignore some signals from HTTP thread
// that would still come
loadingFromCache = true;
@ -1572,13 +1675,13 @@ void QNetworkReplyHttpImplPrivate::setResumeOffset(quint64 offset)
could not be started due to an unopened or roaming session. The caller should recall this
function once the session has been opened or the roaming process has finished.
*/
bool QNetworkReplyHttpImplPrivate::start()
bool QNetworkReplyHttpImplPrivate::start(const QNetworkRequest &newHttpRequest)
{
#ifndef QT_NO_BEARERMANAGEMENT
QSharedPointer<QNetworkSession> networkSession(managerPrivate->getNetworkSession());
if (!networkSession) {
#endif
postRequest();
postRequest(newHttpRequest);
return true;
#ifndef QT_NO_BEARERMANAGEMENT
}
@ -1588,7 +1691,7 @@ bool QNetworkReplyHttpImplPrivate::start()
if (host == QLatin1String("localhost") ||
QHostAddress(host).isLoopback()) {
// Don't need an open session for localhost access.
postRequest();
postRequest(newHttpRequest);
return true;
}
@ -1597,13 +1700,13 @@ bool QNetworkReplyHttpImplPrivate::start()
Q_Q(QNetworkReplyHttpImpl);
QObject::connect(networkSession.data(), SIGNAL(usagePoliciesChanged(QNetworkSession::UsagePolicies)),
q, SLOT(_q_networkSessionUsagePoliciesChanged(QNetworkSession::UsagePolicies)));
postRequest();
postRequest(newHttpRequest);
return true;
} else if (synchronous) {
// Command line applications using the synchronous path such as xmlpatterns may need an extra push.
networkSession->open();
if (networkSession->waitForOpened()) {
postRequest();
postRequest(newHttpRequest);
return true;
}
}
@ -1635,7 +1738,7 @@ void QNetworkReplyHttpImplPrivate::_q_startOperation()
}
#endif
if (!start()) {
if (!start(request)) {
#ifndef QT_NO_BEARERMANAGEMENT
// backend failed to start because the session state is not Connected.
// QNetworkAccessManager will call reply->backend->start() again for us when the session
@ -1697,18 +1800,20 @@ void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead()
// emit readyRead before downloadProgress incase this will cause events to be
// processed and we get into a recursive call (as in QProgressDialog).
// This readyRead() goes to the user. The user then may or may not read() anything.
emit q->readyRead();
if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) {
downloadProgressSignalChoke.restart();
emit q->downloadProgress(bytesDownloaded,
totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong());
}
if (!(isHttpRedirectResponse())) {
// This readyRead() goes to the user. The user then may or may not read() anything.
emit q->readyRead();
if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) {
downloadProgressSignalChoke.restart();
emit q->downloadProgress(bytesDownloaded,
totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong());
}
}
// If there are still bytes available in the cacheLoadDevice then the user did not read
// in response to the readyRead() signal. This means we have to load from the cacheLoadDevice
// and buffer that stuff. This is needed to be able to properly emit finished() later.
while (cacheLoadDevice->bytesAvailable()) {
while (cacheLoadDevice->bytesAvailable() && !isHttpRedirectResponse()) {
downloadMultiBuffer.append(cacheLoadDevice->readAll());
}
@ -1731,7 +1836,6 @@ void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead()
cacheLoadDevice = 0;
QMetaObject::invokeMethod(q, "_q_finished", Qt::QueuedConnection);
}
}
@ -1951,6 +2055,15 @@ void QNetworkReplyHttpImplPrivate::finished()
#endif
}
// if we don't know the total size of or we received everything save the cache
if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize)
completeCacheSave();
// We check for errorCode too as in case of SSL handshake failure, we still
// get the HTTP redirect status code (301, 303 etc)
if (isHttpRedirectResponse() && errorCode == QNetworkReply::NoError)
return;
state = Finished;
q->setFinished(true);
@ -1963,10 +2076,6 @@ void QNetworkReplyHttpImplPrivate::finished()
if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer))
emit q->uploadProgress(0, 0);
// if we don't know the total size of or we received everything save the cache
if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize)
completeCacheSave();
emit q->readChannelFinished();
emit q->finished();
}

View File

@ -89,6 +89,7 @@ public:
Q_DECLARE_PRIVATE(QNetworkReplyHttpImpl)
Q_PRIVATE_SLOT(d_func(), void _q_startOperation())
Q_PRIVATE_SLOT(d_func(), bool start(const QNetworkRequest &))
Q_PRIVATE_SLOT(d_func(), void _q_cacheLoadReadyRead())
Q_PRIVATE_SLOT(d_func(), void _q_bufferOutgoingData())
Q_PRIVATE_SLOT(d_func(), void _q_bufferOutgoingDataFinished())
@ -126,7 +127,7 @@ public:
Q_PRIVATE_SLOT(d_func(), void emitReplyUploadProgress(qint64, qint64))
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))
#ifndef QT_NO_SSL
protected:
@ -157,7 +158,7 @@ public:
QNetworkReplyHttpImplPrivate();
~QNetworkReplyHttpImplPrivate();
bool start();
bool start(const QNetworkRequest &newHttpRequest);
void _q_startOperation();
void _q_cacheLoadReadyRead();
@ -200,6 +201,7 @@ public:
QIODevice *outgoingData;
QSharedPointer<QRingBuffer> outgoingDataBuffer;
void emitReplyUploadProgress(qint64 bytesSent, qint64 bytesTotal); // dup?
void onRedirected(const QUrl &redirectUrl, int httpStatus, int maxRedirectsRemainig);
qint64 bytesUploaded;
@ -259,9 +261,10 @@ public:
QNetworkCacheMetaData fetchCacheMetaData(const QNetworkCacheMetaData &metaData) const;
void postRequest();
void postRequest(const QNetworkRequest& newHttpRequest);
QNetworkAccessManager::Operation getRedirectOperation(QNetworkAccessManager::Operation currentOp, int httpStatus);
QNetworkRequest createRedirectRequest(const QNetworkRequest &originalRequests, const QUrl &url, int maxRedirectsRemainig);
bool isHttpRedirectResponse() const;
public:
// From HTTP thread:

View File

@ -140,7 +140,9 @@ QT_BEGIN_NAMESPACE
request to a different URL. The Network Access API does not by
default follow redirections: it's up to the application to
determine if the requested redirection should be allowed,
according to its security policies.
according to its security policies. However, if
QNetworkRequest::FollowRedirectsAttribute is set, then this attribute
will not be present in the reply.
The returned URL might be relative. Use QUrl::resolved()
to create an absolute URL out of it.
@ -256,6 +258,12 @@ QT_BEGIN_NAMESPACE
in 100 millisecond intervals.
(This value was introduced in 5.5.)
\value FollowRedirectsAttribute
Requests only, type: QMetaType::Bool (default: false)
Indicates whether the Network Access API should automatically follow a
HTTP redirect response or not. Currently redirects that are insecure,
that is redirecting from "https" to "http" protocol, are not allowed.
\value User
Special type. Additional information can be passed in
QVariants with types ranging from User to UserMax. The default
@ -306,11 +314,13 @@ QT_BEGIN_NAMESPACE
class QNetworkRequestPrivate: public QSharedData, public QNetworkHeadersPrivate
{
public:
static const int maxRedirectCount = 50;
inline QNetworkRequestPrivate()
: priority(QNetworkRequest::NormalPriority)
#ifndef QT_NO_SSL
, sslConfiguration(0)
#endif
, maxRedirectsAllowed(maxRedirectCount)
{ qRegisterMetaType<QNetworkRequest>(); }
~QNetworkRequestPrivate()
{
@ -325,7 +335,7 @@ public:
{
url = other.url;
priority = other.priority;
maxRedirectsAllowed = other.maxRedirectsAllowed;
#ifndef QT_NO_SSL
sslConfiguration = 0;
if (other.sslConfiguration)
@ -338,7 +348,8 @@ public:
return url == other.url &&
priority == other.priority &&
rawHeaders == other.rawHeaders &&
attributes == other.attributes;
attributes == other.attributes &&
maxRedirectsAllowed == other.maxRedirectsAllowed;
// don't compare cookedHeaders
}
@ -347,6 +358,7 @@ public:
#ifndef QT_NO_SSL
mutable QSslConfiguration *sslConfiguration;
#endif
int maxRedirectsAllowed;
};
/*!
@ -657,6 +669,32 @@ void QNetworkRequest::setPriority(Priority priority)
d->priority = priority;
}
/*!
\since 5.6
Returns the maximum number of redirects allowed to be followed for this
request.
\sa setMaximumRedirectsAllowed()
*/
int QNetworkRequest::maximumRedirectsAllowed() const
{
return d->maxRedirectsAllowed;
}
/*!
\since 5.6
Sets the maximum number of redirects allowed to be followed for this
request to \a maxRedirectsAllowed.
\sa maximumRedirectsAllowed()
*/
void QNetworkRequest::setMaximumRedirectsAllowed(int maxRedirectsAllowed)
{
d->maxRedirectsAllowed = maxRedirectsAllowed;
}
static QByteArray headerName(QNetworkRequest::KnownHeaders header)
{
switch (header) {

View File

@ -81,6 +81,7 @@ public:
SpdyAllowedAttribute,
SpdyWasUsedAttribute,
EmitAllUploadProgressSignalsAttribute,
FollowRedirectsAttribute,
User = 1000,
UserMax = 32767
@ -141,6 +142,10 @@ public:
Priority priority() const;
void setPriority(Priority priority);
// HTTP redirect related
int maximumRedirectsAllowed() const;
void setMaximumRedirectsAllowed(int maximumRedirectsAllowed);
private:
QSharedDataPointer<QNetworkRequestPrivate> d;
friend class QNetworkRequestPrivate;

View File

@ -121,6 +121,14 @@ class tst_QNetworkReply: public QObject
return s;
};
static QString tempRedirectReplyStr() {
QString s = "HTTP/1.1 307 Temporary Redirect\r\n"
"Content-Type: text/plain\r\n"
"location: %1\r\n"
"\r\n";
return s;
};
QEventLoop *loop;
enum RunSimpleRequestReturn { Timeout = 0, Success, Failure };
int returnCode;
@ -458,6 +466,11 @@ private Q_SLOTS:
void putWithRateLimiting();
void ioHttpSingleRedirect();
void ioHttpChangeMaxRedirects();
void ioHttpRedirectErrors_data();
void ioHttpRedirectErrors();
// NOTE: This test must be last!
void parentingRepliesToTheApp();
private:
@ -595,10 +608,16 @@ protected:
virtual void reply() {
Q_ASSERT(!client.isNull());
// we need to emulate the bytesWrittenSlot call if the data is empty.
if (dataToTransmit.size() == 0)
if (dataToTransmit.size() == 0) {
QMetaObject::invokeMethod(this, "bytesWrittenSlot", Qt::QueuedConnection);
else
} else {
client->write(dataToTransmit);
// FIXME: For SSL connections, if we don't flush the socket, the
// client never receives the data and since we're doing a disconnect
// immediately afterwards, it causes a RemoteHostClosedError for the
// client
client->flush();
}
}
private:
void connectSocketSignals()
@ -7958,7 +7977,142 @@ void tst_QNetworkReply::putWithRateLimiting()
QCOMPARE(uploadedData, data);
}
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);
QUrl redirectUrl = QUrl(localhost);
redirectUrl.setPort(server2.serverPort());
QByteArray tempRedirectReply =
tempRedirectReplyStr().arg(QString(redirectUrl.toEncoded())).toLatin1();
// Setup redirect server
MiniHttpServer server(tempRedirectReply);
localhost.setPort(server.serverPort());
QNetworkRequest request(localhost);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReplyPtr reply(manager.get(request));
QSignalSpy redSpy(reply.data(), SIGNAL(redirected(QUrl)));
QSignalSpy finSpy(reply.data(), SIGNAL(finished()));
QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply));
// Redirected and finished should be emitted exactly once
QCOMPARE(redSpy.count(), 1);
QCOMPARE(finSpy.count(), 1);
// Original URL should not be changed after redirect
QCOMPARE(request.url(), localhost);
// Verify Redirect url
QList<QVariant> args = redSpy.takeFirst();
QCOMPARE(args.at(0).toUrl(), redirectUrl);
// Reply url is set to the redirect url
QCOMPARE(reply->url(), redirectUrl);
QCOMPARE(reply->error(), QNetworkReply::NoError);
}
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);
QUrl server2Url(localhost);
server2Url.setPort(server2.serverPort());
server1.setDataToTransmit(tempRedirectReplyStr().arg(
QString(server2Url.toEncoded())).toLatin1());
QUrl server3Url(localhost);
server3Url.setPort(server3.serverPort());
server2.setDataToTransmit(tempRedirectReplyStr().arg(
QString(server3Url.toEncoded())).toLatin1());
localhost.setPort(server1.serverPort());
QNetworkRequest request(localhost);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
// Set Max redirects to 1. This will cause TooManyRedirectsError
request.setMaximumRedirectsAllowed(1);
QNetworkReplyPtr reply(manager.get(request));
QSignalSpy redSpy(reply.data(), SIGNAL(redirected(QUrl)));
QSignalSpy spy(reply.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QVERIFY(waitForFinish(reply) == Failure);
QCOMPARE(redSpy.count(), request.maximumRedirectsAllowed());
QCOMPARE(spy.count(), 1);
QVERIFY(reply->error() == QNetworkReply::TooManyRedirectsError);
// Increase max redirects to allow successful completion
request.setMaximumRedirectsAllowed(3);
QNetworkReplyPtr reply2(manager.get(request));
QSignalSpy redSpy2(reply2.data(), SIGNAL(redirected(QUrl)));
QVERIFY2(waitForFinish(reply2) == Success, msgWaitForFinished(reply2));
QCOMPARE(redSpy2.count(), 2);
QCOMPARE(reply2->url(), server3Url);
QVERIFY(reply2->error() == QNetworkReply::NoError);
}
void tst_QNetworkReply::ioHttpRedirectErrors_data()
{
QTest::addColumn<QString>("url");
QTest::addColumn<QString>("dataToSend");
QTest::addColumn<QNetworkReply::NetworkError>("error");
QString tempRedirectReply = QString("HTTP/1.1 307 Temporary Redirect\r\n"
"Content-Type: text/plain\r\n"
"location: http://localhost:%1\r\n\r\n");
QTest::newRow("too-many-redirects") << "http://localhost" << tempRedirectReply << QNetworkReply::TooManyRedirectsError;
QTest::newRow("insecure-redirect") << "https://localhost" << tempRedirectReply << QNetworkReply::InsecureRedirectError;
QTest::newRow("unknown-redirect") << "http://localhost"<< tempRedirectReply.replace("http", "bad_protocol") << QNetworkReply::ProtocolUnknownError;
}
void tst_QNetworkReply::ioHttpRedirectErrors()
{
QFETCH(QString, url);
QFETCH(QString, dataToSend);
QFETCH(QNetworkReply::NetworkError, error);
QUrl localhost(url);
MiniHttpServer server("", localhost.scheme() == "https");
localhost.setPort(server.serverPort());
QByteArray d2s = dataToSend.arg(
QString::number(server.serverPort())).toLatin1();
server.setDataToTransmit(d2s);
QNetworkRequest request(localhost);
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReplyPtr reply(manager.get(request));
if (localhost.scheme() == "https")
reply.data()->ignoreSslErrors();
QSignalSpy spy(reply.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QVERIFY(waitForFinish(reply) == Failure);
QCOMPARE(spy.count(), 1);
QVERIFY(reply->error() == error);
}
// NOTE: This test must be last testcase in tst_qnetworkreply!
void tst_QNetworkReply::parentingRepliesToTheApp()