diff options
Diffstat (limited to 'lib/connection.cpp')
-rw-r--r-- | lib/connection.cpp | 256 |
1 files changed, 185 insertions, 71 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index ac69228b..0c98c383 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -32,12 +32,13 @@ #include "csapi/joining.h" #include "csapi/to_device.h" #include "csapi/room_send.h" +#include "csapi/wellknown.h" +#include "csapi/versions.h" #include "jobs/syncjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" #include "csapi/voip.h" -#include <QtNetwork/QDnsLookup> #include <QtCore/QFile> #include <QtCore/QDir> #include <QtCore/QStandardPaths> @@ -79,10 +80,10 @@ class Connection::Private std::unique_ptr<ConnectionData> data; // A complex key below is a pair of room name and whether its // state is Invited. The spec mandates to keep Invited room state - // separately so we should, e.g., keep objects for Invite and - // Leave state of the same room. + // separately; specifically, we should keep objects for Invite and + // Leave state of the same room if the two happen to co-exist. QHash<QPair<QString, bool>, Room*> roomMap; - // Mapping from aliases to room ids, as per the last sync + /// Mapping from aliases to room ids, as of the last sync QHash<QString, QString> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; @@ -101,6 +102,8 @@ class Connection::Private GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; + QVector<GetLoginFlowsJob::LoginFlow> loginFlows; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -108,8 +111,10 @@ class Connection::Private .value("cache_type").toString() != "json"; bool lazyLoading = false; - void connectWithToken(const QString& user, const QString& accessToken, - const QString& deviceId); + template <typename... LoginArgTs> + void loginToServer(LoginArgTs&&... loginArgs); + void assumeIdentity(const QString& newUserId, const QString& accessToken, + const QString& deviceId); template <typename EventT> EventT* unpackAccountData() const @@ -170,44 +175,73 @@ void Connection::resolveServer(const QString& mxidOrDomain) maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" if (!match.hasMatch() || !maybeBaseUrl.isValid()) { - emit resolveError( - tr("%1 is not a valid homeserver address") - .arg(maybeBaseUrl.toString())); + emit resolveError(tr("%1 is not a valid homeserver address") + .arg(maybeBaseUrl.toString())); return; } - setHomeserver(maybeBaseUrl); - emit resolved(); - return; - - // FIXME, #178: The below code is incorrect and is no more executed. The - // correct server resolution should be done from .well-known/matrix/client auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; - // Check if the Matrix server has a dedicated service record. - auto* dns = new QDnsLookup(); - dns->setType(QDnsLookup::SRV); - dns->setName("_matrix._tcp." + domain); - - connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { - QUrl baseUrl { maybeBaseUrl }; - if (dns->error() == QDnsLookup::NoError && - dns->serviceRecords().isEmpty()) - { - auto record = dns->serviceRecords().front(); - baseUrl.setHost(record.target()); - baseUrl.setPort(record.port()); - qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); - } else { - qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" - << dns->name() << "- using the hostname as is"; - } - setHomeserver(baseUrl); - emit resolved(); - dns->deleteLater(); - }); - dns->lookup(); + + d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file + auto getWellKnownJob = callApi<GetWellknownJob>(); + // This is a workaround for 0.5.x; due to the way Quaternion's login dialog + // operates, Connection can disappear any moment during server resolution. + // Quotient 0.6 will reparent all jobs to enforce lifetimes. See also #398. + getWellKnownJob->setParent(this); + connect(getWellKnownJob, &BaseJob::finished, this, + [this, getWellKnownJob, maybeBaseUrl] { + if (getWellKnownJob->status() != BaseJob::NotFoundError) { + if (getWellKnownJob->status() != BaseJob::Success) { + qCWarning(MAIN) + << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Failed resolving the homeserver")); + return; + } + QUrl baseUrl { getWellKnownJob->data().homeserver.baseUrl }; + if (baseUrl.isEmpty()) { + qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError( + tr("The homeserver base URL is not provided")); + return; + } + if (!baseUrl.isValid()) { + qCWarning(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("The homeserver base URL is invalid")); + return; + } + qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() + << "is" << baseUrl.authority(); + setHomeserver(baseUrl); + } else { + qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl + << "for base URL"; + setHomeserver(maybeBaseUrl); + } + + auto getVersionsJob = callApi<GetVersionsJob>(); + getVersionsJob->setParent(this); // Same workaround as above + connect(getVersionsJob, &BaseJob::success, this, + &Connection::resolved); + connect(getVersionsJob, &BaseJob::failure, this, [this] { + qCWarning(MAIN) << "Homeserver base URL invalid"; + emit resolveError(tr("The homeserver base URL " + "doesn't seem to work")); + }); + }); +} + +inline UserIdentifier makeUserIdentifier(const QString& id) +{ + return { QStringLiteral("m.id.user"), { { QStringLiteral("user"), id } } }; +} + +inline UserIdentifier make3rdPartyIdentifier(const QString& medium, + const QString& address) +{ + return { QStringLiteral("m.id.thirdparty"), + { { QStringLiteral("medium"), medium }, + { QStringLiteral("address"), address } } }; } void Connection::connectToServer(const QString& user, const QString& password, @@ -219,23 +253,28 @@ void Connection::connectToServer(const QString& user, const QString& password, doConnectToServer(user, password, initialDeviceName, deviceId); }); } + void Connection::doConnectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId) { - auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"), - UserIdentifier { QStringLiteral("m.id.user"), - {{ QStringLiteral("user"), user }} }, - password, /*token*/ "", deviceId, initialDeviceName); - connect(loginJob, &BaseJob::success, this, - [this, loginJob] { - d->connectWithToken(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); - }); - connect(loginJob, &BaseJob::failure, this, - [this, loginJob] { - emit loginError(loginJob->errorString(), loginJob->rawDataSample()); - }); + d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(user), + password, /*token*/ "", deviceId, initialDeviceName); +} + +SsoSession* Connection::prepareForSso(const QString& initialDeviceName, + const QString& deviceId) +{ + return new SsoSession(this, initialDeviceName, deviceId); +} + +void Connection::loginWithToken(const QByteArray& loginToken, + const QString& initialDeviceName, + const QString& deviceId) +{ + d->loginToServer(LoginFlows::Token.type, + makeUserIdentifier(/*user is encoded in loginToken*/ {}), + /*password*/ "", loginToken, deviceId, initialDeviceName); } void Connection::syncLoopIteration() @@ -247,8 +286,15 @@ void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) { + assumeIdentity(userId, accessToken, deviceId); +} + +void Connection::assumeIdentity(const QString& userId, + const QString& accessToken, + const QString& deviceId) +{ checkAndConnect(userId, - [=] { d->connectWithToken(userId, accessToken, deviceId); }); + [=] { d->assumeIdentity(userId, accessToken, deviceId); }); } void Connection::reloadCapabilities() @@ -283,11 +329,25 @@ bool Connection::loadingCapabilities() const return d->capabilities.roomVersions.omitted(); } -void Connection::Private::connectWithToken(const QString& user, - const QString& accessToken, - const QString& deviceId) +template <typename... LoginArgTs> +void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) +{ + auto loginJob = + q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...); + connect(loginJob, &BaseJob::success, q, [this, loginJob] { + assumeIdentity(loginJob->userId(), loginJob->accessToken(), + loginJob->deviceId()); + }); + connect(loginJob, &BaseJob::failure, q, [this, loginJob] { + emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); + }); +} + +void Connection::Private::assumeIdentity(const QString& newUserId, + const QString& accessToken, + const QString& deviceId) { - userId = user; + userId = newUserId; q->user(); // Creates a User object for the local user data->setToken(accessToken.toLatin1()); data->setDeviceId(deviceId); @@ -850,6 +910,21 @@ QString Connection::domain() const return d->userId.section(':', 1); } +QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const +{ + return d->loginFlows; +} + +bool Connection::supportsPasswordAuth() const +{ + return d->loginFlows.contains(LoginFlows::Password); +} + +bool Connection::supportsSso() const +{ + return d->loginFlows.contains(LoginFlows::SSO); +} + Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({roomId, false}, nullptr); @@ -979,6 +1054,33 @@ QHash< QPair<QString, bool>, Room* > Connection::roomMap() const return roomMap; } +QVector<Room*> Connection::allRooms() const +{ + QVector<Room*> result; + result.resize(d->roomMap.size()); + std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin()); + return result; +} + +QVector<Room*> Connection::rooms(JoinStates joinStates) const +{ + QVector<Room*> result; + for (auto* r: qAsConst(d->roomMap)) + if (joinStates.testFlag(r->joinState())) + result.push_back(r); + return result; +} + +int Connection::roomsCount(JoinStates joinStates) const +{ + // Using int to maintain compatibility with QML + // (consider also that QHash<>::size() returns int anyway). + return int(std::count_if(d->roomMap.begin(), d->roomMap.end(), + [joinStates](Room* r) { + return joinStates.testFlag(r->joinState()); + })); +} + bool Connection::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.cend(); @@ -1244,11 +1346,21 @@ QByteArray Connection::generateTxnId() const void Connection::setHomeserver(const QUrl& url) { - if (homeserver() == url) - return; + if (homeserver() != url) { + d->data->setBaseUrl(url); + d->loginFlows.clear(); + emit homeserverChanged(homeserver()); + } - d->data->setBaseUrl(url); - emit homeserverChanged(homeserver()); + // Whenever a homeserver is updated, retrieve available login flows from it + auto* j = callApi<GetLoginFlowsJob>(BackgroundRequest); + connect(j, &BaseJob::finished, this, [this, j] { + if (j->status().good()) + d->loginFlows = j->flows(); + else + d->loginFlows.clear(); + emit loginFlowsChanged(); + }); } void Connection::saveRoomState(Room* r) const @@ -1294,18 +1406,20 @@ void Connection::saveState() const { QStringLiteral("minor"), SyncData::cacheVersion().second } }}}; { - QJsonObject rooms; - QJsonObject inviteRooms; - const auto& rs = roomMap(); // Pass on rooms in Leave state - for (const auto* i : rs) - (i->joinState() == JoinState::Invite ? inviteRooms : rooms) - .insert(i->id(), QJsonValue::Null); + QJsonObject roomsJson; + QJsonObject inviteRoomsJson; + for (const auto* r: qAsConst(d->roomMap)) { + if (r->joinState() == JoinState::Leave) + continue; + (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson) + .insert(r->id(), QJsonValue::Null); + } QJsonObject roomObj; - if (!rooms.isEmpty()) - roomObj.insert(QStringLiteral("join"), rooms); - if (!inviteRooms.isEmpty()) - roomObj.insert(QStringLiteral("invite"), inviteRooms); + if (!roomsJson.isEmpty()) + roomObj.insert(QStringLiteral("join"), roomsJson); + if (!inviteRoomsJson.isEmpty()) + roomObj.insert(QStringLiteral("invite"), inviteRoomsJson); rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); rootObj.insert(QStringLiteral("rooms"), roomObj); |