diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 499e858297..8f97da1ff4 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -119,7 +119,7 @@ function reloadSettings() { var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; -$('#settings').on('click', 'button', function(e){ +$('body').on('click', '.save-button', function(e){ // disable any inputs not changed $("input:not([data-changed])").each(function(){ $(this).prop('disabled', true); @@ -133,6 +133,9 @@ $('#settings').on('click', 'button', function(e){ $(this).prop('disabled', false); }); + // remove focus from the button + $(this).blur() + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved $.ajax('/settings.json', { data: JSON.stringify(formJSON), diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 66cdd36573..d658bd0712 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -22,7 +22,7 @@ - + diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e47c75a51a..24e23b693c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -48,13 +48,14 @@ DomainServer::DomainServer(int argc, char* argv[]) : _cookieSessionHash(), _settingsManager() { - LogUtils::init(); setOrganizationName("High Fidelity"); setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); + + _settingsManager.loadSettingsMap(arguments()); installNativeEventFilter(&_shutdownEventListener); connect(&_shutdownEventListener, SIGNAL(receivedCloseEvent()), SLOT(quit())); @@ -62,8 +63,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); - _argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); - if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -83,8 +82,8 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - QString certPath = _argumentVariantMap.value(X509_CERTIFICATE_OPTION).toString(); - QString keyPath = _argumentVariantMap.value(X509_PRIVATE_KEY_OPTION).toString(); + QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString(); + QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString(); if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use DTLS to encrypt communication with nodes @@ -143,10 +142,11 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - _oauthProviderURL = QUrl(_argumentVariantMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); - _oauthClientID = _argumentVariantMap.value(OAUTH_CLIENT_ID_OPTION).toString(); + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); + _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); - _hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString(); + _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() @@ -171,9 +171,11 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { const QString CUSTOM_PORT_OPTION = "port"; unsigned short domainServerPort = DEFAULT_DOMAIN_SERVER_PORT; + + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - if (_argumentVariantMap.contains(CUSTOM_PORT_OPTION)) { - domainServerPort = (unsigned short) _argumentVariantMap.value(CUSTOM_PORT_OPTION).toUInt(); + if (settingsMap.contains(CUSTOM_PORT_OPTION)) { + domainServerPort = (unsigned short) settingsMap.value(CUSTOM_PORT_OPTION).toUInt(); } unsigned short domainServerDTLSPort = 0; @@ -183,8 +185,8 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - if (_argumentVariantMap.contains(CUSTOM_DTLS_PORT_OPTION)) { - domainServerDTLSPort = (unsigned short) _argumentVariantMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); + if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) { + domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); } } @@ -197,7 +199,7 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { // set our LimitedNodeList UUID to match the UUID from our config // nodes will currently use this to add resources to data-web that relate to our domain - nodeList->setSessionUUID(_argumentVariantMap.value(DOMAIN_CONFIG_ID_KEY).toString()); + nodeList->setSessionUUID(settingsMap.value(DOMAIN_CONFIG_ID_KEY).toString()); connect(nodeList, &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); connect(nodeList, &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled); @@ -260,9 +262,10 @@ bool DomainServer::hasOAuthProviderAndAuthInformation() { bool DomainServer::optionallySetupAssignmentPayment() { const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && - _argumentVariantMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() && + if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && + settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() && hasOAuthProviderAndAuthInformation()) { qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); @@ -288,8 +291,10 @@ bool DomainServer::optionallySetupAssignmentPayment() { void DomainServer::setupDynamicIPAddressUpdating() { const QString ENABLE_DYNAMIC_IP_UPDATING_OPTION = "update-ip"; - if (_argumentVariantMap.contains(ENABLE_DYNAMIC_IP_UPDATING_OPTION) && - _argumentVariantMap.value(ENABLE_DYNAMIC_IP_UPDATING_OPTION).toBool() && + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + + if (settingsMap.contains(ENABLE_DYNAMIC_IP_UPDATING_OPTION) && + settingsMap.value(ENABLE_DYNAMIC_IP_UPDATING_OPTION).toBool() && hasOAuthProviderAndAuthInformation()) { LimitedNodeList* nodeList = LimitedNodeList::getInstance(); @@ -338,9 +343,11 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) // check for configs from the command line, these take precedence const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); + + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); // scan for assignment config keys - QStringList variantMapKeys = _argumentVariantMap.keys(); + QStringList variantMapKeys = settingsMap.keys(); int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); while (configIndex != -1) { @@ -348,7 +355,7 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { - QVariant mapValue = _argumentVariantMap[variantMapKeys[configIndex]]; + QVariant mapValue = settingsMap[variantMapKeys[configIndex]]; QJsonArray assignmentArray; if (mapValue.type() == QVariant::String) { @@ -513,7 +520,7 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock QString connectedUsername; - if (!isAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) { + if (!isAssignment && !_oauthProviderURL.isEmpty() && _settingsManager.getSettingsMap().contains(ALLOWED_ROLES_CONFIG_KEY)) { // this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones if (_sessionAuthenticationHash.contains(packetUUID)) { connectedUsername = _sessionAuthenticationHash.take(packetUUID); @@ -1392,8 +1399,10 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + if (!_oauthProviderURL.isEmpty() - && (_argumentVariantMap.contains(ADMIN_USERS_CONFIG_KEY) || _argumentVariantMap.contains(ADMIN_ROLES_CONFIG_KEY))) { + && (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) { QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -1404,7 +1413,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl cookieUUID = cookieUUIDRegex.cap(1); } - if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) { + if (settingsMap.contains(BASIC_AUTH_CONFIG_KEY)) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } @@ -1414,13 +1423,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); - if (_argumentVariantMap.value(ADMIN_USERS_CONFIG_KEY).toJsonValue().toArray().contains(profileUsername)) { + if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toJsonValue().toArray().contains(profileUsername)) { // this is an authenticated user return true; } // loop the roles of this user and see if they are in the admin-roles array - QJsonArray adminRolesArray = _argumentVariantMap.value(ADMIN_ROLES_CONFIG_KEY).toJsonValue().toArray(); + QJsonArray adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toJsonValue().toArray(); if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { @@ -1455,7 +1464,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl // we don't know about this user yet, so they are not yet authenticated return false; } - } else if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) { + } else if (settingsMap.contains(BASIC_AUTH_CONFIG_KEY)) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; @@ -1474,7 +1483,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString password = credentialList[1]; // we've pulled a username and password - now check if there is a match in our basic auth hash - QJsonObject basicAuthObject = _argumentVariantMap.value(BASIC_AUTH_CONFIG_KEY).toJsonValue().toObject(); + QJsonObject basicAuthObject = settingsMap.value(BASIC_AUTH_CONFIG_KEY).toJsonValue().toObject(); if (basicAuthObject.contains(username)) { const QString BASIC_AUTH_USER_PASSWORD_KEY = "password"; @@ -1557,7 +1566,8 @@ void DomainServer::handleProfileRequestFinished() { // pull the user roles from the response QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray(); - QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray(); + QJsonArray allowedRolesArray = _settingsManager.getSettingsMap() + .value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray(); QString connectableUsername; QString profileUsername = profileJSON.object()["data"].toObject()["user"].toObject()["username"].toString(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index f2b1b85e09..95d508c94a 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -115,8 +115,6 @@ private: QHash _pendingAssignedNodes; TransactionHash _pendingAssignmentCredits; - QVariantMap _argumentVariantMap; - bool _isUsingDTLS; QUrl _oauthProviderURL; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index f5f0bec2c5..63d034bf2f 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -10,19 +10,21 @@ // #include +#include #include #include #include +#include #include #include #include +#include #include #include "DomainServerSettingsManager.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; -const QString SETTINGS_JSON_FILE_RELATIVE_PATH = "/resources/settings.json"; DomainServerSettingsManager::DomainServerSettingsManager() : _descriptionArray(), @@ -33,20 +35,20 @@ DomainServerSettingsManager::DomainServerSettingsManager() : descriptionFile.open(QIODevice::ReadOnly); _descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array(); +} + +void DomainServerSettingsManager::loadSettingsMap(const QStringList& argumentList) { + _settingsMap = HifiConfigVariantMap::mergeMasterConfigWithUserConfig(argumentList); - // load the existing config file to get the current values - QFile configFile(QCoreApplication::applicationDirPath() + SETTINGS_JSON_FILE_RELATIVE_PATH); + qDebug() << _settingsMap; - if (configFile.exists()) { - configFile.open(QIODevice::ReadOnly); - - _settingsMap = QJsonDocument::fromJson(configFile.readAll()).toVariant().toMap(); - } + // figure out where we are supposed to persist our settings to + _settingsFilepath = HifiConfigVariantMap::userConfigFilepath(argumentList); } const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; -const QString SETTINGS_GROUP_KEY_NAME = "key"; +const QString SETTINGS_GROUP_KEY_NAME = "name"; bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) { if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") { @@ -209,7 +211,15 @@ QByteArray DomainServerSettingsManager::getJSONSettingsMap() const { } void DomainServerSettingsManager::persistToFile() { - QFile settingsFile(QCoreApplication::applicationDirPath() + SETTINGS_JSON_FILE_RELATIVE_PATH); + + // make sure we have the dir the settings file is supposed to live in + QFileInfo settingsFileInfo(_settingsFilepath); + + if (!settingsFileInfo.dir().exists()) { + settingsFileInfo.dir().mkpath("."); + } + + QFile settingsFile(_settingsFilepath); if (settingsFile.open(QIODevice::WriteOnly)) { settingsFile.write(getJSONSettingsMap()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 9fd9df908c..0b97a821ef 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -24,7 +24,10 @@ public: bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url); bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); + void loadSettingsMap(const QStringList& argumentList); + QByteArray getJSONSettingsMap() const; + const QVariantMap& getSettingsMap() const { return _settingsMap; } private: void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant, QJsonArray descriptionArray); @@ -32,6 +35,7 @@ private: QJsonArray _descriptionArray; QVariantMap _settingsMap; + QString _settingsFilepath; }; #endif // hifi_DomainServerSettingsManager_h \ No newline at end of file diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 6163bd4d8c..baf7d51d01 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -82,26 +82,78 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL QCoreApplication::applicationName()); } - QFile configFile(configFilePath); - if (configFile.exists()) { - qDebug() << "Reading JSON config file at" << configFilePath; - configFile.open(QIODevice::ReadOnly); - - QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); - QJsonObject rootObject = configDocument.object(); - - // enumerate the keys of the configDocument object - foreach(const QString& key, rootObject.keys()) { - - if (!mergedMap.contains(key)) { - // no match in existing list, add it - mergedMap.insert(key, QVariant(rootObject[key])); - } - } - } else { - qDebug() << "Could not find JSON config file at" << configFilePath; - } return mergedMap; } + +QVariantMap HifiConfigVariantMap::mergeMasterConfigWithUserConfig(const QStringList& argumentList) { + // check if there is a master config file + const QString MASTER_CONFIG_FILE_OPTION = "--master-config"; + + QVariantMap configVariantMap; + + int masterConfigIndex = argumentList.indexOf(MASTER_CONFIG_FILE_OPTION); + if (masterConfigIndex != -1) { + QString masterConfigFilepath = argumentList[masterConfigIndex + 1]; + + mergeMapWithJSONFile(configVariantMap, masterConfigFilepath); + } + + // merge the existing configVariantMap with the user config file + mergeMapWithJSONFile(configVariantMap, userConfigFilepath(argumentList)); + + return configVariantMap; +} + +QString HifiConfigVariantMap::userConfigFilepath(const QStringList& argumentList) { + // we've loaded up the master config file, now fill in anything it didn't have with the user config file + const QString USER_CONFIG_FILE_OPTION = "--user-config"; + + int userConfigIndex = argumentList.indexOf(USER_CONFIG_FILE_OPTION); + QString userConfigFilepath; + if (userConfigIndex != -1) { + userConfigFilepath = argumentList[userConfigIndex + 1]; + } else { + userConfigFilepath = QString("%1/%2/%3/config.json").arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation), + QCoreApplication::organizationName(), + QCoreApplication::applicationName()); + } + + return userConfigFilepath; +} + +void HifiConfigVariantMap::mergeMapWithJSONFile(QVariantMap& existingMap, const QString& filename) { + QFile configFile(filename); + + if (configFile.exists()) { + qDebug() << "Reading JSON config file at" << filename; + configFile.open(QIODevice::ReadOnly); + + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); + + if (existingMap.isEmpty()) { + existingMap = configDocument.toVariant().toMap(); + } else { + addMissingValuesToExistingMap(existingMap, configDocument.toVariant().toMap()); + } + + } else { + qDebug() << "Could not find JSON config file at" << filename; + } +} + +void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap) { + foreach(const QString& key, newMap.keys()) { + if (existingMap.contains(key)) { + // if this is just a regular value, we're done - we don't ovveride + + if (newMap[key].canConvert(QMetaType::QVariantMap) && existingMap[key].canConvert(QMetaType::QVariantMap)) { + // there's a variant map below and the existing map has one too, so we need to keep recursing + addMissingValuesToExistingMap(reinterpret_cast(existingMap[key]), newMap[key].toMap()); + } + } else { + existingMap[key] = newMap[key]; + } + } +} diff --git a/libraries/shared/src/HifiConfigVariantMap.h b/libraries/shared/src/HifiConfigVariantMap.h index 378aa749c5..eae5de26d5 100644 --- a/libraries/shared/src/HifiConfigVariantMap.h +++ b/libraries/shared/src/HifiConfigVariantMap.h @@ -17,6 +17,11 @@ class HifiConfigVariantMap { public: static QVariantMap mergeCLParametersWithJSONConfig(const QStringList& argumentList); + static QVariantMap mergeMasterConfigWithUserConfig(const QStringList& argumentList); + static QString userConfigFilepath(const QStringList& argumentList); +private: + static void mergeMapWithJSONFile(QVariantMap& existingMap, const QString& filename); + static void addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap); }; #endif // hifi_HifiConfigVariantMap_h