// // DomainGatekeeper.cpp // domain-server/src // // Created by Stephen Birarda on 2015-08-24. // Copyright 2015 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 "DomainGatekeeper.h" #include #include #include #include #include #include #include #include "DomainServer.h" #include "DomainServerNodeData.h" using SharedAssignmentPointer = QSharedPointer; DomainGatekeeper::DomainGatekeeper(DomainServer* server) : _server(server) { initLocalIDManagement(); } void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, const QUuid& walletUUID, const QString& nodeVersion) { _pendingAssignedNodes.emplace(std::piecewise_construct, std::forward_as_tuple(nodeUUID), std::forward_as_tuple(assignmentUUID, walletUUID, nodeVersion)); } QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) { auto it = _pendingAssignedNodes.find(tempUUID); if (it != _pendingAssignedNodes.end()) { return it->second.getAssignmentUUID(); } else { return QUuid(); } } const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer; void DomainGatekeeper::processConnectRequestPacket(QSharedPointer message) { if (message->getSize() == 0) { return; } QDataStream packetStream(message->getMessage()); // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); QByteArray myProtocolVersion = protocolVersionsSignature(); if (nodeConnection.protocolVersion != myProtocolVersion) { sendProtocolMismatchConnectionDenial(message->getSenderSockAddr()); return; } if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; return; } static const NodeSet VALID_NODE_TYPES { NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer, NodeType::EntityScriptServer }; if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) { qDebug() << "Received an invalid node type with connect request. Will not allow connection from" << nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType; return; } // check if this connect request matches an assignment in the queue auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedNodePointer node; if (pendingAssignment != _pendingAssignedNodes.end()) { node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QString username; QByteArray usernameSignature; if (message->getBytesLeftToRead() > 0) { // read username from packet packetStream >> username; if (message->getBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; } } node = processAgentConnectRequest(nodeConnection, username, usernameSignature); } if (node) { // set the sending sock addr and node interest set on this node DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); // guard against patched agents asking to hear about other agents auto safeInterestSet = nodeConnection.interestList.toSet(); if (nodeConnection.nodeType == NodeType::Agent) { safeInterestSet.remove(NodeType::Agent); } nodeData->setNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) << "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress << "and machine fingerprint" << nodeConnection.machineFingerprint; // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); } else { qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() << "with hardware address" << nodeConnection.hardwareAddress << "and machine fingerprint" << nodeConnection.machineFingerprint; } } NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress, const QString& hardwareAddress, const QUuid& machineFingerprint) { NodePermissions userPerms; userPerms.setAll(false); if (isLocalUser) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); #ifdef WANT_DEBUG qDebug() << "| user-permissions: is local user, so:" << userPerms; #endif } if (verifiedUsername.isEmpty()) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); #ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; #endif if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { // this user comes from a MAC we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; #endif } } else { if (_server->_settingsManager.havePermissionsForName(verifiedUsername)) { userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms; #endif } else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { // this user comes from a MAC we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; #endif } else { // they are logged into metaverse, but we don't have specific permissions for them. userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is logged-into metaverse, so:" << userPerms; #endif // if this user is a friend of the domain-owner, give them friend's permissions if (_domainOwnerFriends.contains(verifiedUsername)) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameFriends); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is friends with domain-owner, so:" << userPerms; #endif } // if this user is a known member of a group, give them the implied permissions foreach (QUuid groupID, _server->_settingsManager.getGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms |= _server->_settingsManager.getPermissionsForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:" << rank.name << "so:" << userPerms; #endif } } // if this user is a known member of a blacklist group, remove the implied permissions foreach (QUuid groupID, _server->_settingsManager.getBlacklistGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms &= ~_server->_settingsManager.getForbiddensForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is in blacklist group:" << groupID << " rank:" << rank.name << "so:" << userPerms; #endif } } } } userPerms.setID(verifiedUsername); userPerms.setVerifiedUserName(verifiedUsername); } #ifdef WANT_DEBUG qDebug() << "| user-permissions: final:" << userPerms; #endif return userPerms; } void DomainGatekeeper::updateNodePermissions() { // If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required -- // we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all // the connected nodes, so these changes are propagated to other nodes. QList nodesToKill; auto limitedNodeList = DependencyManager::get(); QWeakPointer limitedNodeListWeak = limitedNodeList; limitedNodeList->eachNode([this, limitedNodeListWeak, &nodesToKill](const SharedNodePointer& node){ // the id and the username in NodePermissions will often be the same, but id is set before // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { // this node is an assignment-client userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canRezPermanentCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; } else { // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); QString hardwareAddress; QUuid machineFingerprint; bool isLocalUser { false }; DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); if (nodeData) { hardwareAddress = nodeData->getHardwareAddress(); machineFingerprint = nodeData->getMachineFingerprint(); auto sendingAddress = nodeData->getSendingSockAddr().getAddress(); auto nodeList = limitedNodeListWeak.lock(); isLocalUser = ((nodeList && sendingAddress == nodeList->getLocalSockAddr().getAddress()) || sendingAddress == QHostAddress::LocalHost); } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress, machineFingerprint); } node->setPermissions(userPerms); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { qDebug() << "node" << node->getUUID() << "no longer has permission to connect."; // hang up on this node nodesToKill << node; } }); foreach (auto node, nodesToKill) { emit killNode(node); } } SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment) { // make sure this matches an assignment the DS told us we sent out auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); if (it != _pendingAssignedNodes.end()) { // find the matching queued static assignment in DS queue matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType); if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); } 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(nodeConnection.connectUUID); return SharedNodePointer(); } } else { qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } // add the new node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); DomainServerNodeData* nodeData = static_cast(newNode->getLinkedData()); // set assignment related data on the linked data for this node nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setHardwareAddress(nodeConnection.hardwareAddress); nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); NodePermissions userPerms; userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; // always allow assignment clients to create and destroy entities userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canRezPermanentCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; newNode->setPermissions(userPerms); return newNode; } const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature) { auto limitedNodeList = DependencyManager::get(); // start with empty permissions NodePermissions userPerms(NodePermissionsKey(username, 0)); userPerms.setAll(false); // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); QString verifiedUsername; // if this remains empty, consider this an anonymous connection attempt if (!username.isEmpty()) { const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); if (usernameSignature.isEmpty() || connectionToken.isNull()) { // user is attempting to prove their identity to us, but we don't have enough information sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); // ask for their public key right now to make sure we have it requestUserPublicKey(username, true); getGroupMemberships(username); // optimistically get started on group memberships #ifdef WANT_DEBUG qDebug() << "stalling login because we have no username-signature:" << username; #endif return SharedNodePointer(); } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they sent us a username and the signature verifies it getGroupMemberships(username); verifiedUsername = username.toLower(); } else { // they sent us a username, but it didn't check out requestUserPublicKey(username); #ifdef WANT_DEBUG qDebug() << "stalling login because signature verification failed:" << username; #endif return SharedNodePointer(); } } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(), nodeConnection.hardwareAddress, nodeConnection.machineFingerprint); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorized); #ifdef WANT_DEBUG qDebug() << "stalling login due to permissions:" << username; #endif return SharedNodePointer(); } if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) { // we can't allow this user to connect because we are at max capacity QString redirectOnMaxCapacity; QVariant redirectOnMaxCapacityVariant = _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); if (redirectOnMaxCapacityVariant.canConvert()) { redirectOnMaxCapacity = redirectOnMaxCapacityVariant.toString(); qDebug() << "Redirection domain:" << redirectOnMaxCapacity; } sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers, redirectOnMaxCapacity); #ifdef WANT_DEBUG qDebug() << "stalling login due to max capacity:" << username; #endif return SharedNodePointer(); } QUuid existingNodeID; // in case this is a node that's failing to connect // double check we don't have the same node whose sockets match exactly already in the list limitedNodeList->eachNodeBreakable([nodeConnection, username, &existingNodeID](const SharedNodePointer& node){ if (node->getPublicSocket() == nodeConnection.publicSockAddr && node->getLocalSocket() == nodeConnection.localSockAddr) { // we have a node that already has these exact sockets // this can occur if a node is failing to connect to the domain // remove the old node before adding the new node qDebug() << "Deleting existing connection from same sockaddr: " << node->getUUID(); existingNodeID = node->getUUID(); return false; } return true; }); if (!existingNodeID.isNull()) { limitedNodeList->killNodeWithUUID(existingNodeID); } // add the connecting node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); // set the edit rights for this user newNode->setPermissions(userPerms); // grab the linked data for our new node so we can set the username DomainServerNodeData* nodeData = static_cast(newNode->getLinkedData()); // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); // set the hardware address passed in the connect request nodeData->setHardwareAddress(nodeConnection.hardwareAddress); // set the machine fingerprint passed in the connect request nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); #ifdef WANT_DEBUG qDebug() << "accepting login:" << username; #endif return newNode; } SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) { HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); if (connectedPeer && connectedPeer->getActiveSocket()) { // set their discovered socket to whatever the activated socket on the network peer object was discoveredSocket = *connectedPeer->getActiveSocket(); } // create a new node ID for the verified connecting node auto nodeID = QUuid::createUuid(); // add a mapping from connection node ID to ICE peer ID // so that we can remove the ICE peer once we see this node connect _nodeToICEPeerIDs.insert(nodeID, nodeConnection.connectUUID); auto limitedNodeList = DependencyManager::get(); Node::LocalID newLocalID = findOrCreateLocalID(nodeID); SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType, nodeConnection.publicSockAddr, nodeConnection.localSockAddr, newLocalID); // 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); return newNode; } void DomainGatekeeper::cleanupICEPeerForNode(const QUuid& nodeID) { // remove this node ID from our node to ICE peer ID map // and the associated ICE peer (if it still exists) auto icePeerID = _nodeToICEPeerIDs.take(nodeID); if (!icePeerID.isNull()) { _icePeers.remove(icePeerID); } } bool DomainGatekeeper::verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr) { // it's possible this user can be allowed to connect, but we need to check their username signature auto lowerUsername = username.toLower(); KeyFlagPair publicKeyPair = _userPublicKeys.value(lowerUsername); QByteArray publicKeyArray = publicKeyPair.first; bool isOptimisticKey = publicKeyPair.second; const QUuid& connectionToken = _connectionTokenHash.value(lowerUsername); if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) { // 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()); QByteArray lowercaseUsernameUTF8 = lowerUsername.toUtf8(); QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsernameUTF8.append(connectionToken.toRfc4122()), QCryptographicHash::Sha256); if (rsaPublicKey) { int decryptResult = RSA_verify(NID_sha256, reinterpret_cast(usernameWithToken.constData()), usernameWithToken.size(), reinterpret_cast(usernameSignature.constData()), usernameSignature.size(), rsaPublicKey); if (decryptResult == 1) { qDebug() << "Username signature matches for" << username; // free up the public key and remove connection token before we return RSA_free(rsaPublicKey); _connectionTokenHash.remove(username); return true; } else { // we only send back a LoginError if this wasn't an "optimistic" key // (a key that we hoped would work but is probably stale) if (!senderSockAddr.isNull() && !isOptimisticKey) { qDebug() << "Error decrypting username signature for" << username << "- denying connection."; sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginError); } else if (!senderSockAddr.isNull()) { qDebug() << "Error decrypting username signature for" << username << "with optimisitic key -" << "re-requesting public key and delaying connection"; } // 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 if (!senderSockAddr.isNull()) { qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginError); } } } else { if (!senderSockAddr.isNull()) { qDebug() << "Insufficient data to decrypt username signature - delaying connection."; } } requestUserPublicKey(username); // no joy. maybe next time? return false; } bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is QVariant maximumUserCapacityVariant = _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY); unsigned int maximumUserCapacity = maximumUserCapacityVariant.isValid() ? maximumUserCapacityVariant.toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); if (connectedUsers >= maximumUserCapacity) { qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; return false; } qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection."; } return true; } void DomainGatekeeper::requestUserPublicKey(const QString& username, bool isOptimistic) { // don't request public keys for the standard psuedo-account-names if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { return; } QString lowerUsername = username.toLower(); if (_inFlightPublicKeyRequests.contains(lowerUsername)) { // public-key request for this username is already flight, not rerequesting return; } _inFlightPublicKeyRequests.insert(lowerUsername, isOptimistic); // even if we have a public key for them right now, request a new one in case it has just changed JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "publicKeyJSONCallback"; callbackParams.errorCallbackMethod = "publicKeyJSONErrorCallback"; const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; qDebug().nospace() << "Requesting " << (isOptimistic ? "optimistic " : " ") << "public key for user " << username; DependencyManager::get()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); } QString extractUsernameFromPublicKeyRequest(QNetworkReply* requestReply) { // extract the username from the request url QString username; 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) { username = usernameRegex.cap(1); } return username.toLower(); } void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply* requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object(); QString username = extractUsernameFromPublicKeyRequest(requestReply); bool isOptimisticKey = _inFlightPublicKeyRequests.take(username); if (jsonObject["status"].toString() == "success" && !username.isEmpty()) { // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; qDebug().nospace() << "Extracted " << (isOptimisticKey ? "optimistic " : " ") << "public key for " << username.toLower(); _userPublicKeys[username.toLower()] = { QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()), isOptimisticKey }; } } void DomainGatekeeper::publicKeyJSONErrorCallback(QNetworkReply* requestReply) { qDebug() << "publicKey api call failed:" << requestReply->error(); QString username = extractUsernameFromPublicKeyRequest(requestReply); _inFlightPublicKeyRequests.remove(username); } void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) { QString protocolVersionError = "Protocol version mismatch - Domain version: " + QCoreApplication::applicationVersion(); qDebug() << "Protocol Version mismatch - denying connection."; sendConnectionDeniedPacket(protocolVersionError, senderSockAddr, DomainHandler::ConnectionRefusedReason::ProtocolMismatch); } void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, DomainHandler::ConnectionRefusedReason reasonCode, QString extraInfo) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfReasonString = reason.toUtf8(); quint16 reasonSize = utfReasonString.size(); QByteArray utfExtraInfo = extraInfo.toUtf8(); quint16 extraInfoSize = utfExtraInfo.size(); // setup the DomainConnectionDenied packet auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, sizeof(uint8_t) + // reasonCode reasonSize + sizeof(reasonSize) + extraInfoSize + sizeof(extraInfoSize)); // pack in the reason the connection was denied (the client displays this) uint8_t reasonCodeWire = (uint8_t)reasonCode; connectionDeniedPacket->writePrimitive(reasonCodeWire); connectionDeniedPacket->writePrimitive(reasonSize); connectionDeniedPacket->write(utfReasonString); // write the extra info as well connectionDeniedPacket->writePrimitive(extraInfoSize); connectionDeniedPacket->write(utfExtraInfo); // send the packet off DependencyManager::get()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr); } void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) { // get the existing connection token or create a new one QUuid& connectionToken = _connectionTokenHash[username.toLower()]; if (connectionToken.isNull()) { connectionToken = QUuid::createUuid(); } // setup a static connection token packet static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID); // reset the packet before each time we send connectionTokenPacket->reset(); // write the connection token connectionTokenPacket->write(connectionToken.toRfc4122()); // send off the packet unreliably DependencyManager::get()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr); } const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS; void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) { if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) { // 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 DomainGatekeeper::handlePeerPingTimeout() { NetworkPeer* senderPeer = qobject_cast(sender()); if (senderPeer) { SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID()); if (sharedPeer && !sharedPeer->getActiveSocket()) { pingPunchForConnectingPeer(sharedPeer); } } } void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer message) { // 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(message->getMessage()); 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, &DomainGatekeeper::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 DomainGatekeeper::processICEPingPacket(QSharedPointer message) { auto limitedNodeList = DependencyManager::get(); // before we respond to this ICE ping packet, make sure we have a peer in the list that matches QUuid icePeerID = QUuid::fromRfc4122({ message->getRawMessage(), NUM_BYTES_RFC4122_UUID }); if (_icePeers.contains(icePeerID)) { auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr()); } } void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer message) { QDataStream packetStream(message->getMessage()); 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(message->getSenderSockAddr()); } } void DomainGatekeeper::getGroupMemberships(const QString& username) { // loop through the groups mentioned on the settings page and ask if this user is in each. The replies // will be received asynchronously and permissions will be updated as the answers come in. QJsonObject json; QSet groupIDSet; foreach (QUuid groupID, _server->_settingsManager.getGroupIDs() + _server->_settingsManager.getBlacklistGroupIDs()) { groupIDSet += groupID.toString().mid(1,36); } if (groupIDSet.isEmpty()) { // if no groups are in the permissions settings, don't ask who is in which groups. return; } QJsonArray groupIDs = QJsonArray::fromStringList(groupIDSet.toList()); json["groups"] = groupIDs; // if we've already asked, wait for the answer before asking again QString lowerUsername = username.toLower(); if (_inFlightGroupMembershipsRequests.contains(lowerUsername)) { // public-key request for this username is already flight, not rerequesting return; } _inFlightGroupMembershipsRequests += lowerUsername; JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "getIsGroupMemberJSONCallback"; callbackParams.errorCallbackMethod = "getIsGroupMemberErrorCallback"; const QString GET_IS_GROUP_MEMBER_PATH = "api/v1/groups/members/%2"; DependencyManager::get()->sendRequest(GET_IS_GROUP_MEMBER_PATH.arg(username), AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, callbackParams, QJsonDocument(json).toJson()); } QString extractUsernameFromGroupMembershipsReply(QNetworkReply* requestReply) { // extract the username from the request url QString username; const QString GROUP_MEMBERSHIPS_URL_REGEX_STRING = "api\\/v1\\/groups\\/members\\/([A-Za-z0-9_\\.]+)"; QRegExp usernameRegex(GROUP_MEMBERSHIPS_URL_REGEX_STRING); if (usernameRegex.indexIn(requestReply->url().toString()) != -1) { username = usernameRegex.cap(1); } return username.toLower(); } void DomainGatekeeper::getIsGroupMemberJSONCallback(QNetworkReply* requestReply) { // { // "data":{ // "username":"sethalves", // "groups":{ // "fd55479a-265d-4990-854e-3d04214ad1b0":{ // "name":"Blerg Blah", // "rank":{ // "name":"admin", // "order":1 // } // } // } // }, // "status":"success" // } QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object(); if (jsonObject["status"].toString() == "success") { QJsonObject data = jsonObject["data"].toObject(); QJsonObject groups = data["groups"].toObject(); QString username = data["username"].toString(); _server->_settingsManager.clearGroupMemberships(username); foreach (auto groupID, groups.keys()) { QJsonObject group = groups[groupID].toObject(); QJsonObject rank = group["rank"].toObject(); QUuid rankID = QUuid(rank["id"].toString()); _server->_settingsManager.recordGroupMembership(username, groupID, rankID); } } else { qDebug() << "getIsGroupMember api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); } _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); } void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply) { qDebug() << "getIsGroupMember api call failed:" << requestReply->error(); _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); } void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "getDomainOwnerFriendsListJSONCallback"; callbackParams.errorCallbackMethod = "getDomainOwnerFriendsListErrorCallback"; const QString GET_FRIENDS_LIST_PATH = "api/v1/user/friends"; if (DependencyManager::get()->hasValidAccessToken()) { DependencyManager::get()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required, QNetworkAccessManager::GetOperation, callbackParams, QByteArray(), NULL, QVariantMap()); } } void DomainGatekeeper::getDomainOwnerFriendsListJSONCallback(QNetworkReply* requestReply) { // { // status: "success", // data: { // friends: [ // "chris", // "freidrica", // "G", // "huffman", // "leo", // "philip", // "ryan", // "sam", // "ZappoMan" // ] // } // } QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object(); if (jsonObject["status"].toString() == "success") { _domainOwnerFriends.clear(); QJsonArray friends = jsonObject["data"].toObject()["friends"].toArray(); for (int i = 0; i < friends.size(); i++) { _domainOwnerFriends += friends.at(i).toString().toLower(); } } else { qDebug() << "getDomainOwnerFriendsList api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); } } void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* requestReply) { qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error(); } void DomainGatekeeper::refreshGroupsCache() { // if agents are connected to this domain, refresh our cached information about groups and memberships in such. getDomainOwnerFriendsList(); auto nodeList = DependencyManager::get(); nodeList->eachNode([this](const SharedNodePointer& node) { if (!node->getPermissions().isAssignment) { // this node is an agent const QString& verifiedUserName = node->getPermissions().getVerifiedUserName(); if (!verifiedUserName.isEmpty()) { getGroupMemberships(verifiedUserName); } } }); _server->_settingsManager.apiRefreshGroupInformation(); updateNodePermissions(); #if WANT_DEBUG _server->_settingsManager.debugDumpGroupsState(); #endif } void DomainGatekeeper::initLocalIDManagement() { std::uniform_int_distribution sixteenBitRand; std::random_device randomDevice; std::default_random_engine engine { randomDevice() }; _currentLocalID = sixteenBitRand(engine); // Ensure increment is odd. _idIncrement = sixteenBitRand(engine) | 1; } Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { auto existingLocalIDIt = _uuidToLocalID.find(uuid); if (existingLocalIDIt != _uuidToLocalID.end()) { return existingLocalIDIt->second; } assert(_localIDs.size() < std::numeric_limits::max() - 2); Node::LocalID newLocalID; do { newLocalID = _currentLocalID; _currentLocalID += _idIncrement; } while (newLocalID == Node::NULL_LOCAL_ID || _localIDs.find(newLocalID) != _localIDs.end()); _uuidToLocalID.emplace(uuid, newLocalID); _localIDs.insert(newLocalID); return newLocalID; }