AbstractNetworkJob: Improve redirect handling #5555

* For requests:
  - reuse the original QNetworkRequest, so headers and attributes
    are the same as in the original request
  - determine the original http method from the reply and the request
    attributes
  - keep the original request body around such that it can be sent
    again in case the request is redirected

* Simplify the interface that is used for creating new requests in
  AbstractNetworkJob.
This commit is contained in:
Christian Kamm 2017-03-03 11:20:53 +01:00 коммит произвёл ckamm
Родитель 298684aaa0
Коммит 4a1a5fa076
15 изменённых файлов: 166 добавлений и 159 удалений

Просмотреть файл

@ -54,9 +54,7 @@ void NotificationConfirmJob::start()
req.setRawHeader("Ocs-APIREQUEST", "true");
req.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
QIODevice *iodevice = 0;
setReply(davRequest(_verb, _link, req, iodevice));
setupConnections(reply());
sendRequest(_verb, _link, req);
AbstractNetworkJob::start();
}

Просмотреть файл

@ -93,9 +93,7 @@ void OcsJob::start()
queryItems.append(qMakePair(QByteArray("format"), QByteArray("json")));
url.setEncodedQueryItems(queryItems);
setReply(davRequest(_verb, url, req, buffer));
setupConnections(reply());
buffer->setParent(reply());
sendRequest(_verb, url, req, buffer);
AbstractNetworkJob::start();
}

Просмотреть файл

@ -594,9 +594,7 @@ DetermineAuthTypeJob::DetermineAuthTypeJob(AccountPtr account, QObject *parent)
void DetermineAuthTypeJob::start()
{
QNetworkReply *reply = getRequest(account()->davPath());
setReply(reply);
setupConnections(reply);
sendRequest("GET", account()->davUrl());
AbstractNetworkJob::start();
}
@ -613,8 +611,7 @@ bool DetermineAuthTypeJob::finished()
// do a new run
_redirects++;
resetTimeout();
setReply(getRequest(redirection));
setupConnections(reply());
sendRequest("GET", redirection);
return false; // don't discard
} else {
#ifndef NO_SHIBBOLETH

Просмотреть файл

@ -27,8 +27,7 @@ ThumbnailJob::ThumbnailJob(const QString &path, AccountPtr account, QObject* par
void ThumbnailJob::start()
{
setReply(getRequest(path()));
setupConnections(reply());
sendRequest("GET", makeAccountUrl(path()));
AbstractNetworkJob::start();
}

Просмотреть файл

@ -122,40 +122,49 @@ QNetworkReply* AbstractNetworkJob::addTimer(QNetworkReply *reply)
return reply;
}
QNetworkReply* AbstractNetworkJob::davRequest(const QByteArray &verb, const QString &relPath,
QNetworkRequest req, QIODevice *data)
QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUrl &url,
QNetworkRequest req, QIODevice *requestBody)
{
return addTimer(_account->davRequest(verb, relPath, req, data));
auto reply = _account->sendRequest(verb, url, req, requestBody);
_requestBody = requestBody;
if (_requestBody) {
_requestBody->setParent(reply);
}
addTimer(reply);
setReply(reply);
setupConnections(reply);
return reply;
}
QNetworkReply *AbstractNetworkJob::davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
QUrl AbstractNetworkJob::makeAccountUrl(const QString& relativePath) const
{
return addTimer(_account->davRequest(verb, url, req, data));
return Utility::concatUrlPath(_account->url(), relativePath);
}
QNetworkReply* AbstractNetworkJob::getRequest(const QString &relPath)
QUrl AbstractNetworkJob::makeDavUrl(const QString& relativePath) const
{
return addTimer(_account->getRequest(relPath));
return Utility::concatUrlPath(_account->davUrl(), relativePath);
}
QNetworkReply *AbstractNetworkJob::getRequest(const QUrl &url)
QByteArray AbstractNetworkJob::requestVerb(QNetworkReply* reply)
{
return addTimer(_account->getRequest(url));
}
QNetworkReply *AbstractNetworkJob::headRequest(const QString &relPath)
{
return addTimer(_account->headRequest(relPath));
}
QNetworkReply *AbstractNetworkJob::headRequest(const QUrl &url)
{
return addTimer(_account->headRequest(url));
}
QNetworkReply *AbstractNetworkJob::deleteRequest(const QUrl &url)
{
return addTimer(_account->deleteRequest(url));
switch (reply->operation()) {
case QNetworkAccessManager::HeadOperation:
return "HEAD";
case QNetworkAccessManager::GetOperation:
return "GET";
case QNetworkAccessManager::PutOperation:
return "PUT";
case QNetworkAccessManager::PostOperation:
return "POST";
case QNetworkAccessManager::DeleteOperation:
return "DELETE";
case QNetworkAccessManager::CustomOperation:
return reply->request().attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
case QNetworkAccessManager::UnknownOperation:
break;
}
return QByteArray();
}
void AbstractNetworkJob::slotFinished()
@ -178,24 +187,36 @@ void AbstractNetworkJob::slotFinished()
// get the Date timestamp from reply
_responseTimestamp = _reply->rawHeader("Date");
if (_followRedirects) {
// ### the qWarnings here should be exported via displayErrors() so they
QUrl requestedUrl = reply()->request().url();
QUrl redirectUrl = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (_followRedirects && !redirectUrl.isEmpty()) {
_redirectCount++;
// ### some of the qWarnings here should be exported via displayErrors() so they
// ### can be presented to the user if the job executor has a GUI
QUrl requestedUrl = reply()->request().url();
QUrl redirectUrl = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (!redirectUrl.isEmpty()) {
_redirectCount++;
if (requestedUrl.scheme() == QLatin1String("https") &&
redirectUrl.scheme() == QLatin1String("http")) {
qWarning() << this << "HTTPS->HTTP downgrade detected!";
} else if (requestedUrl == redirectUrl || _redirectCount >= maxRedirects()) {
qWarning() << this << "Redirect loop detected!";
} else {
resetTimeout();
setReply(getRequest(redirectUrl));
setupConnections(reply());
return;
QByteArray verb = requestVerb(reply());
if (requestedUrl.scheme() == QLatin1String("https") &&
redirectUrl.scheme() == QLatin1String("http")) {
qWarning() << this << "HTTPS->HTTP downgrade detected!";
} else if (requestedUrl == redirectUrl || _redirectCount >= maxRedirects()) {
qWarning() << this << "Redirect loop detected!";
} else if (_requestBody && _requestBody->isSequential()) {
qWarning() << this << "cannot redirect request with sequential body";
} else if (verb.isEmpty()) {
qWarning() << this << "cannot redirect request: could not detect original verb";
} else {
// Create the redirected request and send it
qDebug() << "Redirecting" << verb << requestedUrl << redirectUrl;
resetTimeout();
if (_requestBody) {
_requestBody->seek(0);
}
sendRequest(
verb,
redirectUrl,
reply()->request(),
_requestBody);
return;
}
}

Просмотреть файл

@ -78,17 +78,29 @@ signals:
void networkActivity();
protected:
void setupConnections(QNetworkReply *reply);
QNetworkReply* davRequest(const QByteArray& verb, const QString &relPath,
QNetworkRequest req = QNetworkRequest(),
QIODevice *data = 0);
QNetworkReply* davRequest(const QByteArray& verb, const QUrl &url,
QNetworkRequest req = QNetworkRequest(),
QIODevice *data = 0);
QNetworkReply* getRequest(const QString &relPath);
QNetworkReply* getRequest(const QUrl &url);
QNetworkReply* headRequest(const QString &relPath);
QNetworkReply* headRequest(const QUrl &url);
QNetworkReply* deleteRequest(const QUrl &url);
/** Initiate a network request, returning a QNetworkReply.
*
* Calls setReply() and setupConnections() on it.
*
* Takes ownership of the requestBody (to allow redirects).
*/
QNetworkReply* sendRequest(const QByteArray& verb, const QUrl &url,
QNetworkRequest req = QNetworkRequest(),
QIODevice *requestBody = 0);
// sendRequest does not take a relative path instead of an url,
// but the old API allowed that. We have this undefined overload
// to help catch usage errors
QNetworkReply* sendRequest(const QByteArray& verb, const QString &relativePath,
QNetworkRequest req = QNetworkRequest(),
QIODevice *requestBody = 0);
/// Creates a url for the account from a relative path
QUrl makeAccountUrl(const QString& relativePath) const;
/// Like makeAccountUrl() but uses the account's dav base path
QUrl makeDavUrl(const QString& relativePath) const;
int maxRedirects() const { return 10; }
virtual bool finished() = 0;
@ -99,12 +111,19 @@ protected:
// GET requests that don't set up any HTTP body or other flags.
bool _followRedirects;
/** Helper to construct the HTTP verb used in the request
*
* Returns an empty QByteArray for UnknownOperation.
*/
static QByteArray requestVerb(QNetworkReply* reply);
private slots:
void slotFinished();
virtual void slotTimeout();
protected:
AccountPtr _account;
private:
QNetworkReply* addTimer(QNetworkReply *reply);
bool _ignoreCredentialFailure;
@ -112,6 +131,12 @@ private:
QString _path;
QTimer _timer;
int _redirectCount;
// Set by the xyzRequest() functions and needed to be able to redirect
// requests, should it be required.
//
// Reparented to the currently running QNetworkReply.
QPointer<QIODevice> _requestBody;
};
/**

Просмотреть файл

@ -202,54 +202,23 @@ QNetworkAccessManager *Account::networkAccessManager()
return _am.data();
}
QNetworkReply *Account::headRequest(const QString &relPath)
{
return headRequest(Utility::concatUrlPath(url(), relPath));
}
QNetworkReply *Account::headRequest(const QUrl &url)
{
QNetworkRequest request(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
request.setSslConfiguration(this->getOrCreateSslConfig());
#endif
return _am->head(request);
}
QNetworkReply *Account::getRequest(const QString &relPath)
{
return getRequest(Utility::concatUrlPath(url(), relPath));
}
QNetworkReply *Account::getRequest(const QUrl &url)
{
QNetworkRequest request(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
request.setSslConfiguration(this->getOrCreateSslConfig());
#endif
return _am->get(request);
}
QNetworkReply *Account::deleteRequest( const QUrl &url)
{
QNetworkRequest request(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
request.setSslConfiguration(this->getOrCreateSslConfig());
#endif
return _am->deleteResource(request);
}
QNetworkReply *Account::davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data)
{
return davRequest(verb, Utility::concatUrlPath(davUrl(), relPath), req, data);
}
QNetworkReply *Account::davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
QNetworkReply *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
{
req.setUrl(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
req.setSslConfiguration(this->getOrCreateSslConfig());
#endif
if (verb == "HEAD" && !data) {
return _am->head(req);
} else if (verb == "GET" && !data) {
return _am->get(req);
} else if (verb == "POST") {
return _am->post(req, data);
} else if (verb == "PUT") {
return _am->put(req, data);
} else if (verb == "DELETE" && !data) {
return _am->deleteResource(req);
}
return _am->sendCustomRequest(req, verb, data);
}

Просмотреть файл

@ -106,14 +106,10 @@ public:
// For creating various network requests
QNetworkReply* headRequest(const QString &relPath);
QNetworkReply* headRequest(const QUrl &url);
QNetworkReply* getRequest(const QString &relPath);
QNetworkReply* getRequest(const QUrl &url);
QNetworkReply* deleteRequest( const QUrl &url);
QNetworkReply* davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data = 0);
QNetworkReply* davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data = 0);
QNetworkReply* sendRequest(const QByteArray &verb,
const QUrl &url,
QNetworkRequest req = QNetworkRequest(),
QIODevice *data = 0);
/** The ssl configuration during the first connection */
QSslConfiguration getOrCreateSslConfig();

Просмотреть файл

@ -67,9 +67,7 @@ void RequestEtagJob::start()
buf->setData(xml);
buf->open(QIODevice::ReadOnly);
// assumes ownership
setReply(davRequest("PROPFIND", path(), req, buf));
buf->setParent(reply());
setupConnections(reply());
sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
if( reply()->error() != QNetworkReply::NoError ) {
qDebug() << "getting etag: request network error: " << reply()->errorString();
@ -122,10 +120,11 @@ void MkColJob::start()
}
// assumes ownership
QNetworkReply *reply = _url.isValid() ? davRequest("MKCOL", _url, req)
: davRequest("MKCOL", path(), req);
setReply(reply);
setupConnections(reply);
if (_url.isValid()) {
sendRequest("MKCOL", _url, req);
} else {
sendRequest("MKCOL", makeDavUrl(path()), req);
}
AbstractNetworkJob::start();
}
@ -322,11 +321,11 @@ void LsColJob::start()
QBuffer *buf = new QBuffer(this);
buf->setData(xml);
buf->open(QIODevice::ReadOnly);
QNetworkReply *reply = _url.isValid() ? davRequest("PROPFIND", _url, req, buf)
: davRequest("PROPFIND", path(), req, buf);
buf->setParent(reply);
setReply(reply);
setupConnections(reply);
if (_url.isValid()) {
sendRequest("PROPFIND", _url, req, buf);
} else {
sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
}
AbstractNetworkJob::start();
}
@ -380,8 +379,7 @@ CheckServerJob::CheckServerJob(AccountPtr account, QObject *parent)
void CheckServerJob::start()
{
setReply(getRequest(path()));
setupConnections(reply());
sendRequest("GET", makeAccountUrl(path()));
connect(reply(), SIGNAL(metaDataChanged()), this, SLOT(metaDataChangedSlot()));
connect(reply(), SIGNAL(encrypted()), this, SLOT(encryptedSlot()));
AbstractNetworkJob::start();
@ -529,9 +527,7 @@ void PropfindJob::start()
QBuffer *buf = new QBuffer(this);
buf->setData(xml);
buf->open(QIODevice::ReadOnly);
setReply(davRequest("PROPFIND", path(), req, buf));
buf->setParent(reply());
setupConnections(reply());
sendRequest("PROPFIND", makeDavUrl(path()), req, buf);
AbstractNetworkJob::start();
}
@ -632,9 +628,7 @@ void ProppatchJob::start()
QBuffer *buf = new QBuffer(this);
buf->setData(xml);
buf->open(QIODevice::ReadOnly);
setReply(davRequest("PROPPATCH", path(), req, buf));
buf->setParent(reply());
setupConnections(reply());
sendRequest("PROPPATCH", makeDavUrl(path()), req, buf);
AbstractNetworkJob::start();
}
@ -671,8 +665,7 @@ EntityExistsJob::EntityExistsJob(AccountPtr account, const QString &path, QObjec
void EntityExistsJob::start()
{
setReply(headRequest(path()));
setupConnections(reply());
sendRequest("HEAD", makeAccountUrl(path()));
AbstractNetworkJob::start();
}
@ -700,8 +693,7 @@ void JsonApiJob::start()
QList<QPair<QString, QString> > params = _additionalParams;
params << qMakePair(QString::fromLatin1("format"), QString::fromLatin1("json"));
url.setQueryItems(params);
setReply(davRequest("GET", url, req));
setupConnections(reply());
sendRequest("GET", url, req);
AbstractNetworkJob::start();
}

Просмотреть файл

@ -100,12 +100,11 @@ void GETFileJob::start() {
}
if (_directDownloadUrl.isEmpty()) {
setReply(davRequest("GET", path(), req));
sendRequest("GET", makeDavUrl(path()), req);
} else {
// Use direct URL
setReply(davRequest("GET", _directDownloadUrl, req));
sendRequest("GET", _directDownloadUrl, req);
}
setupConnections(reply());
reply()->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
qDebug() << Q_FUNC_INFO << _bandwidthManager << _bandwidthChoked << _bandwidthLimited;

Просмотреть файл

@ -30,8 +30,11 @@ DeleteJob::DeleteJob(AccountPtr account, const QUrl& url, QObject* parent)
void DeleteJob::start()
{
QNetworkRequest req;
setReply(_url.isValid() ? davRequest("DELETE", _url, req) : davRequest("DELETE", path(), req));
setupConnections(reply());
if (_url.isValid()) {
sendRequest("DELETE", _url, req);
} else {
sendRequest("DELETE", makeDavUrl(path()), req);
}
if( reply()->error() != QNetworkReply::NoError ) {
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();

Просмотреть файл

@ -43,8 +43,11 @@ void MoveJob::start()
for(auto it = _extraHeaders.constBegin(); it != _extraHeaders.constEnd(); ++it) {
req.setRawHeader(it.key(), it.value());
}
setReply(_url.isValid() ? davRequest("MOVE", _url, req) : davRequest("MOVE", path(), req));
setupConnections(reply());
if (_url.isValid()) {
sendRequest("MOVE", _url, req);
} else {
sendRequest("MOVE", makeDavUrl(path()), req);
}
if( reply()->error() != QNetworkReply::NoError ) {
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();

Просмотреть файл

@ -73,9 +73,11 @@ void PUTFileJob::start() {
req.setRawHeader(it.key(), it.value());
}
setReply(_url.isValid() ? davRequest("PUT", _url, req, _device.data())
: davRequest("PUT", path(), req, _device.data()));
setupConnections(reply());
if (_url.isValid()) {
sendRequest("PUT", _url, req, _device);
} else {
sendRequest("PUT", makeDavUrl(path()), req, _device);
}
if( reply()->error() != QNetworkReply::NoError ) {
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();
@ -89,10 +91,10 @@ void PUTFileJob::start() {
// (workaround disabled on windows and mac because the binaries we ship have patched qt)
#if QT_VERSION < QT_VERSION_CHECK(4, 8, 7)
if (QLatin1String(qVersion()) < QLatin1String("4.8.7"))
connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
connect(_device, SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
#elif QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(5, 4, 2) && !defined Q_OS_WIN && !defined Q_OS_MAC
if (QLatin1String(qVersion()) < QLatin1String("5.4.2"))
connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
connect(_device, SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
#endif
AbstractNetworkJob::start();
@ -119,8 +121,7 @@ void PollJob::start()
QUrl accountUrl = account()->url();
QUrl finalUrl = QUrl::fromUserInput(accountUrl.scheme() + QLatin1String("://") + accountUrl.authority()
+ (path().startsWith('/') ? QLatin1String("") : QLatin1String("/")) + path());
setReply(getRequest(finalUrl));
setupConnections(reply());
sendRequest("GET", finalUrl);
connect(reply(), SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(resetTimeout()));
AbstractNetworkJob::start();
}

Просмотреть файл

@ -86,7 +86,7 @@ class PUTFileJob : public AbstractNetworkJob {
Q_OBJECT
private:
QScopedPointer<QIODevice> _device;
QIODevice* _device;
QMap<QByteArray, QByteArray> _headers;
QString _errorString;
QUrl _url;
@ -95,11 +95,17 @@ public:
// Takes ownership of the device
explicit PUTFileJob(AccountPtr account, const QString& path, QIODevice *device,
const QMap<QByteArray, QByteArray> &headers, int chunk, QObject* parent = 0)
: AbstractNetworkJob(account, path, parent), _device(device), _headers(headers), _chunk(chunk) {}
: AbstractNetworkJob(account, path, parent), _device(device), _headers(headers), _chunk(chunk)
{
_device->setParent(this);
}
explicit PUTFileJob(AccountPtr account, const QUrl& url, QIODevice *device,
const QMap<QByteArray, QByteArray> &headers, int chunk, QObject* parent = 0)
: AbstractNetworkJob(account, QString(), parent), _device(device), _headers(headers)
, _url(url), _chunk(chunk) {}
, _url(url), _chunk(chunk)
{
_device->setParent(this);
}
~PUTFileJob();
int _chunk;

Просмотреть файл

@ -730,13 +730,13 @@ protected:
if (verb == QLatin1String("PROPFIND"))
// Ignore outgoingData always returning somethign good enough, works for now.
return new FakePropfindReply{info, op, request, this};
else if (verb == QLatin1String("GET"))
else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
return new FakeGetReply{info, op, request, this};
else if (verb == QLatin1String("PUT"))
else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
return new FakePutReply{info, op, request, outgoingData->readAll(), this};
else if (verb == QLatin1String("MKCOL"))
return new FakeMkcolReply{info, op, request, this};
else if (verb == QLatin1String("DELETE"))
else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
return new FakeDeleteReply{info, op, request, this};
else if (verb == QLatin1String("MOVE") && !isUpload)
return new FakeMoveReply{info, op, request, this};