//// 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. Load more
// // 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() {