// // DomainServer.cpp // domain-server/src // // Created by Stephen Birarda on 9/26/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DomainServerNodeData.h" #include "DomainServer.h" int const DomainServer::EXIT_CODE_REBOOT = 234923; const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.io"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _httpManager(DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), _httpsManager(NULL), _allAssignments(), _unfulfilledAssignments(), _pendingAssignedNodes(), _isUsingDTLS(false), _oauthProviderURL(), _oauthClientID(), _hostname(), _webAuthenticationStateSet(), _cookieSessionHash(), _automaticNetworkingSetting(), _settingsManager(), _iceServerSocket(ICE_SERVER_DEFAULT_HOSTNAME, ICE_SERVER_DEFAULT_PORT) { qInstallMessageHandler(LogHandler::verboseMessageHandler); LogUtils::init(); Setting::init(); connect(this, &QCoreApplication::aboutToQuit, this, &DomainServer::aboutToQuit); setOrganizationName("High Fidelity"); setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) AccountManager::getInstance(true); _settingsManager.setupConfigMap(arguments()); // setup a shutdown event listener to handle SIGTERM or WM_CLOSE for us #ifdef _WIN32 installNativeEventFilter(&ShutdownEventListener::getInstance()); #else ShutdownEventListener::getInstance(); #endif qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); 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 qDebug() << "Setting up LimitedNodeList and assignments."; setupNodeListAndAssignments(); loadExistingSessionsFromSettings(); // setup automatic networking settings with data server setupAutomaticNetworking(); // preload some user public keys so they can connect on first request preloadAllowedUserPublicKeys(); } } void DomainServer::aboutToQuit() { // clear the log handler so that Qt doesn't call the destructor on LogHandler qInstallMessageHandler(0); } void DomainServer::restart() { qDebug() << "domain-server is restarting."; exit(DomainServer::EXIT_CODE_REBOOT); } bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_CERTIFICATE_OPTION = "cert"; const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; 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 // let's make sure we can load the key and certificate // _x509Credentials = new gnutls_certificate_credentials_t; // gnutls_certificate_allocate_credentials(_x509Credentials); QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); qDebug() << "Reading certificate file at" << certPath << "for DTLS."; qDebug() << "Reading key file at" << keyPath << "for DTLS."; // int gnutlsReturn = gnutls_certificate_set_x509_key_file2(*_x509Credentials, // certPath.toLocal8Bit().constData(), // keyPath.toLocal8Bit().constData(), // GNUTLS_X509_FMT_PEM, // keyPassphraseString.toLocal8Bit().constData(), // 0); // // if (gnutlsReturn < 0) { // qDebug() << "Unable to load certificate or key file." << "Error" << gnutlsReturn << "- domain-server will now quit."; // QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); // return false; // } // qDebug() << "Successfully read certificate and private key."; // we need to also pass this certificate and private key to the HTTPS manager // this is used for Oauth callbacks when authorizing users against a data server QFile certFile(certPath); certFile.open(QIODevice::ReadOnly); QFile keyFile(keyPath); keyFile.open(QIODevice::ReadOnly); QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); _httpsManager = new HTTPSManager(DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; } else if (!certPath.isEmpty() || !keyPath.isEmpty()) { qDebug() << "Missing certificate or private key. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return false; } return true; } bool DomainServer::optionallySetupOAuth() { const QString OAUTH_PROVIDER_URL_OPTION = "oauth-provider"; const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id"; const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL; } AccountManager& accountManager = AccountManager::getInstance(); accountManager.disableSettingsFilePersistence(); accountManager.setAuthURL(_oauthProviderURL); _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() || _hostname.isEmpty() || _oauthClientID.isEmpty() || _oauthClientSecret.isEmpty()) { qDebug() << "Missing OAuth provider URL, hostname, client ID, or client secret. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return false; } else { qDebug() << "OAuth will be used to identify clients using provider at" << _oauthProviderURL.toString(); qDebug() << "OAuth Client ID is" << _oauthClientID; } } return true; } const QString DOMAIN_CONFIG_ID_KEY = "id"; const QString METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH = "metaverse.automatic_networking"; const QString FULL_AUTOMATIC_NETWORKING_VALUE = "full"; const QString IP_ONLY_AUTOMATIC_NETWORKING_VALUE = "ip"; const QString DISABLED_AUTOMATIC_NETWORKING_VALUE = "disabled"; void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port"; QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); unsigned short domainServerPort = (unsigned short) localPortValue.toUInt(); QVariantMap& settingsMap = _settingsManager.getSettingsMap(); unsigned short domainServerDTLSPort = 0; if (_isUsingDTLS) { domainServerDTLSPort = DEFAULT_DOMAIN_SERVER_DTLS_PORT; const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) { domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); } } QSet parsedTypes; parseAssignmentConfigs(parsedTypes); populateDefaultStaticAssignmentsExcludingTypes(parsedTypes); // check for scripts the user wants to persist from their domain-server config populateStaticScriptedAssignmentsFromSettings(); auto nodeList = DependencyManager::set(domainServerPort, domainServerDTLSPort); // no matter the local port, save it to shared mem so that local assignment clients can ask what it is nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, this, nodeList->getNodeSocket().localPort()); // store our local http ports in shared memory quint16 localHttpPort = DOMAIN_SERVER_HTTP_PORT; nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_HTTP_PORT_SMEM_KEY, this, localHttpPort); quint16 localHttpsPort = DOMAIN_SERVER_HTTPS_PORT; nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_HTTPS_PORT_SMEM_KEY, this, localHttpsPort); // 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 const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled); // register as the packet receiver for the types we want PacketReceiver& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::RequestAssignment, this, "processRequestAssignmentPacket"); packetReceiver.registerListener(PacketType::DomainConnectRequest, this, "processConnectRequestPacket"); packetReceiver.registerListener(PacketType::DomainListRequest, this, "processListRequestPacket"); packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket"); packetReceiver.registerListener(PacketType::DomainServerConnectionToken, this, "processConnectRequestPacket"); packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket"); packetReceiver.registerListener(PacketType::ICEPing, this, "processICEPingPacket"); packetReceiver.registerListener(PacketType::ICEPingReply, this, "processICEPingReplyPacket"); packetReceiver.registerListener(PacketType::ICEServerPeerInformation, this, "processICEPeerInformationPacket"); // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); } bool DomainServer::didSetupAccountManagerWithAccessToken() { if (AccountManager::getInstance().hasValidAccessToken()) { // we already gave the account manager a valid access token return true; } return resetAccountManagerAccessToken(); } const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; bool DomainServer::resetAccountManagerAccessToken() { if (!_oauthProviderURL.isEmpty()) { // check for an access-token in our settings, can optionally be overidden by env value const QString ENV_ACCESS_TOKEN_KEY = "DOMAIN_SERVER_ACCESS_TOKEN"; QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY); if (accessToken.isEmpty()) { const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { accessToken = accessTokenVariant->toString(); } else { qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present." << "Set an access token via the web interface, in your user or master config" << "at keypath metaverse.access_token or in your ENV at key DOMAIN_SERVER_ACCESS_TOKEN"; return false; } } else { qDebug() << "Using access token from DOMAIN_SERVER_ACCESS_TOKEN in env. This overrides any access token present" << " in the user or master config."; } // give this access token to the AccountManager AccountManager::getInstance().setAccessTokenForCurrentAuthURL(accessToken); return true; } else { qDebug() << "Missing OAuth provider URL, but a domain-server feature was required that requires authentication." << "domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return false; } } bool DomainServer::optionallySetupAssignmentPayment() { const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() && didSetupAccountManagerWithAccessToken()) { qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); // assume that the fact we are authing against HF data server means we will pay for assignments // setup a timer to send transactions to pay assigned nodes every 30 seconds QTimer* creditSetupTimer = new QTimer(this); connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits); const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000; creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS); QTimer* nodePaymentTimer = new QTimer(this); connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer); const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000; nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS); } return true; } void DomainServer::setupAutomaticNetworking() { auto nodeList = DependencyManager::get(); _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { // call our sendHeartbeatToIceServer immediately anytime a local or public socket changes connect(nodeList.data(), &LimitedNodeList::localSockAddrChanged, this, &DomainServer::sendHeartbeatToIceServer); connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, this, &DomainServer::sendHeartbeatToIceServer); // we need this DS to know what our public IP is - start trying to figure that out now nodeList->startSTUNPublicSocketUpdate(); // setup a timer to heartbeat with the ice-server every so often QTimer* iceHeartbeatTimer = new QTimer(this); connect(iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToIceServer); iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); } if (!didSetupAccountManagerWithAccessToken()) { qDebug() << "Cannot send heartbeat to data server without an access token."; qDebug() << "Add an access token to your config file or via the web interface."; return; } if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { const QUuid& domainID = nodeList->getSessionUUID(); if (!domainID.isNull()) { qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) { // send any public socket changes to the data server so nodes can find us at our new IP connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, this, &DomainServer::performIPAddressUpdate); // have the LNL enable public socket updating via STUN nodeList->startSTUNPublicSocketUpdate(); } else { // send our heartbeat to data server so it knows what our network settings are sendHeartbeatToDataServer(); } } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." << "Please add an ID to your config file or via the web interface."; return; } } else { sendHeartbeatToDataServer(); } qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; // no matter the auto networking settings we should heartbeat to the data-server every 15s const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; QTimer* dataHeartbeatTimer = new QTimer(this); connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToDataServer())); dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); } void DomainServer::loginFailed() { qDebug() << "Login to data server has failed. domain-server will now quit"; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); } 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 = settingsMap.keys(); int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); while (configIndex != -1) { // figure out which assignment type this matches Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { QVariant mapValue = settingsMap[variantMapKeys[configIndex]]; QVariantList assignmentList = mapValue.toList(); if (assignmentType != Assignment::AgentType) { createStaticAssignmentsForType(assignmentType, assignmentList); } excludedTypes.insert(assignmentType); } configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment) { qDebug() << "Inserting assignment" << *newAssignment << "to static assignment hash."; newAssignment->setIsStatic(true); _allAssignments.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment)); } void DomainServer::populateStaticScriptedAssignmentsFromSettings() { const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts"; const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH); if (persistentScriptsVariant) { QVariantList persistentScriptsList = persistentScriptsVariant->toList(); foreach(const QVariant& persistentScriptVariant, persistentScriptsList) { QVariantMap persistentScript = persistentScriptVariant.toMap(); const QString PERSISTENT_SCRIPT_URL_KEY = "url"; const QString PERSISTENT_SCRIPT_NUM_INSTANCES_KEY = "num_instances"; const QString PERSISTENT_SCRIPT_POOL_KEY = "pool"; if (persistentScript.contains(PERSISTENT_SCRIPT_URL_KEY)) { // check how many instances of this script to add int numInstances = persistentScript[PERSISTENT_SCRIPT_NUM_INSTANCES_KEY].toInt(); QString scriptURL = persistentScript[PERSISTENT_SCRIPT_URL_KEY].toString(); QString scriptPool = persistentScript.value(PERSISTENT_SCRIPT_POOL_KEY).toString(); qDebug() << "Adding" << numInstances << "of persistent script at URL" << scriptURL << "- pool" << scriptPool; for (int i = 0; i < numInstances; ++i) { // add a scripted assignment to the queue for this instance Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, scriptPool); scriptAssignment->setPayload(scriptURL.toUtf8()); // add it to static hash so we know we have to keep giving it back out addStaticAssignmentToAssignmentHash(scriptAssignment); } } } } } void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const QVariantList &configList) { // we have a string for config for this type qDebug() << "Parsing config for assignment type" << type; int configCounter = 0; foreach(const QVariant& configVariant, configList) { if (configVariant.canConvert(QMetaType::QVariantMap)) { QVariantMap configMap = configVariant.toMap(); // check the config string for a pool const QString ASSIGNMENT_POOL_KEY = "pool"; QString assignmentPool = configMap.value(ASSIGNMENT_POOL_KEY).toString(); if (!assignmentPool.isEmpty()) { configMap.remove(ASSIGNMENT_POOL_KEY); } ++configCounter; qDebug() << "Type" << type << "config" << configCounter << "=" << configMap; Assignment* configAssignment = new Assignment(Assignment::CreateCommand, type, assignmentPool); // setup the payload as a semi-colon separated list of key = value QStringList payloadStringList; foreach(const QString& payloadKey, configMap.keys()) { QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(configMap[payloadKey].toString()); } configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); addStaticAssignmentToAssignmentHash(configAssignment); } } } void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes) { // enumerate over all assignment types and see if we've already excluded it for (Assignment::Type defaultedType = Assignment::AudioMixerType; defaultedType != Assignment::AllTypes; defaultedType = static_cast(static_cast(defaultedType) + 1)) { if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::UNUSED_0 && defaultedType != Assignment::UNUSED_1 && defaultedType != Assignment::UNUSED_2 && defaultedType != Assignment::AgentType) { // type has not been set from a command line or config file config, use the default // by clearing whatever exists and writing a single default assignment with no payload Assignment* newAssignment = new Assignment(Assignment::CreateCommand, (Assignment::Type) defaultedType); addStaticAssignmentToAssignmentHash(newAssignment); } } } const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer; void DomainServer::processConnectRequestPacket(QSharedPointer packet) { NodeType_t nodeType; HifiSockAddr publicSockAddr, localSockAddr; if (packet->getPayloadSize() == 0) { return; } QDataStream packetStream(packet.data()); QUuid connectUUID; packetStream >> connectUUID; const HifiSockAddr& senderSockAddr = packet->getSenderSockAddr(); parseNodeData(packetStream, nodeType, publicSockAddr, localSockAddr, senderSockAddr); if (localSockAddr.isNull() || senderSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; return; } // check if this connect request matches an assignment in the queue bool isAssignment = _pendingAssignedNodes.contains(connectUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); PendingAssignedNodeData* pendingAssigneeData = NULL; if (isAssignment) { pendingAssigneeData = _pendingAssignedNodes.value(connectUUID); if (pendingAssigneeData) { matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(pendingAssigneeData->getAssignmentUUID(), nodeType); if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(connectUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); // remove this unique assignment deployment from the hash of pending assigned nodes // cleanup of the PendingAssignedNodeData happens below after the node has been added to the LimitedNodeList _pendingAssignedNodes.remove(connectUUID); } else { // this is a node connecting to fulfill an assignment that doesn't exist // don't reply back to them so they cycle back and re-request an assignment qDebug() << "No match for assignment deployed with" << uuidStringWithoutCurlyBraces(connectUUID); return; } } } QList nodeInterestList; QString username; QByteArray usernameSignature; auto limitedNodeList = DependencyManager::get(); packetStream >> nodeInterestList; if (packet->bytesLeftToRead() > 0) { // try to verify username and usernameSignature packetStream >> username >> usernameSignature; } else { QUuid connectionToken = _connectionTokenHash[username]; if(connectionToken.isNull()) { // set up the connection token packet static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID); connectionTokenPacket.reset(); connectionTokenPacket->write(connectionToken.toRfc4122()); nodeList->sendUnreliablePacket(connectionToken, packet->getSenderSockAddr()); return; } else { // reset existing packet //connectionTokenPacket.reset(); } } QString reason; if (!isAssignment && !shouldAllowConnectionFromNode(username, usernameSignature, senderSockAddr, reason)) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfString = reason.toUtf8(); quint16 payloadSize = utfString.size(); auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize)); connectionDeniedPacket->writePrimitive(payloadSize); connectionDeniedPacket->write(utfString); // tell client it has been refused. limitedNodeList->sendPacket(std::move(connectionDeniedPacket), senderSockAddr); return; } if ((!isAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType)) || (isAssignment && matchingQueuedAssignment)) { // this was either not a static assignment or it was and we had a matching one in the queue QUuid nodeUUID; HifiSockAddr discoveredSocket = senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(connectUUID); if (connectedPeer) { // this user negotiated a connection with us via ICE, so re-use their ICE client ID nodeUUID = connectUUID; if (connectedPeer->getActiveSocket()) { // set their discovered socket to whatever the activated socket on the network peer object was discoveredSocket = *connectedPeer->getActiveSocket(); } } else { // we got a connectUUID we didn't recognize, just add the node with a new UUID nodeUUID = QUuid::createUuid(); } // if this user is in the editors list (or if the editors list is empty) set the user's node's canAdjustLocks to true const QVariant* allowedEditorsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); bool canAdjustLocks = allowedEditors.isEmpty() || allowedEditors.contains(username); const QVariant* editorsAreRezzersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); bool onlyEditorsAreRezzers = false; if (editorsAreRezzersVariant) { onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); } bool canRez = true; if (onlyEditorsAreRezzers) { canRez = canAdjustLocks; } SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeUUID, nodeType, publicSockAddr, localSockAddr, canAdjustLocks, canRez); // So that we can send messages to this node at will - we need to activate the correct socket on this node now newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket); // when the newNode is created the linked data is also created // if this was a static assignment set the UUID, set the sendingSockAddr DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); if (isAssignment) { nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(pendingAssigneeData->getWalletUUID()); // always allow assignment clients to create and destroy entities newNode->setCanAdjustLocks(true); newNode->setCanRez(true); // now that we've pulled the wallet UUID and added the node to our list, delete the pending assignee data delete pendingAssigneeData; } if (!username.isEmpty()) { // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); // also add an interpolation to JSONBreakableMarshal so that servers can get username in stats JSONBreakableMarshal::addInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(nodeUUID), username); } nodeData->setSendingSockAddr(senderSockAddr); // reply back to the user with a PacketType::DomainList sendDomainListToNode(newNode, senderSockAddr, nodeInterestList.toSet()); // send out this node to our other connected nodes broadcastNewNode(newNode); } } void DomainServer::processListRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { NodeType_t throwawayNodeType; HifiSockAddr nodePublicAddress, nodeLocalAddress; QDataStream packetStream(packet.data()); parseNodeData(packetStream, throwawayNodeType, nodePublicAddress, nodeLocalAddress, packet->getSenderSockAddr()); sendingNode->setPublicSocket(nodePublicAddress); sendingNode->setLocalSocket(nodeLocalAddress); QList nodeInterestList; packetStream >> nodeInterestList; sendDomainListToNode(sendingNode, packet->getSenderSockAddr(), nodeInterestList.toSet()); } unsigned int DomainServer::countConnectedUsers() { unsigned int result = 0; auto nodeList = DependencyManager::get(); nodeList->eachNode([&](const SharedNodePointer& otherNode){ if (otherNode->getType() == NodeType::Agent) { result++; } }); return result; } bool DomainServer::verifyUserSignature(const QString& username, const QByteArray& usernameSignature, QString& reasonReturn) { // it's possible this user can be allowed to connect, but we need to check their username signature QByteArray publicKeyArray = _userPublicKeys.value(username); if (!publicKeyArray.isEmpty()) { // if we do have a public key for the user, check for a signature match const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); // first load up the public key into an RSA struct RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); if (rsaPublicKey) { QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); int decryptResult = RSA_public_decrypt(usernameSignature.size(), reinterpret_cast(usernameSignature.constData()), reinterpret_cast(decryptedArray.data()), rsaPublicKey, RSA_PKCS1_PADDING); QByteArray lowercaseUsername = username.toLower().toUtf8(); QUuid connectionToken = _connectionTokenHash[username]; QByteArray usernameWithToken = lowercaseUsername.append(connectionToken.toRfc4122()); if (decryptResult != -1) { if (usernameWithToken == decryptedArray) { qDebug() << "Username signature matches for" << username << "- allowing connection."; // free up the public key before we return RSA_free(rsaPublicKey); // remove the username's connection token from the hash _connectionTokenHash.remove(username); return true; } else { qDebug() << "Username signature did not match for" << username << "- denying connection."; reasonReturn = "Username signature did not match."; } } else { qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection."; reasonReturn = "Couldn't decrypt user signature."; } // free up the public key, we don't need it anymore RSA_free(rsaPublicKey); } else { // we can't let this user in since we couldn't convert their public key to an RSA key we could use qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; reasonReturn = "Couldn't convert data to RSA key."; } } requestUserPublicKey(username); // no joy. maybe next time? return false; } bool DomainServer::shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr, QString& reasonReturn) { bool isRestrictingAccess = _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); // we always let in a user who is sending a packet from our local socket or from the localhost address if (senderSockAddr.getAddress() == DependencyManager::get()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost) { return true; } if (isRestrictingAccess) { QStringList allowedUsers = _settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); if (allowedUsers.contains(username, Qt::CaseInsensitive)) { if (!verifyUserSignature(username, usernameSignature, reasonReturn)) { return false; } } else { qDebug() << "Connect request denied for user" << username << "not in allowed users list."; reasonReturn = "User not on whitelist."; return false; } } // either we aren't restricting users, or this user is in the allowed list // if this user is in the editors list, exempt them from the max-capacity check const QVariant* allowedEditorsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); if (allowedEditors.contains(username)) { if (verifyUserSignature(username, usernameSignature, reasonReturn)) { return true; } } // if we haven't reached max-capacity, let them in. const QVariant* maximumUserCapacityVariant = valueForKeyPath(_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = countConnectedUsers(); if (connectedUsers >= maximumUserCapacity) { // too many users, deny the new connection. qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; reasonReturn = "Too many connected users."; return false; } qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, perhaps allowing new connection."; } return true; } void DomainServer::preloadAllowedUserPublicKeys() { const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); if (allowedUsers.size() > 0) { // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not // going to create > 100 requests foreach(const QString& username, allowedUsers) { requestUserPublicKey(username); } } } void DomainServer::requestUserPublicKey(const QString& username) { // even if we have a public key for them right now, request a new one in case it has just changed JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "publicKeyJSONCallback"; const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; qDebug() << "Requesting public key for user" << username; AccountManager::getInstance().sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); } QUrl DomainServer::oauthRedirectURL() { return QString("https://%1:%2/oauth").arg(_hostname).arg(_httpsManager->serverPort()); } const QString OAUTH_CLIENT_ID_QUERY_KEY = "client_id"; const QString OAUTH_REDIRECT_URI_QUERY_KEY = "redirect_uri"; QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { // for now these are all interface clients that have a GUI // so just send them back the full authorization URL QUrl authorizationURL = _oauthProviderURL; const QString OAUTH_AUTHORIZATION_PATH = "/oauth/authorize"; authorizationURL.setPath(OAUTH_AUTHORIZATION_PATH); QUrlQuery authorizationQuery; authorizationQuery.addQueryItem(OAUTH_CLIENT_ID_QUERY_KEY, _oauthClientID); const QString OAUTH_RESPONSE_TYPE_QUERY_KEY = "response_type"; const QString OAUTH_REPSONSE_TYPE_QUERY_VALUE = "code"; authorizationQuery.addQueryItem(OAUTH_RESPONSE_TYPE_QUERY_KEY, OAUTH_REPSONSE_TYPE_QUERY_VALUE); const QString OAUTH_STATE_QUERY_KEY = "state"; // create a new UUID that will be the state parameter for oauth authorization AND the new session UUID for that node authorizationQuery.addQueryItem(OAUTH_STATE_QUERY_KEY, uuidStringWithoutCurlyBraces(stateUUID)); authorizationQuery.addQueryItem(OAUTH_REDIRECT_URI_QUERY_KEY, oauthRedirectURL().toString()); authorizationURL.setQuery(authorizationQuery); return authorizationURL; } int DomainServer::parseNodeData(QDataStream& packetStream, NodeType_t& nodeType, HifiSockAddr& publicSockAddr, HifiSockAddr& localSockAddr, const HifiSockAddr& senderSockAddr) { packetStream >> nodeType; packetStream >> publicSockAddr >> localSockAddr; if (publicSockAddr.getAddress().isNull()) { // this node wants to use us its STUN server // so set the node public address to whatever we perceive the public address to be // if the sender is on our box then leave its public address to 0 so that // other users attempt to reach it on the same address they have for the domain-server if (senderSockAddr.getAddress().isLoopback()) { publicSockAddr.setAddress(QHostAddress()); } else { publicSockAddr.setAddress(senderSockAddr.getAddress()); } } return packetStream.device()->pos(); } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr, const NodeSet& nodeInterestSet) { const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NUM_BYTES_RFC4122_UUID + 2; // setup the extended header for the domain list packets // this data is at the beginning of each of the domain list packets QByteArray extendedHeader(NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES, 0); QDataStream extendedHeaderStream(&extendedHeader, QIODevice::WriteOnly); auto limitedNodeList = DependencyManager::get(); extendedHeaderStream << limitedNodeList->getSessionUUID(); extendedHeaderStream << node->getUUID(); extendedHeaderStream << (quint8) node->getCanAdjustLocks(); extendedHeaderStream << (quint8) node->getCanRez(); NLPacketList domainListPackets(PacketType::DomainList, extendedHeader); // always send the node their own UUID back QDataStream domainListStream(&domainListPackets); DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); // store the nodeInterestSet on this DomainServerNodeData, in case it has changed nodeData->setNodeInterestSet(nodeInterestSet); if (nodeInterestSet.size() > 0) { // DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL; if (nodeData->isAuthenticated()) { // if this authenticated node has any interest types, send back those nodes as well limitedNodeList->eachNode([&](const SharedNodePointer& otherNode){ if (otherNode->getUUID() != node->getUUID() && nodeInterestSet.contains(otherNode->getType())) { // since we're about to add a node to the packet we start a segment domainListPackets.startSegment(); // don't send avatar nodes to other avatars, that will come from avatar mixer domainListStream << *otherNode.data(); // pack the secret that these two nodes will use to communicate with each other domainListStream << connectionSecretForNodes(node, otherNode); // we've added the node we wanted so end the segment now domainListPackets.endSegment(); } }); } } // send an empty list to the node, in case there were no other nodes domainListPackets.closeCurrentPacket(true); // write the PacketList to this node limitedNodeList->sendPacketList(domainListPackets, *node); } QUuid DomainServer::connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) { DomainServerNodeData* nodeAData = dynamic_cast(nodeA->getLinkedData()); DomainServerNodeData* nodeBData = dynamic_cast(nodeB->getLinkedData()); if (nodeAData && nodeBData) { QUuid& secretUUID = nodeAData->getSessionSecretHash()[nodeB->getUUID()]; if (secretUUID.isNull()) { // generate a new secret UUID these two nodes can use secretUUID = QUuid::createUuid(); // set it on the other Node's sessionSecretHash reinterpret_cast(nodeBData)->getSessionSecretHash().insert(nodeA->getUUID(), secretUUID); } return secretUUID; } return QUuid(); } void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) { auto limitedNodeList = DependencyManager::get(); auto addNodePacket = NLPacket::create(PacketType::DomainServerAddedNode); // setup the add packet for this new node QDataStream addNodeStream(addNodePacket.get()); addNodeStream << *addedNode.data(); int connectionSecretIndex = addNodePacket->pos(); limitedNodeList->eachMatchingNode( [&](const SharedNodePointer& node)->bool { if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) { // is the added Node in this node's interest list? DomainServerNodeData* nodeData = dynamic_cast(node->getLinkedData()); return nodeData->getNodeInterestSet().contains(addedNode->getType()); } else { return false; } }, [&](const SharedNodePointer& node) { addNodePacket->seek(connectionSecretIndex); QByteArray rfcConnectionSecret = connectionSecretForNodes(node, addedNode).toRfc4122(); // replace the bytes at the end of the packet for the connection secret between these nodes addNodePacket->write(rfcConnectionSecret); // send off this packet to the node limitedNodeList->sendUnreliablePacket(*addNodePacket, *node); } ); } void DomainServer::processRequestAssignmentPacket(QSharedPointer packet) { // construct the requested assignment from the packet data Assignment requestAssignment(*packet); // Suppress these for Assignment::AgentType to once per 5 seconds static QElapsedTimer noisyMessageTimer; static bool wasNoisyTimerStarted = false; if (!wasNoisyTimerStarted) { noisyMessageTimer.start(); wasNoisyTimerStarted = true; } const qint64 NOISY_MESSAGE_INTERVAL_MSECS = 5 * 1000; if (requestAssignment.getType() != Assignment::AgentType || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) { static QString repeatedMessage = LogHandler::getInstance().addOnlyOnceMessageRegex ("Received a request for assignment type [^ ]+ from [^ ]+"); qDebug() << "Received a request for assignment type" << requestAssignment.getType() << "from" << packet->getSenderSockAddr(); noisyMessageTimer.restart(); } SharedAssignmentPointer assignmentToDeploy = deployableAssignmentForRequest(requestAssignment); if (assignmentToDeploy) { qDebug() << "Deploying assignment -" << *assignmentToDeploy.data() << "- to" << packet->getSenderSockAddr(); // give this assignment out, either the type matches or the requestor said they will take any static std::unique_ptr assignmentPacket; if (!assignmentPacket) { assignmentPacket = NLPacket::create(PacketType::CreateAssignment); } // setup a copy of this assignment that will have a unique UUID, for packaging purposes Assignment uniqueAssignment(*assignmentToDeploy.data()); uniqueAssignment.setUUID(QUuid::createUuid()); // reset the assignmentPacket assignmentPacket->reset(); QDataStream assignmentStream(assignmentPacket.get()); assignmentStream << uniqueAssignment; auto limitedNodeList = DependencyManager::get(); limitedNodeList->sendUnreliablePacket(*assignmentPacket, packet->getSenderSockAddr()); // add the information for that deployed assignment to the hash of pending assigned nodes PendingAssignedNodeData* pendingNodeData = new PendingAssignedNodeData(assignmentToDeploy->getUUID(), requestAssignment.getWalletUUID()); _pendingAssignedNodes.insert(uniqueAssignment.getUUID(), pendingNodeData); } else { if (requestAssignment.getType() != Assignment::AgentType || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) { static QString repeatedMessage = LogHandler::getInstance().addOnlyOnceMessageRegex ("Unable to fulfill assignment request of type [^ ]+ from [^ ]+"); qDebug() << "Unable to fulfill assignment request of type" << requestAssignment.getType() << "from" << packet->getSenderSockAddr(); noisyMessageTimer.restart(); } } } void DomainServer::setupPendingAssignmentCredits() { // enumerate the NodeList to find the assigned nodes DependencyManager::get()->eachNode([&](const SharedNodePointer& node){ DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) { // check if we have a non-finalized transaction for this node to add this amount to TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); WalletTransaction* existingTransaction = NULL; while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { if (!i.value()->isFinalized()) { existingTransaction = i.value(); break; } else { ++i; } } qint64 elapsedMsecsSinceLastPayment = nodeData->getPaymentIntervalTimer().elapsed(); nodeData->getPaymentIntervalTimer().restart(); const float CREDITS_PER_HOUR = 0.10f; const float CREDITS_PER_MSEC = CREDITS_PER_HOUR / (60 * 60 * 1000); const int SATOSHIS_PER_MSEC = CREDITS_PER_MSEC * SATOSHIS_PER_CREDIT; float pendingCredits = elapsedMsecsSinceLastPayment * SATOSHIS_PER_MSEC; if (existingTransaction) { existingTransaction->incrementAmount(pendingCredits); } else { // create a fresh transaction to pay this node, there is no transaction to append to WalletTransaction* freshTransaction = new WalletTransaction(nodeData->getWalletUUID(), pendingCredits); _pendingAssignmentCredits.insert(nodeData->getWalletUUID(), freshTransaction); } } }); } void DomainServer::sendPendingTransactionsToServer() { AccountManager& accountManager = AccountManager::getInstance(); if (accountManager.hasValidAccessToken()) { // enumerate the pending transactions and send them to the server to complete payment TransactionHash::iterator i = _pendingAssignmentCredits.begin(); JSONCallbackParameters transactionCallbackParams; transactionCallbackParams.jsonCallbackReceiver = this; transactionCallbackParams.jsonCallbackMethod = "transactionJSONCallback"; while (i != _pendingAssignmentCredits.end()) { accountManager.sendRequest("api/v1/transactions", AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, transactionCallbackParams, i.value()->postJson().toJson()); // set this transaction to finalized so we don't add additional credits to it i.value()->setIsFinalized(true); ++i; } } } void DomainServer::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); if (jsonObject["status"].toString() == "success") { // figure out which user this is for const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key"; QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING); if (usernameRegex.indexIn(requestReply.url().toString()) != -1) { QString username = usernameRegex.cap(1); qDebug() << "Storing a public key for user" << username; // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; _userPublicKeys[username] = QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); } } } void DomainServer::transactionJSONCallback(const QJsonObject& data) { // check if this was successful - if so we can remove it from our list of pending if (data.value("status").toString() == "success") { // create a dummy wallet transaction to unpack the JSON to WalletTransaction dummyTransaction; dummyTransaction.loadFromJson(data); TransactionHash::iterator i = _pendingAssignmentCredits.find(dummyTransaction.getDestinationUUID()); while (i != _pendingAssignmentCredits.end() && i.key() == dummyTransaction.getDestinationUUID()) { if (i.value()->getUUID() == dummyTransaction.getUUID()) { // we have a match - we can remove this from the hash of pending credits // and delete it for clean up WalletTransaction* matchingTransaction = i.value(); _pendingAssignmentCredits.erase(i); delete matchingTransaction; break; } else { ++i; } } } } QJsonObject jsonForDomainSocketUpdate(const HifiSockAddr& socket) { const QString SOCKET_NETWORK_ADDRESS_KEY = "network_address"; const QString SOCKET_PORT_KEY = "port"; QJsonObject socketObject; socketObject[SOCKET_NETWORK_ADDRESS_KEY] = socket.getAddress().toString(); socketObject[SOCKET_PORT_KEY] = socket.getPort(); return socketObject; } const QString DOMAIN_UPDATE_AUTOMATIC_NETWORKING_KEY = "automatic_networking"; void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) { sendHeartbeatToDataServer(newPublicSockAddr.getAddress().toString()); } void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; auto nodeList = DependencyManager::get(); const QUuid& domainID = nodeList->getSessionUUID(); // setup the domain object to send to the data server const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; QJsonObject domainObject; if (!networkAddress.isEmpty()) { domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; } domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings const QString RESTRICTED_ACCESS_FLAG = "restricted"; domainObject[RESTRICTED_ACCESS_FLAG] = _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); // add the number of currently connected agent users int numConnectedAuthedUsers = 0; nodeList->eachNode([&numConnectedAuthedUsers](const SharedNodePointer& node){ if (node->getLinkedData() && !static_cast(node->getLinkedData())->getUsername().isEmpty()) { ++numConnectedAuthedUsers; } }); const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; QJsonObject heartbeatObject; heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedAuthedUsers; domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); AccountManager::getInstance().sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, JSONCallbackParameters(), domainUpdateJSON.toUtf8()); } // TODO: have data-web respond with ice-server hostname to use void DomainServer::sendHeartbeatToIceServer() { DependencyManager::get()->sendHeartbeatToIceServer(_iceServerSocket); } const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS; void DomainServer::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) { if (peer->getConnectionAttempts() > 0 && peer->getConnectionAttempts() % NUM_PEER_PINGS_BEFORE_DELETE == 0) { // we've reached the maximum number of ping attempts qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID(); qDebug() << "Removing from list of connecting peers."; _icePeers.remove(peer->getUUID()); } else { auto limitedNodeList = DependencyManager::get(); // send the ping packet to the local and public sockets for this node auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket()); auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket()); peer->incrementConnectionAttempts(); } } void DomainServer::handlePeerPingTimeout() { NetworkPeer* senderPeer = qobject_cast(sender()); if (senderPeer) { SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID()); if (sharedPeer && !sharedPeer->getActiveSocket()) { pingPunchForConnectingPeer(sharedPeer); } } } void DomainServer::processICEPeerInformationPacket(QSharedPointer packet) { // loop through the packet and pull out network peers // any peer we don't have we add to the hash, otherwise we update QDataStream iceResponseStream(packet.data()); NetworkPeer* receivedPeer = new NetworkPeer; iceResponseStream >> *receivedPeer; if (!_icePeers.contains(receivedPeer->getUUID())) { qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer; SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer); _icePeers[receivedPeer->getUUID()] = newPeer; // make sure we know when we should ping this peer connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainServer::handlePeerPingTimeout); // immediately ping the new peer, and start a timer to continue pinging it until we connect to it newPeer->startPingTimer(); qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID" << newPeer->getUUID(); pingPunchForConnectingPeer(newPeer); } else { delete receivedPeer; } } void DomainServer::processICEPingPacket(QSharedPointer packet) { auto limitedNodeList = DependencyManager::get(); auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*packet, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(pingReplyPacket), packet->getSenderSockAddr()); } void DomainServer::processICEPingReplyPacket(QSharedPointer packet) { QDataStream packetStream(packet.data()); QUuid nodeUUID; packetStream >> nodeUUID; SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID); if (sendingPeer) { // we had this NetworkPeer in our connecting list - add the right sock addr to our connected list sendingPeer->activateMatchingOrNewSymmetricSocket(packet->getSenderSockAddr()); } } void DomainServer::processNodeJSONStatsPacket(QSharedPointer packet, SharedNodePointer sendingNode) { auto nodeData = dynamic_cast(sendingNode->getLinkedData()); if (nodeData) { nodeData->processJSONStatsPacket(*packet); } } QJsonObject DomainServer::jsonForSocket(const HifiSockAddr& socket) { QJsonObject socketJSON; socketJSON["ip"] = socket.getAddress().toString(); socketJSON["port"] = socket.getPort(); return socketJSON; } const char JSON_KEY_UUID[] = "uuid"; const char JSON_KEY_TYPE[] = "type"; const char JSON_KEY_PUBLIC_SOCKET[] = "public"; const char JSON_KEY_LOCAL_SOCKET[] = "local"; const char JSON_KEY_POOL[] = "pool"; const char JSON_KEY_PENDING_CREDITS[] = "pending_credits"; const char JSON_KEY_WAKE_TIMESTAMP[] = "wake_timestamp"; const char JSON_KEY_USERNAME[] = "username"; QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QJsonObject nodeJson; // re-format the type name so it matches the target name QString nodeTypeName = NodeType::getNodeTypeName(node->getType()); nodeTypeName = nodeTypeName.toLower(); nodeTypeName.replace(' ', '-'); // add the node UUID nodeJson[JSON_KEY_UUID] = uuidStringWithoutCurlyBraces(node->getUUID()); // add the node type nodeJson[JSON_KEY_TYPE] = nodeTypeName; // add the node socket information nodeJson[JSON_KEY_PUBLIC_SOCKET] = jsonForSocket(node->getPublicSocket()); nodeJson[JSON_KEY_LOCAL_SOCKET] = jsonForSocket(node->getLocalSocket()); // add the node uptime in our list nodeJson[JSON_KEY_WAKE_TIMESTAMP] = QString::number(node->getWakeTimestamp()); // if the node has pool information, add it DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); // add the node username, if it exists nodeJson[JSON_KEY_USERNAME] = nodeData->getUsername(); SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); if (matchingAssignment) { nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool(); if (!nodeData->getWalletUUID().isNull()) { TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); float pendingCreditAmount = 0; while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { pendingCreditAmount += i.value()->getAmount() / SATOSHIS_PER_CREDIT; ++i; } nodeJson[JSON_KEY_PENDING_CREDITS] = pendingCreditAmount; } } return nodeJson; } const char ASSIGNMENT_SCRIPT_HOST_LOCATION[] = "resources/web/assignment"; QString pathForAssignmentScript(const QUuid& assignmentUUID) { QString newPath(ASSIGNMENT_SCRIPT_HOST_LOCATION); newPath += "/scripts/"; // append the UUID for this script as the new filename, remove the curly braces newPath += uuidStringWithoutCurlyBraces(assignmentUUID); return newPath; } const QString URI_OAUTH = "/oauth"; bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { const QString JSON_MIME_TYPE = "application/json"; const QString URI_ASSIGNMENT = "/assignment"; const QString URI_ASSIGNMENT_SCRIPTS = URI_ASSIGNMENT + "/scripts"; const QString URI_NODES = "/nodes"; const QString URI_SETTINGS = "/settings"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; auto nodeList = DependencyManager::get(); // allow sub-handlers to handle requests that do not require authentication if (_settingsManager.handlePublicHTTPRequest(connection, url)) { return true; } // check if this is a request for a scripted assignment (with a temp unique UUID) const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); if (connection->requestOperation() == QNetworkAccessManager::GetOperation && assignmentRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); if (!matchingAssignment) { // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); if (pendingData) { matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it // via correct URL for the script so the client can download QUrl scriptURL = url; scriptURL.setPath(URI_ASSIGNMENT + "/scripts/" + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); // have the HTTPManager serve the appropriate script file return _httpManager.handleHTTPRequest(connection, scriptURL, true); } } } // request not handled return false; } // check if this is a request for our domain ID const QString URI_ID = "/id"; if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == URI_ID) { QUuid domainID = nodeList->getSessionUUID(); connection->respond(HTTPConnection::StatusCode200, uuidStringWithoutCurlyBraces(domainID).toLocal8Bit()); return true; } // all requests below require a cookie to prove authentication so check that first if (!isAuthenticatedRequest(connection, url)) { // this is not an authenticated request // return true from the handler since it was handled with a 401 or re-direct to auth return true; } if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/assignments.json") { // user is asking for json list of assignments // setup the JSON QJsonObject assignmentJSON; QJsonObject assignedNodesJSON; // enumerate the NodeList to find the assigned nodes nodeList->eachNode([this, &assignedNodesJSON](const SharedNodePointer& node){ DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); if (!nodeData->getAssignmentUUID().isNull()) { // add the node using the UUID as the key QString uuidString = uuidStringWithoutCurlyBraces(nodeData->getAssignmentUUID()); assignedNodesJSON[uuidString] = jsonObjectForNode(node); } }); assignmentJSON["fulfilled"] = assignedNodesJSON; QJsonObject queuedAssignmentsJSON; // add the queued but unfilled assignments to the json foreach(const SharedAssignmentPointer& assignment, _unfulfilledAssignments) { QJsonObject queuedAssignmentJSON; QString uuidString = uuidStringWithoutCurlyBraces(assignment->getUUID()); queuedAssignmentJSON[JSON_KEY_TYPE] = QString(assignment->getTypeName()); // if the assignment has a pool, add it if (!assignment->getPool().isEmpty()) { queuedAssignmentJSON[JSON_KEY_POOL] = assignment->getPool(); } // add this queued assignment to the JSON queuedAssignmentsJSON[uuidString] = queuedAssignmentJSON; } assignmentJSON["queued"] = queuedAssignmentsJSON; // print out the created JSON QJsonDocument assignmentDocument(assignmentJSON); connection->respond(HTTPConnection::StatusCode200, assignmentDocument.toJson(), qPrintable(JSON_MIME_TYPE)); // we've processed this request return true; } else if (url.path() == "/transactions.json") { // enumerate our pending transactions and display them in an array QJsonObject rootObject; QJsonArray transactionArray; TransactionHash::iterator i = _pendingAssignmentCredits.begin(); while (i != _pendingAssignmentCredits.end()) { transactionArray.push_back(i.value()->toJson()); ++i; } rootObject["pending_transactions"] = transactionArray; // print out the created JSON QJsonDocument transactionsDocument(rootObject); connection->respond(HTTPConnection::StatusCode200, transactionsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); return true; } else if (url.path() == QString("%1.json").arg(URI_NODES)) { // setup the JSON QJsonObject rootJSON; QJsonArray nodesJSONArray; // enumerate the NodeList to find the assigned nodes nodeList->eachNode([this, &nodesJSONArray](const SharedNodePointer& node){ // add the node using the UUID as the key nodesJSONArray.append(jsonObjectForNode(node)); }); rootJSON["nodes"] = nodesJSONArray; // print out the created JSON QJsonDocument nodesDocument(rootJSON); // send the response connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); return true; } else { // check if this is for json stats for a node const QString NODE_JSON_REGEX_STRING = QString("\\%1\\/(%2).json\\/?$").arg(URI_NODES).arg(UUID_REGEX_STRING); QRegExp nodeShowRegex(NODE_JSON_REGEX_STRING); if (nodeShowRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(nodeShowRegex.cap(1)); // see if we have a node that matches this ID SharedNodePointer matchingNode = nodeList->nodeWithUUID(matchingUUID); if (matchingNode) { // create a QJsonDocument with the stats QJsonObject QJsonObject statsObject = reinterpret_cast(matchingNode->getLinkedData())->getStatsJSONObject(); // add the node type to the JSON data for output purposes statsObject["node_type"] = NodeType::getNodeTypeName(matchingNode->getType()).toLower().replace(' ', '-'); QJsonDocument statsDocument(statsObject); // send the response connection->respond(HTTPConnection::StatusCode200, statsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); // tell the caller we processed the request return true; } return false; } } } else if (connection->requestOperation() == QNetworkAccessManager::PostOperation) { if (url.path() == URI_ASSIGNMENT) { // this is a script upload - ask the HTTPConnection to parse the form data QList formData = connection->parseFormData(); // check optional headers for # of instances and pool const QString ASSIGNMENT_INSTANCES_HEADER = "ASSIGNMENT-INSTANCES"; const QString ASSIGNMENT_POOL_HEADER = "ASSIGNMENT-POOL"; QByteArray assignmentInstancesValue = connection->requestHeaders().value(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit()); int numInstances = 1; if (!assignmentInstancesValue.isEmpty()) { // the user has requested a specific number of instances // so set that on the created assignment numInstances = assignmentInstancesValue.toInt(); } QString assignmentPool = emptyPool; QByteArray assignmentPoolValue = connection->requestHeaders().value(ASSIGNMENT_POOL_HEADER.toLocal8Bit()); if (!assignmentPoolValue.isEmpty()) { // specific pool requested, set that on the created assignment assignmentPool = QString(assignmentPoolValue); } for (int i = 0; i < numInstances; i++) { // create an assignment for this saved script Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); QString newPath = pathForAssignmentScript(scriptAssignment->getUUID()); // create a file with the GUID of the assignment in the script host location QFile scriptFile(newPath); if (scriptFile.open(QIODevice::WriteOnly)) { scriptFile.write(formData[0].second); qDebug() << qPrintable(QString("Saved a script for assignment at %1%2") .arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool)); // add the script assigment to the assignment queue SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); _unfulfilledAssignments.enqueue(sharedScriptedAssignment); _allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment); } else { // unable to save script for assignment - we shouldn't be here but debug it out qDebug() << "Unable to save a script for assignment at" << newPath; qDebug() << "Script will not be added to queue"; } } // respond with a 200 code for successful upload connection->respond(HTTPConnection::StatusCode200); return true; } } else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) { const QString ALL_NODE_DELETE_REGEX_STRING = QString("\\%1\\/?$").arg(URI_NODES); const QString NODE_DELETE_REGEX_STRING = QString("\\%1\\/(%2)\\/$").arg(URI_NODES).arg(UUID_REGEX_STRING); QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID // pull the captured string, if it exists QUuid deleteUUID = QUuid(nodeDeleteRegex.cap(1)); SharedNodePointer nodeToKill = nodeList->nodeWithUUID(deleteUUID); if (nodeToKill) { // start with a 200 response connection->respond(HTTPConnection::StatusCode200); // we have a valid UUID and node - kill the node that has this assignment QMetaObject::invokeMethod(nodeList.data(), "killNodeWithUUID", Q_ARG(const QUuid&, deleteUUID)); // successfully processed request return true; } return true; } else if (allNodesDeleteRegex.indexIn(url.path()) != -1) { qDebug() << "Received request to kill all nodes."; nodeList->eraseAllNodes(); return true; } } // didn't process the request, let our DomainServerSettingsManager or HTTPManager handle return _settingsManager.handleAuthenticatedHTTPRequest(connection, url); } const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID"; bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url, bool skipSubHandler) { qDebug() << "HTTPS request received at" << url.toString(); if (url.path() == URI_OAUTH) { QUrlQuery codeURLQuery(url); const QString CODE_QUERY_KEY = "code"; QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY); const QString STATE_QUERY_KEY = "state"; QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY)); if (!authorizationCode.isEmpty() && !stateUUID.isNull()) { // fire off a request with this code and state to get an access token for the user const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token"; QUrl tokenRequestUrl = _oauthProviderURL; tokenRequestUrl.setPath(OAUTH_TOKEN_REQUEST_PATH); const QString OAUTH_GRANT_TYPE_POST_STRING = "grant_type=authorization_code"; QString tokenPostBody = OAUTH_GRANT_TYPE_POST_STRING; tokenPostBody += QString("&code=%1&redirect_uri=%2&client_id=%3&client_secret=%4") .arg(authorizationCode, oauthRedirectURL().toString(), _oauthClientID, _oauthClientSecret); QNetworkRequest tokenRequest(tokenRequestUrl); tokenRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply* tokenReply = NetworkAccessManager::getInstance().post(tokenRequest, tokenPostBody.toLocal8Bit()); if (_webAuthenticationStateSet.remove(stateUUID)) { // this is a web user who wants to auth to access web interface // we hold the response back to them until we get their profile information // and can decide if they are let in or not QEventLoop loop; connect(tokenReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); // start the loop for the token request loop.exec(); QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply); // stop the loop once the profileReply is complete connect(profileReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); // restart the loop for the profile request loop.exec(); // call helper method to get cookieHeaders Headers cookieHeaders = setupCookieHeadersFromProfileReply(profileReply); connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, cookieHeaders); delete tokenReply; delete profileReply; // we've redirected the user back to our homepage return true; } } // respond with a 200 code indicating that login is complete connection->respond(HTTPConnection::StatusCode200); return true; } else { return false; } } bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url) { const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; const QString ADMIN_USERS_CONFIG_KEY = "admin-users"; const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles"; const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username"; const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password"; const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; QVariantMap& settingsMap = _settingsManager.getSettingsMap(); if (!_oauthProviderURL.isEmpty() && (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-]+)($|;)"; QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); QUuid cookieUUID; if (cookieString.indexOf(cookieUUIDRegex) != -1) { cookieUUID = cookieUUIDRegex.cap(1); } if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } if (!cookieUUID.isNull() && _cookieSessionHash.contains(cookieUUID)) { // pull the QJSONObject for the user with this cookie UUID DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().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 QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList(); if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { if (adminRolesArray.contains(userRole)) { // this user has a role that allows them to administer the domain-server return true; } } } connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); // the user does not have allowed username or role, return 401 return false; } else { // re-direct this user to OAuth page // generate a random state UUID to use QUuid stateUUID = QUuid::createUuid(); // add it to the set so we can handle the callback from the OAuth provider _webAuthenticationStateSet.insert(stateUUID); QUrl oauthRedirectURL = oauthAuthorizationURL(stateUUID); Headers redirectHeaders; redirectHeaders.insert("Location", oauthRedirectURL.toEncoded()); connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); // we don't know about this user yet, so they are not yet authenticated return false; } } else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; // check if a username and password have been provided with the request QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY); if (!basicAuthString.isEmpty()) { QStringList splitAuthString = basicAuthString.split(' '); QString base64String = splitAuthString.size() == 2 ? splitAuthString[1] : ""; QString credentialString = QByteArray::fromBase64(base64String.toLocal8Bit()); if (!credentialString.isEmpty()) { QStringList credentialList = credentialString.split(':'); if (credentialList.size() == 2) { QString headerUsername = credentialList[0]; QString headerPassword = credentialList[1]; // we've pulled a username and password - now check if there is a match in our basic auth hash QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString(); const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH); QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : ""; if (settingsUsername == headerUsername && headerPassword == settingsPassword) { return true; } } } } // basic HTTP auth being used but no username and password are present // or the username and password are not correct // send back a 401 and ask for basic auth const QByteArray HTTP_AUTH_REQUEST_HEADER_KEY = "WWW-Authenticate"; static QString HTTP_AUTH_REALM_STRING = QString("Basic realm='%1 %2'") .arg(_hostname.isEmpty() ? "localhost" : _hostname) .arg("domain-server"); Headers basicAuthHeader; basicAuthHeader.insert(HTTP_AUTH_REQUEST_HEADER_KEY, HTTP_AUTH_REALM_STRING.toUtf8()); connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY, HTTPConnection::DefaultContentType, basicAuthHeader); // not authenticated, bubble up false return false; } else { // we don't have an OAuth URL + admin roles/usernames, so all users are authenticated return true; } } const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token"; QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenReply) { // pull the access token from the returned JSON and store it with the matching session UUID QJsonDocument returnedJSON = QJsonDocument::fromJson(tokenReply->readAll()); QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); // fire off a request to get this user's identity so we can see if we will let them in QUrl profileURL = _oauthProviderURL; profileURL.setPath("/api/v1/user/profile"); profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); QNetworkRequest profileRequest(profileURL); profileRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); return NetworkAccessManager::getInstance().get(profileRequest); } const QString DS_SETTINGS_SESSIONS_GROUP = "web-sessions"; Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileReply) { Headers cookieHeaders; // create a UUID for this cookie QUuid cookieUUID = QUuid::createUuid(); QJsonDocument profileDocument = QJsonDocument::fromJson(profileReply->readAll()); QJsonObject userObject = profileDocument.object()["data"].toObject()["user"].toObject(); // add the profile to our in-memory data structure so we know who the user is when they send us their cookie DomainServerWebSessionData sessionData(userObject); _cookieSessionHash.insert(cookieUUID, sessionData); // persist the cookie to settings file so we can get it back on DS relaunch QStringList path = QStringList() << DS_SETTINGS_SESSIONS_GROUP << cookieUUID.toString(); Setting::Handle(path).set(QVariant::fromValue(sessionData)); // setup expiry for cookie to 1 month from today QDateTime cookieExpiry = QDateTime::currentDateTimeUtc().addMonths(1); QString cookieString = HIFI_SESSION_COOKIE_KEY + "=" + uuidStringWithoutCurlyBraces(cookieUUID.toString()); cookieString += "; expires=" + cookieExpiry.toString("ddd, dd MMM yyyy HH:mm:ss") + " GMT"; cookieString += "; domain=" + _hostname + "; path=/"; cookieHeaders.insert("Set-Cookie", cookieString.toUtf8()); // redirect the user back to the homepage so they can present their cookie and be authenticated QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()); cookieHeaders.insert("Location", redirectString.toUtf8()); return cookieHeaders; } void DomainServer::loadExistingSessionsFromSettings() { // read data for existing web sessions into memory so existing sessions can be leveraged Settings domainServerSettings; domainServerSettings.beginGroup(DS_SETTINGS_SESSIONS_GROUP); foreach(const QString& uuidKey, domainServerSettings.childKeys()) { _cookieSessionHash.insert(QUuid(uuidKey), domainServerSettings.value(uuidKey).value()); qDebug() << "Pulled web session from settings - cookie UUID is" << uuidKey; } } void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) { QUuid oldUUID = assignment->getUUID(); assignment->resetUUID(); qDebug() << "Reset UUID for assignment -" << *assignment.data() << "- and added to queue. Old UUID was" << uuidStringWithoutCurlyBraces(oldUUID); if (assignment->getType() == Assignment::AgentType && assignment->getPayload().isEmpty()) { // if this was an Agent without a script URL, we need to rename the old file so it can be retrieved at the new UUID QFile::rename(pathForAssignmentScript(oldUUID), pathForAssignmentScript(assignment->getUUID())); } // add the static assignment back under the right UUID, and to the queue _allAssignments.insert(assignment->getUUID(), assignment); _unfulfilledAssignments.enqueue(assignment); } void DomainServer::nodeAdded(SharedNodePointer node) { // we don't use updateNodeWithData, so add the DomainServerNodeData to the node here node->setLinkedData(new DomainServerNodeData()); } void DomainServer::nodeKilled(SharedNodePointer node) { // if this peer connected via ICE then remove them from our ICE peers hash _icePeers.remove(node->getUUID()); DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); if (nodeData) { // if this node's UUID matches a static assignment we need to throw it back in the assignment queue if (!nodeData->getAssignmentUUID().isNull()) { SharedAssignmentPointer matchedAssignment = _allAssignments.take(nodeData->getAssignmentUUID()); if (matchedAssignment && matchedAssignment->isStatic()) { refreshStaticAssignmentAndAddToQueue(matchedAssignment); } } // If this node was an Agent ask JSONBreakableMarshal to potentially remove the interpolation we stored JSONBreakableMarshal::removeInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(node->getUUID())); // cleanup the connection secrets that we set up for this node (on the other nodes) foreach (const QUuid& otherNodeSessionUUID, nodeData->getSessionSecretHash().keys()) { SharedNodePointer otherNode = DependencyManager::get()->nodeWithUUID(otherNodeSessionUUID); if (otherNode) { reinterpret_cast(otherNode->getLinkedData())->getSessionSecretHash().remove(node->getUUID()); } } } } SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& assignmentUUID, NodeType_t nodeType) { QQueue::iterator i = _unfulfilledAssignments.begin(); while (i != _unfulfilledAssignments.end()) { if (i->data()->getType() == Assignment::typeForNodeType(nodeType) && i->data()->getUUID() == assignmentUUID) { // we have an unfulfilled assignment to return // return the matching assignment return _unfulfilledAssignments.takeAt(i - _unfulfilledAssignments.begin()); } else { ++i; } } return SharedAssignmentPointer(); } SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assignment& requestAssignment) { // this is an unassigned client talking to us directly for an assignment // go through our queue and see if there are any assignments to give out QQueue::iterator sharedAssignment = _unfulfilledAssignments.begin(); while (sharedAssignment != _unfulfilledAssignments.end()) { Assignment* assignment = sharedAssignment->data(); bool requestIsAllTypes = requestAssignment.getType() == Assignment::AllTypes; bool assignmentTypesMatch = assignment->getType() == requestAssignment.getType(); bool neitherHasPool = assignment->getPool().isEmpty() && requestAssignment.getPool().isEmpty(); bool assignmentPoolsMatch = assignment->getPool() == requestAssignment.getPool(); if ((requestIsAllTypes || assignmentTypesMatch) && (neitherHasPool || assignmentPoolsMatch)) { // remove the assignment from the queue SharedAssignmentPointer deployableAssignment = _unfulfilledAssignments.takeAt(sharedAssignment - _unfulfilledAssignments.begin()); // until we get a connection for this assignment // put assignment back in queue but stick it at the back so the others have a chance to go out _unfulfilledAssignments.enqueue(deployableAssignment); // stop looping, we've handed out an assignment return deployableAssignment; } else { // push forward the iterator to check the next assignment ++sharedAssignment; } } return SharedAssignmentPointer(); } void DomainServer::removeMatchingAssignmentFromQueue(const SharedAssignmentPointer& removableAssignment) { QQueue::iterator potentialMatchingAssignment = _unfulfilledAssignments.begin(); while (potentialMatchingAssignment != _unfulfilledAssignments.end()) { if (potentialMatchingAssignment->data()->getUUID() == removableAssignment->getUUID()) { _unfulfilledAssignments.erase(potentialMatchingAssignment); // we matched and removed an assignment, bail out break; } else { ++potentialMatchingAssignment; } } } void DomainServer::addStaticAssignmentsToQueue() { // if the domain-server has just restarted, // check if there are static assignments that we need to throw into the assignment queue QHash staticHashCopy = _allAssignments; QHash::iterator staticAssignment = staticHashCopy.begin(); while (staticAssignment != staticHashCopy.end()) { // add any of the un-matched static assignments to the queue // enumerate the nodes and check if there is one with an attached assignment with matching UUID if (!DependencyManager::get()->nodeWithUUID(staticAssignment->data()->getUUID())) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue refreshStaticAssignmentAndAddToQueue(*staticAssignment); } ++staticAssignment; } } void DomainServer::processPathQueryPacket(QSharedPointer packet) { // this is a query for the viewpoint resulting from a path // first pull the query path from the packet // figure out how many bytes the sender said this path is quint16 numPathBytes; packet->readPrimitive(&numPathBytes); if (numPathBytes <= packet->bytesLeftToRead()) { // the number of path bytes makes sense for the sent packet - pull out the path QString pathQuery = QString::fromUtf8(packet->getPayload() + packet->pos(), numPathBytes); // our settings contain paths that start with a leading slash, so make sure this query has that if (!pathQuery.startsWith("/")) { pathQuery.prepend("/"); } const QString PATHS_SETTINGS_KEYPATH_FORMAT = "%1.%2"; const QString PATH_VIEWPOINT_KEY = "viewpoint"; // check out paths in the _configMap to see if we have a match const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY) .arg(pathQuery)); if (pathMatch) { // we got a match, respond with the resulting viewpoint auto nodeList = DependencyManager::get(); QString responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString(); if (!responseViewpoint.isEmpty()) { QByteArray viewpointUTF8 = responseViewpoint.toUtf8(); // prepare a packet for the response auto pathResponsePacket = NLPacket::create(PacketType::DomainServerPathResponse); // check the number of bytes the viewpoint is quint16 numViewpointBytes = viewpointUTF8.size(); // are we going to be able to fit this response viewpoint in a packet? if (numPathBytes + numViewpointBytes + sizeof(numViewpointBytes) + sizeof(numPathBytes) < (unsigned long) pathResponsePacket->bytesAvailableForWrite()) { // append the number of bytes this path is pathResponsePacket->writePrimitive(numPathBytes); // append the path itself pathResponsePacket->write(pathQuery.toUtf8()); // append the number of bytes the resulting viewpoint is pathResponsePacket->writePrimitive(numViewpointBytes); // append the viewpoint itself pathResponsePacket->write(viewpointUTF8); qDebug() << "Sending a viewpoint response for path query" << pathQuery << "-" << viewpointUTF8; // send off the packet - see if we can associate this outbound data to a particular node // TODO: does this senderSockAddr always work for a punched DS client? nodeList->sendPacket(std::move(pathResponsePacket), packet->getSenderSockAddr()); } } } else { // we don't respond if there is no match - this may need to change once this packet // query/response is made reliable qDebug() << "No match for path query" << pathQuery << "- refusing to respond."; } } }