// // AccountManager.cpp // hifi // // Created by Stephen Birarda on 2/18/2014. // Copyright (c) 2014 HighFidelity, Inc. All rights reserved. // #include #include #include #include #include #include #include #include #include "NodeList.h" #include "PacketHeaders.h" #include "AccountManager.h" const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; AccountManager& AccountManager::getInstance() { static AccountManager sharedInstance; return sharedInstance; } Q_DECLARE_METATYPE(OAuthAccessToken) Q_DECLARE_METATYPE(DataServerAccountInfo) Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; AccountManager::AccountManager() : _authURL(), _networkAccessManager(NULL), _pendingCallbackMap(), _accountInfo() { qRegisterMetaType("OAuthAccessToken"); qRegisterMetaTypeStreamOperators("OAuthAccessToken"); qRegisterMetaType("DataServerAccountInfo"); qRegisterMetaTypeStreamOperators("DataServerAccountInfo"); qRegisterMetaType("QNetworkAccessManager::Operation"); qRegisterMetaType("JSONCallbackParameters"); } const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash"; void AccountManager::logout() { // a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file _accountInfo = DataServerAccountInfo(); QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE)); settings.remove(keyURLString); qDebug() << "Removed account info for" << _authURL << "from in-memory accounts and .ini file"; emit logoutComplete(); // the username has changed to blank emit usernameChanged(QString()); } void AccountManager::setAuthURL(const QUrl& authURL) { if (_authURL != authURL) { _authURL = authURL; qDebug() << "URL for node authentication has been changed to" << qPrintable(_authURL.toString()); qDebug() << "Re-setting authentication flow."; // check if there are existing access tokens to load from settings QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); foreach(const QString& key, settings.allKeys()) { // take a key copy to perform the double slash replacement QString keyCopy(key); QUrl keyURL(keyCopy.replace("slashslash", "//")); if (keyURL == _authURL) { // pull out the stored access token and store it in memory _accountInfo = settings.value(key).value(); qDebug() << "Found a data-server access token for" << qPrintable(keyURL.toString()); emit accessTokenChanged(); } } // tell listeners that the auth endpoint has changed emit authEndpointChanged(); } } void AccountManager::authenticatedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { QMetaObject::invokeMethod(this, "invokedRequest", Q_ARG(const QString&, path), Q_ARG(QNetworkAccessManager::Operation, operation), Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart)); } void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { if (!_networkAccessManager) { _networkAccessManager = new QNetworkAccessManager(this); } if (hasValidAccessToken()) { QNetworkRequest authenticatedRequest; QUrl requestURL = _authURL; requestURL.setPath(path); requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); authenticatedRequest.setUrl(requestURL); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Making an authenticated request to" << qPrintable(requestURL.toString()); if (!dataByteArray.isEmpty()) { qDebug() << "The POST/PUT body -" << QString(dataByteArray); } } QNetworkReply* networkReply = NULL; switch (operation) { case QNetworkAccessManager::GetOperation: networkReply = _networkAccessManager->get(authenticatedRequest); break; case QNetworkAccessManager::PostOperation: case QNetworkAccessManager::PutOperation: authenticatedRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if (dataMultiPart) { if (operation == QNetworkAccessManager::PostOperation) { networkReply = _networkAccessManager->post(authenticatedRequest, dataMultiPart); } else { networkReply = _networkAccessManager->put(authenticatedRequest, dataMultiPart); } } else { if (operation == QNetworkAccessManager::PostOperation) { networkReply = _networkAccessManager->post(authenticatedRequest, dataByteArray); } else { networkReply = _networkAccessManager->put(authenticatedRequest, dataByteArray); } } break; default: // other methods not yet handled break; } if (networkReply) { if (!callbackParams.isEmpty()) { // if we have information for a callback, insert the callbackParams into our local map _pendingCallbackMap.insert(networkReply, callbackParams); } // if we ended up firing of a request, hook up to it now connect(networkReply, SIGNAL(finished()), this, SLOT(passSuccessToCallback())); connect(networkReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(passErrorToCallback(QNetworkReply::NetworkError))); } } } void AccountManager::passSuccessToCallback() { QNetworkReply* requestReply = reinterpret_cast(sender()); QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); if (callbackParams.jsonCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod), Q_ARG(const QJsonObject&, jsonResponse.object())); // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Received JSON response from data-server that has no matching callback."; qDebug() << jsonResponse; } } } void AccountManager::passErrorToCallback(QNetworkReply::NetworkError errorCode) { QNetworkReply* requestReply = reinterpret_cast(sender()); JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); if (callbackParams.errorCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.errorCallbackReceiver, qPrintable(callbackParams.errorCallbackMethod), Q_ARG(QNetworkReply::NetworkError, errorCode), Q_ARG(const QString&, requestReply->errorString())); // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Received error response from data-server that has no matching callback."; qDebug() << "Error" << errorCode << "-" << requestReply->errorString(); } } } bool AccountManager::hasValidAccessToken() { if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "An access token is required for requests to" << qPrintable(_authURL.toString()); } return false; } else { return true; } } bool AccountManager::checkAndSignalForAccessToken() { bool hasToken = hasValidAccessToken(); if (!hasToken) { // emit a signal so somebody can call back to us and request an access token given a username and password emit authRequired(); } return hasToken; } void AccountManager::requestAccessToken(const QString& login, const QString& password) { if (!_networkAccessManager) { _networkAccessManager = new QNetworkAccessManager(this); } QNetworkRequest request; QUrl grantURL = _authURL; grantURL.setPath("/oauth/token"); QByteArray postData; postData.append("grant_type=password&"); postData.append("username=" + login + "&"); postData.append("password=" + password); request.setUrl(grantURL); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply* requestReply = _networkAccessManager->post(request, postData); connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestFinished); connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); } void AccountManager::requestFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); if (!rootObject.contains("error")) { // construct an OAuthAccessToken from the json object if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") || !rootObject.contains("token_type")) { // TODO: error handling - malformed token response qDebug() << "Received a response for password grant that is missing one or more expected values."; } else { // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); qDebug() << "Storing an account with access-token for" << qPrintable(rootURL.toString()); _accountInfo = DataServerAccountInfo(rootObject); emit loginComplete(rootURL); // the username has changed to whatever came back emit usernameChanged(_accountInfo.getUsername()); // we have found or requested an access token emit accessTokenChanged(); // store this access token into the local settings QSettings localSettings; localSettings.beginGroup(ACCOUNTS_GROUP); localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE), QVariant::fromValue(_accountInfo)); } } else { // TODO: error handling qDebug() << "Error in response for password grant -" << rootObject["error_description"].toString(); } } void AccountManager::requestError(QNetworkReply::NetworkError error) { // TODO: error handling qDebug() << "AccountManager requestError - " << error; }