diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 21df46b08597..b4ca6b3b7808 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -1233,6 +1233,9 @@ void AccountSettings::slotAccountStateChanged() case AccountState::MaintenanceMode: showConnectionLabel(tr("Server %1 is currently in maintenance mode.").arg(server)); break; + case AccountState::RedirectDetected: + showConnectionLabel(tr("Server %1 is currently being redirected, or your connection is behind a captive portal.").arg(server)); + break; case AccountState::SignedOut: showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser)); break; diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index 6bcf4e4cec70..f5938ad9a415 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -118,10 +118,10 @@ void AccountState::setState(State state) // If we stop being voluntarily signed-out, try to connect and // auth right now! checkConnectivity(); - } else if (_state == ServiceUnavailable) { - // Check if we are actually down for maintenance. + } else if (_state == ServiceUnavailable || _state == RedirectDetected) { + // Check if we are actually down for maintenance/in a redirect state (captive portal?). // To do this we must clear the connection validator that just - // produced the 503. It's finished anyway and will delete itself. + // produced the 503/302. It's finished anyway and will delete itself. _connectionValidator.clear(); checkConnectivity(); } @@ -150,6 +150,8 @@ QString AccountState::stateString(State state) return tr("Service unavailable"); case MaintenanceMode: return tr("Maintenance mode"); + case RedirectDetected: + return tr("Redirect detected"); case NetworkError: return tr("Network error"); case ConfigurationError: @@ -342,10 +344,11 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta _lastConnectionValidatorStatus = status; - // Come online gradually from 503 or maintenance mode + // Come online gradually from 503, captive portal(redirection) or maintenance mode if (status == ConnectionValidator::Connected && (_connectionStatus == ConnectionValidator::ServiceUnavailable - || _connectionStatus == ConnectionValidator::MaintenanceMode)) { + || _connectionStatus == ConnectionValidator::MaintenanceMode + || _connectionStatus == ConnectionValidator::StatusRedirect)) { if (!_timeSinceMaintenanceOver.isValid()) { qCInfo(lcAccountState) << "AccountState reconnection: delaying for" << _maintenanceToConnectedDelay << "ms"; @@ -411,6 +414,10 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta _timeSinceMaintenanceOver.invalidate(); setState(MaintenanceMode); break; + case ConnectionValidator::StatusRedirect: + _timeSinceMaintenanceOver.invalidate(); + setState(RedirectDetected); + break; case ConnectionValidator::Timeout: setState(NetworkError); updateRetryCount(); diff --git a/src/gui/accountstate.h b/src/gui/accountstate.h index 8ca27b38c3d9..fc6a4ab858fa 100644 --- a/src/gui/accountstate.h +++ b/src/gui/accountstate.h @@ -65,6 +65,10 @@ class AccountState : public QObject, public QSharedData /// don't bother the user too much and try again. ServiceUnavailable, + /// Connection is being redirected (likely a captive portal is in effect) + /// Do not proceed with connecting and check back later + RedirectDetected, + /// Similar to ServiceUnavailable, but we know the server is down /// for maintenance MaintenanceMode, diff --git a/src/gui/connectionvalidator.cpp b/src/gui/connectionvalidator.cpp index 515c47f8a974..f9b149787ed0 100644 --- a/src/gui/connectionvalidator.cpp +++ b/src/gui/connectionvalidator.cpp @@ -63,7 +63,7 @@ void ConnectionValidator::checkServerAndAuth() // We want to reset the QNAM proxy so that the global proxy settings are used (via ClientProxy settings) _account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy)); // use a queued invocation so we're as asynchronous as with the other code path - QMetaObject::invokeMethod(this, "slotCheckServerAndAuth", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "slotCheckRedirectCostFreeUrl", Qt::QueuedConnection); } } @@ -81,10 +81,21 @@ void ConnectionValidator::systemProxyLookupDone(const QNetworkProxy &proxy) } _account->networkAccessManager()->setProxy(proxy); - slotCheckServerAndAuth(); + slotCheckRedirectCostFreeUrl(); } // The actual check + +void ConnectionValidator::slotCheckRedirectCostFreeUrl() +{ + const auto checkJob = new CheckRedirectCostFreeUrlJob(_account, this); + checkJob->setTimeout(timeoutToUseMsec); + checkJob->setIgnoreCredentialFailure(true); + connect(checkJob, &CheckRedirectCostFreeUrlJob::timeout, this, &ConnectionValidator::slotJobTimeout); + connect(checkJob, &CheckRedirectCostFreeUrlJob::jobFinished, this, &ConnectionValidator::slotCheckRedirectCostFreeUrlFinished); + checkJob->start(); +} + void ConnectionValidator::slotCheckServerAndAuth() { auto *checkJob = new CheckServerJob(_account, this); @@ -96,6 +107,15 @@ void ConnectionValidator::slotCheckServerAndAuth() checkJob->start(); } +void ConnectionValidator::slotCheckRedirectCostFreeUrlFinished(int statusCode) +{ + if (statusCode >= 301 && statusCode <= 307) { + reportResult(StatusRedirect); + return; + } + slotCheckServerAndAuth(); +} + void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &info) { // Newer servers don't disclose any version in status.php anymore diff --git a/src/gui/connectionvalidator.h b/src/gui/connectionvalidator.h index 38b685c972e9..bd955d4128e6 100644 --- a/src/gui/connectionvalidator.h +++ b/src/gui/connectionvalidator.h @@ -90,6 +90,7 @@ class ConnectionValidator : public QObject CredentialsWrong, // AuthenticationRequiredError SslError, // SSL handshake error, certificate rejected by user? StatusNotFound, // Error retrieving status.php + StatusRedirect, // 204 URL received one of redirect HTTP codes (301-307), possibly a captive portal ServiceUnavailable, // 503 on authed request MaintenanceMode, // maintenance enabled in status.php Timeout // actually also used for other errors on the authed request @@ -111,8 +112,12 @@ public slots: void connectionResult(OCC::ConnectionValidator::Status status, const QStringList &errors); protected slots: + void slotCheckRedirectCostFreeUrl(); + void slotCheckServerAndAuth(); + void slotCheckRedirectCostFreeUrlFinished(int statusCode); + void slotStatusFound(const QUrl &url, const QJsonObject &info); void slotNoStatusFound(QNetworkReply *reply); void slotJobTimeout(const QUrl &url); diff --git a/src/libsync/abstractnetworkjob.cpp b/src/libsync/abstractnetworkjob.cpp index 369ae89eb556..5623c8125938 100644 --- a/src/libsync/abstractnetworkjob.cpp +++ b/src/libsync/abstractnetworkjob.cpp @@ -47,7 +47,7 @@ Q_LOGGING_CATEGORY(lcNetworkJob, "nextcloud.sync.networkjob", QtInfoMsg) // If not set, it is overwritten by the Application constructor with the value from the config int AbstractNetworkJob::httpTimeout = qEnvironmentVariableIntValue("OWNCLOUD_TIMEOUT"); -AbstractNetworkJob::AbstractNetworkJob(AccountPtr account, const QString &path, QObject *parent) +AbstractNetworkJob::AbstractNetworkJob(const AccountPtr &account, const QString &path, QObject *parent) : QObject(parent) , _account(account) , _reply(nullptr) diff --git a/src/libsync/abstractnetworkjob.h b/src/libsync/abstractnetworkjob.h index 9f7da9ca87f4..817f278b07dc 100644 --- a/src/libsync/abstractnetworkjob.h +++ b/src/libsync/abstractnetworkjob.h @@ -40,7 +40,7 @@ class OWNCLOUDSYNC_EXPORT AbstractNetworkJob : public QObject { Q_OBJECT public: - explicit AbstractNetworkJob(AccountPtr account, const QString &path, QObject *parent = nullptr); + explicit AbstractNetworkJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr); ~AbstractNetworkJob() override; virtual void start(); diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index cb6189cadc6b..ddbc08b3be28 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -50,6 +50,7 @@ namespace OCC { Q_LOGGING_CATEGORY(lcEtagJob, "nextcloud.sync.networkjob.etag", QtInfoMsg) Q_LOGGING_CATEGORY(lcLsColJob, "nextcloud.sync.networkjob.lscol", QtInfoMsg) Q_LOGGING_CATEGORY(lcCheckServerJob, "nextcloud.sync.networkjob.checkserver", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCheckRedirectCostFreeUrlJob, "nextcloud.sync.networkjob.checkredirectcostfreeurl", QtInfoMsg) Q_LOGGING_CATEGORY(lcPropfindJob, "nextcloud.sync.networkjob.propfind", QtInfoMsg) Q_LOGGING_CATEGORY(lcAvatarJob, "nextcloud.sync.networkjob.avatar", QtInfoMsg) Q_LOGGING_CATEGORY(lcMkColJob, "nextcloud.sync.networkjob.mkcol", QtInfoMsg) @@ -554,6 +555,42 @@ bool CheckServerJob::finished() /*********************************************************************************************/ +CheckRedirectCostFreeUrlJob::CheckRedirectCostFreeUrlJob(const AccountPtr &account, QObject *parent) + : AbstractNetworkJob(account, QLatin1String(statusphpC), parent) +{ + setIgnoreCredentialFailure(true); +} + +void CheckRedirectCostFreeUrlJob::start() +{ + setFollowRedirects(false); + sendRequest("GET", Utility::concatUrlPath(account()->url(), QStringLiteral("/index.php/204"))); + AbstractNetworkJob::start(); +} + +void CheckRedirectCostFreeUrlJob::onTimedOut() +{ + qCDebug(lcCheckRedirectCostFreeUrlJob) << "TIMEOUT"; + if (reply() && reply()->isRunning()) { + emit timeout(reply()->url()); + } else if (!reply()) { + qCDebug(lcCheckRedirectCostFreeUrlJob) << "Timeout without a reply?"; + } + AbstractNetworkJob::onTimedOut(); +} + +bool CheckRedirectCostFreeUrlJob::finished() +{ + const auto statusCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode >= 301 && statusCode <= 307) { + const auto redirectionTarget = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + qCDebug(lcCheckRedirectCostFreeUrlJob) << "Redirecting cost-free URL" << reply()->url() << " to" << redirectionTarget; + } + emit jobFinished(statusCode); + return true; +} +/*********************************************************************************************/ + PropfindJob::PropfindJob(AccountPtr account, const QString &path, QObject *parent) : AbstractNetworkJob(account, path, parent) { diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 478852d7c9e2..8682a597a8e0 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -364,6 +364,34 @@ private slots: int _permanentRedirects = 0; }; +/** + * @brief The CheckRedirectCostFreeUrlJob class + * @ingroup libsync + */ +class OWNCLOUDSYNC_EXPORT CheckRedirectCostFreeUrlJob : public AbstractNetworkJob +{ + Q_OBJECT +public: + explicit CheckRedirectCostFreeUrlJob(const AccountPtr &account, QObject *parent = nullptr); + void start() override; + +signals: + /** + * a check is finished + * \a statusCode cost-free URL GET HTTP response code + */ + void jobFinished(int statusCode); + /** A timeout occurred. + * + * \a url The specific url where the timeout happened. + */ + void timeout(const QUrl &url); + +private: + bool finished() override; + void onTimedOut() override; +}; + /** * @brief The RequestEtagJob class