1130 lines · 46.2 KB · C++
RawDownload
////  AccountManager.cpp//  libraries/networking/src////  Created by Stephen Birarda on 2/18/2014.//  Copyright 2014 High Fidelity, Inc.//  Copyright 2023-2024 Overte e.V.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "AccountManager.h" #include <memory> #include <QtCore/QDataStream>#include <QtCore/QDir>#include <QtCore/QFile>#include <QtCore/QJsonDocument>#include <QtCore/QJsonObject>#include <QtCore/QMap>#include <QtCore/QStringList>#include <QtCore/QStandardPaths>#include <QtCore/QUrlQuery>#include <QtCore/QThreadPool>#include <QtNetwork/QHttpMultiPart>#include <QtNetwork/QNetworkRequest>#include <QtNetwork/QNetworkReply>#include <qthread.h> #include <SettingHandle.h> #include "NetworkLogging.h"#include "NodeList.h"#include "udt/PacketHeaders.h"#include "RSAKeypairGenerator.h"#include "SharedUtil.h"#include "UserActivityLogger.h"  const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; Q_DECLARE_METATYPE(OAuthAccessToken)Q_DECLARE_METATYPE(DataServerAccountInfo)Q_DECLARE_METATYPE(QNetworkAccessManager::Operation)Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; const int POST_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND;const int PULL_SETTINGS_RETRY_INTERVAL = 2 * MSECS_PER_SECOND;const int MAX_PULL_RETRIES = 10; JSONCallbackParameters::JSONCallbackParameters(QObject* callbackReceiver,    const QString& jsonCallbackMethod,    const QString& errorCallbackMethod,    const QJsonObject& callbackData) :    callbackReceiver(callbackReceiver),    jsonCallbackMethod(jsonCallbackMethod),    errorCallbackMethod(errorCallbackMethod),    callbackData(callbackData){ } QJsonObject AccountManager::dataObjectFromResponse(QNetworkReply* requestReply) {    QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object();     static const QString STATUS_KEY = "status";    static const QString DATA_KEY = "data";     if (jsonObject.contains(STATUS_KEY) && jsonObject[STATUS_KEY] == "success" && jsonObject.contains(DATA_KEY)) {        return jsonObject[DATA_KEY].toObject();    } else {        return QJsonObject();    }} AccountManager::AccountManager(bool accountSettingsEnabled, UserAgentGetter userAgentGetter) :    _userAgentGetter(userAgentGetter),    _authURL(),    _accountSettingsEnabled(accountSettingsEnabled){    qRegisterMetaType<OAuthAccessToken>("OAuthAccessToken");    qRegisterMetaTypeStreamOperators<OAuthAccessToken>("OAuthAccessToken");     qRegisterMetaType<DataServerAccountInfo>("DataServerAccountInfo");    qRegisterMetaTypeStreamOperators<DataServerAccountInfo>("DataServerAccountInfo");     qRegisterMetaType<QNetworkAccessManager::Operation>("QNetworkAccessManager::Operation");    qRegisterMetaType<JSONCallbackParameters>("JSONCallbackParameters");     qRegisterMetaType<QHttpMultiPart*>("QHttpMultiPart*");     qRegisterMetaType<AccountManagerAuth::Type>();    connect(this, &AccountManager::loginComplete, this, &AccountManager::uploadPublicKey);    connect(this, &AccountManager::loginComplete, this, &AccountManager::requestAccountSettings);     _pullSettingsRetryTimer = new QTimer(this);    _pullSettingsRetryTimer->setSingleShot(true);    _pullSettingsRetryTimer->setInterval(PULL_SETTINGS_RETRY_INTERVAL);    connect(_pullSettingsRetryTimer, &QTimer::timeout, this, &AccountManager::requestAccountSettings);     _postSettingsTimer = new QTimer(this);    _postSettingsTimer->setInterval(POST_SETTINGS_INTERVAL);    connect(this, SIGNAL(accountSettingsLoaded()), _postSettingsTimer, SLOT(start()));    connect(this, &AccountManager::logoutComplete, _postSettingsTimer, &QTimer::stop);    connect(_postSettingsTimer, &QTimer::timeout, this, &AccountManager::postAccountSettings);    connect(qApp, &QCoreApplication::aboutToQuit, this, &AccountManager::postAccountSettings);} const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; void AccountManager::logout() {    postAccountSettings();    _numPullRetries = 0;     // a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file    _accountInfo = DataServerAccountInfo();     // remove this account from the account settings file    removeAccountFromFile();    saveLoginStatus(false);     emit logoutComplete();    // the username has changed to blank    emit usernameChanged(QString());     _settings.loggedOut();} QString accountFileDir() {#if defined(Q_OS_ANDROID)    return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/../files";#else    return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);#endif} QString accountFilePath() {    return accountFileDir() + "/AccountInfo.bin";} QVariantMap accountMapFromFile(bool& success) {    QFile accountFile { accountFilePath() };     if (accountFile.open(QIODevice::ReadOnly)) {        // grab the current QVariantMap from the settings file        QDataStream readStream(&accountFile);        QVariantMap accountMap;         readStream >> accountMap;         // close the file now that we have read the data        accountFile.close();         success = true;         return accountMap;    } else {        // failed to open file, return empty QVariantMap        // there was only an error if the account file existed when we tried to load it        success = !accountFile.exists();         return QVariantMap();    }} void AccountManager::setAuthURL(const QUrl& authURL) {    if (_authURL != authURL) {        _authURL = authURL;         qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString());         // check if there are existing access tokens to load from settings        QFile accountsFile { accountFilePath() };        bool loadedMap = false;        auto accountsMap = accountMapFromFile(loadedMap);         if (accountsFile.exists() && loadedMap) {            // pull out the stored account info and store it in memory            _accountInfo = accountsMap[_authURL.toString()].value<DataServerAccountInfo>();             qCDebug(networking) << "Found directory services API account information for" << qPrintable(_authURL.toString());        } else {            qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded.";        }         if (_isAgent && !_accountInfo.getAccessToken().token.isEmpty() && !_accountInfo.hasProfile()) {            // we are missing profile information, request it now            requestProfile();        }         // prepare to refresh our token if it is about to expire        if (needsToRefreshToken()) {            refreshAccessToken();        }         if (isLoggedIn()) {            emit loginComplete(_authURL);        }         // tell listeners that the auth endpoint has changed        emit authEndpointChanged();    }} void AccountManager::updateAuthURLFromMetaverseServerURL() {    setAuthURL(MetaverseAPI::getCurrentMetaverseServerURL());} void AccountManager::setSessionID(const QUuid& sessionID) {    if (_sessionID != sessionID) {        qCDebug(networking) << "Directory Services session ID changed to" << uuidStringWithoutCurlyBraces(sessionID);        _sessionID = sessionID;    }} QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) {    QNetworkRequest networkRequest;    networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);    networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());     networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER,                                uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());     QUrl requestURL = _authURL;    if (requestURL.isEmpty()) {  // Assignment client doesn't set _authURL.        requestURL = getMetaverseServerURL();    }     // qCDebug(networking) << "Received path" << path;    // qCDebug(networking) << "path.left(path.indexOf(\" ? \"))" << path.left(path.indexOf("?"));    // qCDebug(networking) << "getMetaverseServerURLPath(true)" << getMetaverseServerURLPath(true);     int queryStringLocation = path.indexOf("?");    if (path.startsWith("/")) {        requestURL.setPath(getMetaverseServerURLPath(false) + path.left(queryStringLocation));    } else {        requestURL.setPath(getMetaverseServerURLPath(true) + path.left(queryStringLocation));    }     // qCDebug(networking) << "Creating request path" << requestURL;    // qCDebug(networking) << "requestURL.isValid()" << requestURL.isValid();    // qCDebug(networking) << "requestURL.errorString()" << requestURL.errorString();     if (queryStringLocation >= 0) {        QUrlQuery query(path.mid(queryStringLocation+1));        requestURL.setQuery(query);    }     if (authType != AccountManagerAuth::None ) {        if (hasValidAccessToken()) {            networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,                                        _accountInfo.getAccessToken().authorizationHeaderValue());        } else {            if (authType == AccountManagerAuth::Required) {                qCDebug(networking) << "No valid access token present. Bailing on invoked request to"                    << path << "that requires authentication";                return QNetworkRequest();            }        }    }     networkRequest.setUrl(requestURL);     return networkRequest;} void AccountManager::sendRequest(const QString& path,                                 AccountManagerAuth::Type authType,                                 QNetworkAccessManager::Operation operation,                                 const JSONCallbackParameters& callbackParams,                                 const QByteArray& dataByteArray,                                 QHttpMultiPart* dataMultiPart,                                 const QVariantMap& propertyMap) {     if (thread() != QThread::currentThread()) {        QMetaObject::invokeMethod(this, "sendRequest",                                  Q_ARG(const QString&, path),                                  Q_ARG(AccountManagerAuth::Type, authType),                                  Q_ARG(QNetworkAccessManager::Operation, operation),                                  Q_ARG(const JSONCallbackParameters&, callbackParams),                                  Q_ARG(const QByteArray&, dataByteArray),                                  Q_ARG(QHttpMultiPart*, dataMultiPart),                                  Q_ARG(QVariantMap, propertyMap));        return;    }     QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();     QNetworkRequest networkRequest = createRequest(path, authType);     if (VERBOSE_HTTP_REQUEST_DEBUGGING) {        qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString());         if (!dataByteArray.isEmpty()) {            qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray);        }    }     QNetworkReply* networkReply = NULL;     switch (operation) {        case QNetworkAccessManager::GetOperation:            networkReply = networkAccessManager.get(networkRequest);            break;        case QNetworkAccessManager::PostOperation:        case QNetworkAccessManager::PutOperation:            if (dataMultiPart) {                if (operation == QNetworkAccessManager::PostOperation) {                    networkReply = networkAccessManager.post(networkRequest, dataMultiPart);                } else {                    networkReply = networkAccessManager.put(networkRequest, dataMultiPart);                }                 // make sure dataMultiPart is destroyed when the reply is                connect(networkReply, &QNetworkReply::destroyed, dataMultiPart, &QHttpMultiPart::deleteLater);            } else {                networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");                if (operation == QNetworkAccessManager::PostOperation) {                    networkReply = networkAccessManager.post(networkRequest, dataByteArray);                } else {                    networkReply = networkAccessManager.put(networkRequest, dataByteArray);                }            }             break;        case QNetworkAccessManager::DeleteOperation:            networkReply = networkAccessManager.sendCustomRequest(networkRequest, "DELETE");            break;        default:            // other methods not yet handled            break;    }     if (networkReply) {        if (!propertyMap.isEmpty()) {            // we have properties to set on the reply so the user can check them after            foreach(const QString& propertyKey, propertyMap.keys()) {                networkReply->setProperty(qPrintable(propertyKey), propertyMap.value(propertyKey));            }        }         connect(networkReply, &QNetworkReply::finished, this, [this, networkReply] {            // double check if the finished network reply had a session ID in the header and make            // sure that our session ID matches that value if so            if (networkReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) {                _sessionID = networkReply->rawHeader(METAVERSE_SESSION_ID_HEADER);            }        });          if (callbackParams.isEmpty()) {            connect(networkReply, &QNetworkReply::finished, networkReply, &QNetworkReply::deleteLater);        } else {            // There's a cleaner way to fire the JSON/error callbacks below and ensure that deleteLater is called for the            // request reply - unfortunately it requires Qt 5.10 which the Android build does not support as of 06/26/18             connect(networkReply, &QNetworkReply::finished, callbackParams.callbackReceiver,                    [callbackParams, networkReply] {                if (networkReply->error() == QNetworkReply::NoError) {                    if (!callbackParams.jsonCallbackMethod.isEmpty()) {                        bool invoked = false;                        if (callbackParams.callbackData.isEmpty()) {                            invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver,                                qPrintable(callbackParams.jsonCallbackMethod),                                Q_ARG(QNetworkReply*, networkReply));                        } else {                            invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver,                                qPrintable(callbackParams.jsonCallbackMethod),                                Q_ARG(QNetworkReply*, networkReply),                                Q_ARG(QJsonObject, callbackParams.callbackData));                        }                         if (!invoked) {                            QString error = "Could not invoke " + callbackParams.jsonCallbackMethod + " with QNetworkReply* "                            + "on callbackReceiver.";                            qCWarning(networking) << error;                            Q_ASSERT_X(invoked, "AccountManager::passErrorToCallback", qPrintable(error));                        }                    } else {                        if (VERBOSE_HTTP_REQUEST_DEBUGGING) {                            qCDebug(networking) << "Received JSON response from directory services API that has no matching callback.";                            qCDebug(networking) << QJsonDocument::fromJson(networkReply->readAll());                        }                    }                } else {                    if (!callbackParams.errorCallbackMethod.isEmpty()) {                        bool invoked = false;                        if (callbackParams.callbackData.isEmpty()) {                            invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver,                                qPrintable(callbackParams.errorCallbackMethod),                                Q_ARG(QNetworkReply*, networkReply));                        }                        else {                            invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver,                                qPrintable(callbackParams.errorCallbackMethod),                                Q_ARG(QNetworkReply*, networkReply),                                Q_ARG(QJsonObject, callbackParams.callbackData));                        }                         if (!invoked) {                            QString error = "Could not invoke " + callbackParams.errorCallbackMethod + " with QNetworkReply* "                            + "on callbackReceiver.";                            qCWarning(networking) << error;                            Q_ASSERT_X(invoked, "AccountManager::passErrorToCallback", qPrintable(error));                        }                     } else {                        if (VERBOSE_HTTP_REQUEST_DEBUGGING) {                            qCDebug(networking) << "Received error response from directory services API that has no matching callback.";                            qCDebug(networking) << "Error" << networkReply->error() << "-" << networkReply->errorString();                            qCDebug(networking) << networkReply->readAll();                        }                    }                }                 networkReply->deleteLater();            });        }    }} bool writeAccountMapToFile(const QVariantMap& accountMap) {    // re-open the file and truncate it    QFile accountFile { accountFilePath() };     // make sure the directory that will hold the account file exists    QDir accountFileDirectory { accountFileDir() };    accountFileDirectory.mkpath(".");     if (accountFile.open(QIODevice::WriteOnly)) {        QDataStream writeStream(&accountFile);         // persist the updated account QVariantMap to file        writeStream << accountMap;         // close the file with the newly persisted settings        accountFile.close();         return true;    } else {        return false;    }} void AccountManager::persistAccountToFile() {     qCDebug(networking) << "Persisting AccountManager accounts to" << accountFilePath();     bool wasLoaded = false;    auto accountMap = accountMapFromFile(wasLoaded);     if (wasLoaded) {        // replace the current account information for this auth URL in the account map        accountMap[_authURL.toString()] = QVariant::fromValue(_accountInfo);         // re-open the file and truncate it        if (writeAccountMapToFile(accountMap)) {            return;        }    }     qCWarning(networking) << "Could not load accounts file - unable to persist account information to file.";} void AccountManager::removeAccountFromFile() {    bool wasLoaded = false;    auto accountMap = accountMapFromFile(wasLoaded);     if (wasLoaded) {        accountMap.remove(_authURL.toString());        if (writeAccountMapToFile(accountMap)) {            qCDebug(networking) << "Removed account info for" << _authURL << "from settings file.";            return;        }    }     qCWarning(networking) << "Count not load accounts file - unable to remove account information for" << _authURL        << "from settings file.";} void AccountManager::setAccountInfo(const DataServerAccountInfo &newAccountInfo) {    _accountInfo = newAccountInfo;    _pendingPrivateKey.clear();    if (_isAgent && !_accountInfo.getAccessToken().token.isEmpty() && !_accountInfo.hasProfile()) {        // we are missing profile information, request it now        requestProfile();    }     // prepare to refresh our token if it is about to expire    if (needsToRefreshToken()) {        refreshAccessToken();    }} bool AccountManager::hasValidAccessToken() { 

Lines 1–500 of 1130.