diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp new file mode 100644 index 0000000000..0b0e0fedd3 --- /dev/null +++ b/domain-server/src/DomainGatekeeper.cpp @@ -0,0 +1,611 @@ +// +// 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 "DomainServer.h" +#include "DomainServerNodeData.h" + +using SharedAssignmentPointer = QSharedPointer; + +DomainGatekeeper::DomainGatekeeper(DomainServer* server) : + _server(server) +{ + +} + +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; + +void DomainGatekeeper::processConnectRequestPacket(QSharedPointer packet) { + if (packet->getPayloadSize() == 0) { + return; + } + + QDataStream packetStream(packet.data()); + + // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it + NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, packet->getSenderSockAddr()); + + if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.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 + 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 (packet->bytesLeftToRead() > 0) { + // read username from packet + packetStream >> username; + + if (packet->bytesLeftToRead() > 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 = reinterpret_cast(node->getLinkedData()); + nodeData->setSendingSockAddr(packet->getSenderSockAddr()); + nodeData->setNodeInterestSet(nodeConnection.interestList.toSet()); + + // 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" << packet->getSenderSockAddr(); + } +} + +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 = reinterpret_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()); + + // cleanup the PendingAssignedNodeData for this assignment now that it's connecting + _pendingAssignedNodes.erase(it); + + // always allow assignment clients to create and destroy entities + newNode->setCanAdjustLocks(true); + newNode->setCanRez(true); + + return newNode; +} + +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"; + +SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, + const QString& username, + const QByteArray& usernameSignature) { + + auto limitedNodeList = DependencyManager::get(); + + bool isRestrictingAccess = + _server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + + // check if this user is on our local machine - if this is true they are always allowed to connect + QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); + bool isLocalUser = + (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); + + // if we're using restricted access and this user is not local make sure we got a user signature + if (isRestrictingAccess && !isLocalUser) { + if (!username.isEmpty()) { + if (usernameSignature.isEmpty()) { + // if user didn't include usernameSignature in connect request, send a connectionToken packet + sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); + + // ask for their public key right now to make sure we have it + requestUserPublicKey(username); + + return SharedNodePointer(); + } + } + } + + bool verifiedUsername = false; + + // if we do not have a local user we need to subject them to our verification and capacity checks + if (!isLocalUser) { + + // check if we need to look at the username signature + if (isRestrictingAccess) { + if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) { + // we verified the user via their username and signature - set the verifiedUsername + // so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to + // being in the allowed editors list) + verifiedUsername = true; + } else { + // failed to verify user - return a null shared ptr + return SharedNodePointer(); + } + } + + if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) { + // we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor + // or couldn't be verified as one) + return SharedNodePointer(); + } + } + + // 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(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); + QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); + + // if the allowed editors list is empty then everyone can adjust locks + bool canAdjustLocks = allowedEditors.empty(); + + if (allowedEditors.contains(username)) { + // we have a non-empty allowed editors list - check if this user is verified to be in it + if (!verifiedUsername) { + if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) { + // failed to verify a user that is in the allowed editors list + + // TODO: fix public key refresh in interface/metaverse and force this check + qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user" + << "will be given edit rights to avoid a thrasing of public key requests and connect requests."; + } + + canAdjustLocks = true; + } else { + // already verified this user and they are in the allowed editors list + canAdjustLocks = true; + } + } + + // check if only editors should be able to rez entities + const QVariant* editorsAreRezzersVariant = + valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); + + bool onlyEditorsAreRezzers = false; + if (editorsAreRezzersVariant) { + onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); + } + + bool canRez = true; + if (onlyEditorsAreRezzers) { + canRez = canAdjustLocks; + } + + // add the new node + SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); + + // set the edit rights for this user + newNode->setCanAdjustLocks(canAdjustLocks); + newNode->setCanRez(canRez); + + // grab the linked data for our new node so we can set the username + DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); + + // 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(newNode->getUUID()), username); + + return newNode; +} + +SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) { + HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; + SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); + + QUuid nodeUUID; + + if (connectedPeer) { + // this user negotiated a connection with us via ICE, so re-use their ICE client ID + nodeUUID = nodeConnection.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(); + } + + auto limitedNodeList = DependencyManager::get(); + + SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeUUID, nodeConnection.nodeType, + nodeConnection.publicSockAddr, nodeConnection.localSockAddr); + + // 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; +} + +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 + QByteArray publicKeyArray = _userPublicKeys.value(username); + + const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); + + 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 lowercaseUsername = username.toLower().toUtf8(); + QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()), + QCryptographicHash::Sha256); + + if (rsaPublicKey) { + QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); + 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 << "- allowing connection."; + + // free up the public key and remove connection token before we return + RSA_free(rsaPublicKey); + _connectionTokenHash.remove(username); + + return true; + + } else { + if (!senderSockAddr.isNull()) { + qDebug() << "Error decrypting username signature for " << username << "- denying connection."; + sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr); + } + + // 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); + } + } + } else { + if (!senderSockAddr.isNull()) { + qDebug() << "Insufficient data to decrypt username signature - denying connection."; + sendConnectionDeniedPacket("Insufficient data", senderSockAddr); + } + } + + requestUserPublicKey(username); // no joy. maybe next time? + return false; +} + +bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, + const HifiSockAddr& senderSockAddr) { + + if (username.isEmpty()) { + qDebug() << "Connect request denied - no username provided."; + + sendConnectionDeniedPacket("No username provided", senderSockAddr); + + return false; + } + + QStringList allowedUsers = + _server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); + + if (allowedUsers.contains(username, Qt::CaseInsensitive)) { + if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) { + return false; + } + } else { + qDebug() << "Connect request denied for user" << username << "- not in allowed users list."; + sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr); + + return false; + } + + return true; +} + +bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, + bool& verifiedUsername, + const HifiSockAddr& senderSockAddr) { + // find out what our maximum capacity is + const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); + unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; + + if (maximumUserCapacity > 0) { + unsigned int connectedUsers = _server->countConnectedUsers(); + + if (connectedUsers >= maximumUserCapacity) { + // too many users, deny the new connection unless this user is an allowed editor + + const QVariant* allowedEditorsVariant = + valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); + + QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); + if (allowedEditors.contains(username)) { + if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) { + verifiedUsername = true; + qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity << + "but user" << username << "is in allowed editors list so will be allowed to connect."; + return true; + } + } + + // deny connection from this user + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; + sendConnectionDeniedPacket("Too many connected users.", senderSockAddr); + + return false; + } + + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection."; + } + + return true; +} + + +void DomainGatekeeper::preloadAllowedUserPublicKeys() { + const QVariant* allowedUsersVariant = valueForKeyPath(_server->_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 DomainGatekeeper::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); +} + +void DomainGatekeeper::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 DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr) { + // 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(); + + // setup the DomainConnectionDenied packet + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize)); + + // pack in the reason the connection was denied (the client displays this) + if (payloadSize > 0) { + connectionDeniedPacket->writePrimitive(payloadSize); + connectionDeniedPacket->write(utfString); + } + + // 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 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, &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 packet) { + auto limitedNodeList = DependencyManager::get(); + auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*packet, limitedNodeList->getSessionUUID()); + + limitedNodeList->sendPacket(std::move(pingReplyPacket), packet->getSenderSockAddr()); +} + +void DomainGatekeeper::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()); + } +} diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h new file mode 100644 index 0000000000..857b4419da --- /dev/null +++ b/domain-server/src/DomainGatekeeper.h @@ -0,0 +1,93 @@ +// +// DomainGatekeeper.h +// 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 +// + +#pragma once + +#ifndef hifi_DomainGatekeeper_h +#define hifi_DomainGatekeeper_h + +#include + +#include +#include + +#include +#include +#include + +#include "NodeConnectionData.h" +#include "PendingAssignedNodeData.h" + +class DomainServer; + +class DomainGatekeeper : public QObject { + Q_OBJECT +public: + DomainGatekeeper(DomainServer* server); + + void addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, + const QUuid& walletUUID, const QString& nodeVersion); + QUuid assignmentUUIDForPendingAssignment(const QUuid& tempUUID); + + void preloadAllowedUserPublicKeys(); + + void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); } +public slots: + void processConnectRequestPacket(QSharedPointer packet); + void processICEPingPacket(QSharedPointer packet); + void processICEPingReplyPacket(QSharedPointer packet); + void processICEPeerInformationPacket(QSharedPointer packet); + + void publicKeyJSONCallback(QNetworkReply& requestReply); + +signals: + void connectedNode(SharedNodePointer node); + +private slots: + void handlePeerPingTimeout(); +private: + SharedNodePointer processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, + const PendingAssignedNodeData& pendingAssignment); + SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection, + const QString& username, + const QByteArray& usernameSignature); + SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); + + bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, + const HifiSockAddr& senderSockAddr); + bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, + const HifiSockAddr& senderSockAddr); + bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, + bool& verifiedUsername, + const HifiSockAddr& senderSockAddr); + + bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, + const HifiSockAddr& senderSockAddr); + + void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr); + void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr); + + void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); + + void requestUserPublicKey(const QString& username); + + DomainServer* _server; + + std::unordered_map _pendingAssignedNodes; + + QHash _icePeers; + + QHash _connectionTokenHash; + QHash _userPublicKeys; +}; + + +#endif // hifi_DomainGatekeeper_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 23957380e6..392c09bd8a 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -9,9 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include -#include +#include "DomainServer.h" #include #include @@ -38,24 +36,19 @@ #include #include "DomainServerNodeData.h" - -#include "DomainServer.h" +#include "NodeConnectionData.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), + _gatekeeper(this), _httpManager(DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), _httpsManager(NULL), _allAssignments(), _unfulfilledAssignments(), - _pendingAssignedNodes(), _isUsingDTLS(false), _oauthProviderURL(), _oauthClientID(), @@ -94,6 +87,9 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); + + // make sure we hear about newly connected nodes from our gatekeeper + connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { // we either read a certificate and private key or were not passed one @@ -108,7 +104,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : setupAutomaticNetworking(); // preload some user public keys so they can connect on first request - preloadAllowedUserPublicKeys(); + _gatekeeper.preloadAllowedUserPublicKeys(); } } @@ -284,13 +280,13 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { // 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::DomainConnectRequest, &_gatekeeper, "processConnectRequestPacket"); packetReceiver.registerListener(PacketType::DomainListRequest, this, "processListRequestPacket"); packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket"); packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket"); - packetReceiver.registerListener(PacketType::ICEPing, this, "processICEPingPacket"); - packetReceiver.registerListener(PacketType::ICEPingReply, this, "processICEPingReplyPacket"); - packetReceiver.registerListener(PacketType::ICEServerPeerInformation, this, "processICEPeerInformationPacket"); + packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket"); + packetReceiver.registerListener(PacketType::ICEPingReply, &_gatekeeper, "processICEPingReplyPacket"); + packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_gatekeeper, "processICEPeerInformationPacket"); // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); @@ -575,217 +571,20 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet 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 - packetStream >> username; - } - - 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 - bool isLocalUser = (senderSockAddr.getAddress() == DependencyManager::get()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost); - - if (isRestrictingAccess && !isLocalUser) { - if (!username.isEmpty()) { - // if there's a username, try to unpack username signature - packetStream >> usernameSignature; - - if (usernameSignature.isEmpty()) { - // if user didn't include usernameSignature in connect request, send a connectionToken packet - QUuid& connectionToken = _connectionTokenHash[username.toLower()]; - - if (connectionToken.isNull()) { - connectionToken = QUuid::createUuid(); - } - - static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID); - connectionTokenPacket->reset(); - connectionTokenPacket->write(connectionToken.toRfc4122()); - limitedNodeList->sendUnreliablePacket(*connectionTokenPacket, packet->getSenderSockAddr()); - return; - } - } - } - - 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)); - if (payloadSize > 0) { - 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()); - nodeData->setNodeVersion(pendingAssigneeData->getNodeVersion()); - - // 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()); + NodeConnectionData nodeRequestData = NodeConnectionData::fromDataStream(packetStream, packet->getSenderSockAddr(), false); - parseNodeData(packetStream, throwawayNodeType, nodePublicAddress, nodeLocalAddress, packet->getSenderSockAddr()); + // update this node's sockets in case they have changed + sendingNode->setPublicSocket(nodeRequestData.publicSockAddr); + sendingNode->setLocalSocket(nodeRequestData.localSockAddr); + + // update the NodeInterestSet in case there have been any changes + DomainServerNodeData* nodeData = reinterpret_cast(sendingNode->getLinkedData()); + nodeData->setNodeInterestSet(nodeRequestData.interestList.toSet()); - sendingNode->setPublicSocket(nodePublicAddress); - sendingNode->setLocalSocket(nodeLocalAddress); - - QList nodeInterestList; - packetStream >> nodeInterestList; - - sendDomainListToNode(sendingNode, packet->getSenderSockAddr(), nodeInterestList.toSet()); + sendDomainListToNode(sendingNode, packet->getSenderSockAddr()); } unsigned int DomainServer::countConnectedUsers() { @@ -799,156 +598,6 @@ unsigned int DomainServer::countConnectedUsers() { 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); - - const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); - - 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 lowercaseUsername = username.toLower().toUtf8(); - QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()), - QCryptographicHash::Sha256); - - if (rsaPublicKey) { - QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); - 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 << "- allowing connection."; - - // free up the public key and remove connection token before we return - RSA_free(rsaPublicKey); - _connectionTokenHash.remove(username); - - return true; - - } else { - qDebug() << "Error decrypting username signature for " << username << "- denying connection."; - reasonReturn = "Error decrypting username 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."; - } - } else { - qDebug() << "Insufficient data to decrypt username signature - denying connection."; - reasonReturn = "Insufficient data"; - } - - requestUserPublicKey(username); // no joy. maybe next time? - return false; -} - - -bool DomainServer::shouldAllowConnectionFromNode(const QString& username, - const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr, - QString& reasonReturn) { - - //TODO: improve flow so these bools aren't declared twice - bool isRestrictingAccess = - _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - bool isLocalUser = (senderSockAddr.getAddress() == DependencyManager::get()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost); - - if (isRestrictingAccess && !isLocalUser) { - QStringList allowedUsers = - _settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); - - if (allowedUsers.contains(username, Qt::CaseInsensitive)) { - if (username.isEmpty()) { - qDebug() << "Connect request denied - no username provided."; - reasonReturn = "No username provided"; - return false; - } - 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()); } @@ -983,30 +632,18 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { return authorizationURL; } -int DomainServer::parseNodeData(QDataStream& packetStream, NodeType_t& nodeType, - HifiSockAddr& publicSockAddr, HifiSockAddr& localSockAddr, - const HifiSockAddr& senderSockAddr) { - packetStream >> nodeType; - packetStream >> publicSockAddr >> localSockAddr; +void DomainServer::handleConnectedNode(SharedNodePointer newNode) { - 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(); + DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); + + // reply back to the user with a PacketType::DomainList + sendDomainListToNode(newNode, nodeData->getSendingSockAddr()); + + // send out this node to our other connected nodes + broadcastNewNode(newNode); } -void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr, - const NodeSet& nodeInterestSet) { +void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { 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 @@ -1029,7 +666,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); // store the nodeInterestSet on this DomainServerNodeData, in case it has changed - nodeData->setNodeInterestSet(nodeInterestSet); + auto& nodeInterestSet = nodeData->getNodeInterestSet(); if (nodeInterestSet.size() > 0) { @@ -1038,6 +675,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif // 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(); @@ -1169,11 +807,10 @@ void DomainServer::processRequestAssignmentPacket(QSharedPointer packe 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(), - requestAssignment.getNodeVersion()); - _pendingAssignedNodes.insert(uniqueAssignment.getUUID(), pendingNodeData); + // give the information for that deployed assignment to the gatekeeper so it knows to that that node + // in when it comes back around + _gatekeeper.addPendingAssignedNode(uniqueAssignment.getUUID(), assignmentToDeploy->getUUID(), + requestAssignment.getWalletUUID(), requestAssignment.getNodeVersion()); } else { if (requestAssignment.getType() != Assignment::AgentType || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) { @@ -1253,30 +890,6 @@ void DomainServer::sendPendingTransactionsToServer() { } } -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") { @@ -1373,91 +986,6 @@ 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) { @@ -1569,9 +1097,9 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url 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()); + QUuid assignmentUUID = _gatekeeper.assignmentUUIDForPendingAssignment(matchingUUID); + if (!assignmentUUID.isNull()) { + matchingAssignment = _allAssignments.value(assignmentUUID); if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it @@ -1579,7 +1107,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QUrl scriptURL = url; scriptURL.setPath(URI_ASSIGNMENT + "/scripts/" - + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); + + uuidStringWithoutCurlyBraces(assignmentUUID)); // have the HTTPManager serve the appropriate script file return _httpManager.handleHTTPRequest(connection, scriptURL, true); @@ -2113,7 +1641,7 @@ void DomainServer::nodeAdded(SharedNodePointer node) { void DomainServer::nodeKilled(SharedNodePointer node) { // if this peer connected via ICE then remove them from our ICE peers hash - _icePeers.remove(node->getUUID()); + _gatekeeper.removeICEPeer(node->getUUID()); DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); @@ -2141,7 +1669,7 @@ void DomainServer::nodeKilled(SharedNodePointer node) { } } -SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& assignmentUUID, NodeType_t nodeType) { +SharedAssignmentPointer DomainServer::dequeueMatchingAssignment(const QUuid& assignmentUUID, NodeType_t nodeType) { QQueue::iterator i = _unfulfilledAssignments.begin(); while (i != _unfulfilledAssignments.end()) { @@ -2192,20 +1720,6 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig 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, diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 7495e080de..a7e1a37c10 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -25,6 +25,7 @@ #include #include +#include "DomainGatekeeper.h" #include "DomainServerSettingsManager.h" #include "DomainServerWebSessionData.h" #include "WalletTransaction.h" @@ -50,19 +51,14 @@ public slots: /// Called by NodeList to inform us a node has been killed void nodeKilled(SharedNodePointer node); - void publicKeyJSONCallback(QNetworkReply& requestReply); void transactionJSONCallback(const QJsonObject& data); void restart(); void processRequestAssignmentPacket(QSharedPointer packet); - void processConnectRequestPacket(QSharedPointer packet); void processListRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void processNodeJSONStatsPacket(QSharedPointer packet, SharedNodePointer sendingNode); void processPathQueryPacket(QSharedPointer packet); - void processICEPingPacket(QSharedPointer packet); - void processICEPingReplyPacket(QSharedPointer packet); - void processICEPeerInformationPacket(QSharedPointer packet); private slots: void aboutToQuit(); @@ -74,7 +70,9 @@ private slots: void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); void sendHeartbeatToDataServer() { sendHeartbeatToDataServer(QString()); } void sendHeartbeatToIceServer(); - void handlePeerPingTimeout(); + + void handleConnectedNode(SharedNodePointer newNode); + private: void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); bool optionallySetupOAuth(); @@ -87,20 +85,9 @@ private: void setupAutomaticNetworking(); void sendHeartbeatToDataServer(const QString& networkAddress); - void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); - unsigned int countConnectedUsers(); - bool verifyUserSignature (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn); - bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr, QString& reasonReturn); - void preloadAllowedUserPublicKeys(); - void requestUserPublicKey(const QString& username); - - int parseNodeData(QDataStream& packetStream, NodeType_t& nodeType, - HifiSockAddr& publicSockAddr, HifiSockAddr& localSockAddr, const HifiSockAddr& senderSockAddr); - void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr, - const NodeSet& nodeInterestSet); + void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr); QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB); void broadcastNewNode(const SharedNodePointer& node); @@ -111,12 +98,11 @@ private: void populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes); void populateStaticScriptedAssignmentsFromSettings(); - SharedAssignmentPointer matchingQueuedAssignmentForCheckIn(const QUuid& checkInUUID, NodeType_t nodeType); + SharedAssignmentPointer dequeueMatchingAssignment(const QUuid& checkInUUID, NodeType_t nodeType); SharedAssignmentPointer deployableAssignmentForRequest(const Assignment& requestAssignment); - void removeMatchingAssignmentFromQueue(const SharedAssignmentPointer& removableAssignment); void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment); void addStaticAssignmentsToQueue(); - + QUrl oauthRedirectURL(); QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid()); @@ -131,13 +117,14 @@ private: QJsonObject jsonForSocket(const HifiSockAddr& socket); QJsonObject jsonObjectForNode(const SharedNodePointer& node); + + DomainGatekeeper _gatekeeper; HTTPManager _httpManager; HTTPSManager* _httpsManager; QHash _allAssignments; QQueue _unfulfilledAssignments; - QHash _pendingAssignedNodes; TransactionHash _pendingAssignmentCredits; bool _isUsingDTLS; @@ -149,18 +136,14 @@ private: QSet _webAuthenticationStateSet; QHash _cookieSessionHash; - - QHash _connectionTokenHash; - - QHash _userPublicKeys; - - QHash _icePeers; QString _automaticNetworkingSetting; DomainServerSettingsManager _settingsManager; HifiSockAddr _iceServerSocket; + + friend class DomainGatekeeper; }; diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp new file mode 100644 index 0000000000..3c75e8faad --- /dev/null +++ b/domain-server/src/NodeConnectionData.cpp @@ -0,0 +1,42 @@ +// +// NodeConnectionData.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 "NodeConnectionData.h" + +NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, const HifiSockAddr& senderSockAddr, + bool isConnectRequest) { + NodeConnectionData newHeader; + + if (isConnectRequest) { + dataStream >> newHeader.connectUUID; + } + + dataStream >> newHeader.nodeType + >> newHeader.publicSockAddr >> newHeader.localSockAddr + >> newHeader.interestList; + + newHeader.senderSockAddr = senderSockAddr; + + if (newHeader.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()) { + newHeader.publicSockAddr.setAddress(QHostAddress()); + } else { + newHeader.publicSockAddr.setAddress(senderSockAddr.getAddress()); + } + } + + return newHeader; +} diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h new file mode 100644 index 0000000000..6b3b8eb7c1 --- /dev/null +++ b/domain-server/src/NodeConnectionData.h @@ -0,0 +1,33 @@ +// +// NodeConnectionData.h +// 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 +// + +#pragma once + +#ifndef hifi_NodeConnectionData_h +#define hifi_NodeConnectionData_h + +#include + +class NodeConnectionData { +public: + static NodeConnectionData fromDataStream(QDataStream& dataStream, const HifiSockAddr& senderSockAddr, + bool isConnectRequest = true); + + QUuid connectUUID; + NodeType_t nodeType; + HifiSockAddr publicSockAddr; + HifiSockAddr localSockAddr; + HifiSockAddr senderSockAddr; + QList interestList; +}; + + +#endif // hifi_NodeConnectionData_h diff --git a/domain-server/src/PendingAssignedNodeData.cpp b/domain-server/src/PendingAssignedNodeData.cpp index 30310ac01f..1376e6aa0e 100644 --- a/domain-server/src/PendingAssignedNodeData.cpp +++ b/domain-server/src/PendingAssignedNodeData.cpp @@ -16,5 +16,5 @@ PendingAssignedNodeData::PendingAssignedNodeData(const QUuid& assignmentUUID, co _walletUUID(walletUUID), _nodeVersion(nodeVersion) { - -} \ No newline at end of file + +} diff --git a/domain-server/src/PendingAssignedNodeData.h b/domain-server/src/PendingAssignedNodeData.h index 2d546f573f..eb384a7a93 100644 --- a/domain-server/src/PendingAssignedNodeData.h +++ b/domain-server/src/PendingAssignedNodeData.h @@ -34,4 +34,4 @@ private: QString _nodeVersion; }; -#endif // hifi_PendingAssignedNodeData_h \ No newline at end of file +#endif // hifi_PendingAssignedNodeData_h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9448e942ea..e4081621a6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -189,6 +189,11 @@ static QTimer* billboardPacketTimer = NULL; static QTimer* checkFPStimer = NULL; static QTimer* idleTimer = NULL; +static const unsigned int TARGET_SIM_FRAMERATE = 60; +static const unsigned int THROTTLED_SIM_FRAMERATE = 15; +static const int TARGET_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / TARGET_SIM_FRAMERATE; +static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; + const QString CHECK_VERSION_URL = "https://highfidelity.com/latestVersion.xml"; const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/hifi.skipversion"; @@ -347,7 +352,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _trayIcon(new QSystemTrayIcon(_window)), _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()), - _isVSyncOn(true), _isThrottleFPSEnabled(true), _aboutToQuit(false), _notifiedPacketVersionMismatchThisDomain(false), @@ -745,8 +749,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : }); connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); - - setVSyncEnabled(); // make sure VSync is set properly at startup } void Application::aboutToQuit() { @@ -920,7 +922,7 @@ void Application::initializeGL() { // call our idle function whenever we can idleTimer = new QTimer(this); connect(idleTimer, SIGNAL(timeout()), SLOT(idle())); - idleTimer->start(0); + idleTimer->start(TARGET_SIM_FRAME_PERIOD_MS); _idleLoopStdev.reset(); if (_justStarted) { @@ -2033,38 +2035,38 @@ void Application::checkFPS() { } void Application::idle() { - PROFILE_RANGE(__FUNCTION__); - static SimpleAverage interIdleDurations; - - static uint64_t lastIdleStart{ 0 }; - static uint64_t lastIdleEnd{ 0 }; - uint64_t now = usecTimestampNow(); - uint64_t idleStartToStartDuration = now - lastIdleStart; - - if (lastIdleStart > 0 && idleStartToStartDuration > 0) { - _simsPerSecond.updateAverage((float)USECS_PER_SECOND / (float)idleStartToStartDuration); - } - - lastIdleStart = now; - - if (lastIdleEnd != 0) { - interIdleDurations.update(now - lastIdleEnd); - static uint64_t lastReportTime = now; - if ((now - lastReportTime) >= (USECS_PER_SECOND)) { - static QString LOGLINE("Average inter-idle time: %1 us for %2 samples"); - if (Menu::getInstance()->isOptionChecked(MenuOption::LogExtraTimings)) { - qCDebug(interfaceapp_timing) << LOGLINE.arg((int)interIdleDurations.getAverage()).arg(interIdleDurations.getCount()); - } - interIdleDurations.reset(); - lastReportTime = now; - } - } - - PerformanceTimer perfTimer("idle"); if (_aboutToQuit) { return; // bail early, nothing to do here. } + // depending on whether we're throttling or not. + // Once rendering is off on another thread we should be able to have Application::idle run at start(0) in + // perpetuity and not expect events to get backed up. + bool isThrottled = getActiveDisplayPlugin()->isThrottled(); + // Only run simulation code if more than the targetFramePeriod have passed since last time we ran + // This attempts to lock the simulation at 60 updates per second, regardless of framerate + float timeSinceLastUpdateUs = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_USEC; + float secondsSinceLastUpdate = timeSinceLastUpdateUs / USECS_PER_SECOND; + + if (isThrottled && (timeSinceLastUpdateUs / USECS_PER_MSEC) < THROTTLED_SIM_FRAME_PERIOD_MS) { + return; // bail early, we're throttled and not enough time has elapsed + } + + _lastTimeUpdated.start(); + + + { + PROFILE_RANGE(__FUNCTION__); + uint64_t now = usecTimestampNow(); + static uint64_t lastIdleStart{ now }; + uint64_t idleStartToStartDuration = now - lastIdleStart; + if (idleStartToStartDuration != 0) { + _simsPerSecond.updateAverage((float)USECS_PER_SECOND / (float)idleStartToStartDuration); + } + lastIdleStart = now; + } + + PerformanceTimer perfTimer("idle"); // Drop focus from _keyboardFocusedItem if no keyboard messages for 30 seconds if (!_keyboardFocusedItem.isInvalidID()) { const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus @@ -2080,69 +2082,42 @@ void Application::idle() { bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "idle()"); - // Only run simulation code if more than the targetFramePeriod have passed since last time we ran - double targetFramePeriod = 0.0; - unsigned int targetFramerate = getRenderTargetFramerate(); - if (targetFramerate > 0) { - targetFramePeriod = 1000.0 / targetFramerate; + { + PerformanceTimer perfTimer("update"); + PerformanceWarning warn(showWarnings, "Application::idle()... update()"); + static const float BIGGEST_DELTA_TIME_SECS = 0.25f; + update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); } - double timeSinceLastUpdate = (double)_lastTimeUpdated.nsecsElapsed() / 1000000.0; - if (timeSinceLastUpdate > targetFramePeriod) { - _lastTimeUpdated.start(); - { - PerformanceTimer perfTimer("update"); - PerformanceWarning warn(showWarnings, "Application::idle()... update()"); - const float BIGGEST_DELTA_TIME_SECS = 0.25f; - PROFILE_RANGE(__FUNCTION__ "/idleUpdate"); - update(glm::clamp((float)timeSinceLastUpdate / 1000.0f, 0.0f, BIGGEST_DELTA_TIME_SECS)); - } - { - PerformanceTimer perfTimer("updateGL"); - PerformanceWarning warn(showWarnings, "Application::idle()... updateGL()"); - getActiveDisplayPlugin()->idle(); - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - QString name = inputPlugin->getName(); - QAction* action = Menu::getInstance()->getActionForOption(name); - if (action && action->isChecked()) { - inputPlugin->idle(); - } + { + PerformanceTimer perfTimer("pluginIdle"); + PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); + getActiveDisplayPlugin()->idle(); + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + QString name = inputPlugin->getName(); + QAction* action = Menu::getInstance()->getActionForOption(name); + if (action && action->isChecked()) { + inputPlugin->idle(); } } - { - PerformanceTimer perfTimer("rest"); - PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); - _idleLoopStdev.addValue(timeSinceLastUpdate); + } + { + PerformanceTimer perfTimer("rest"); + PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); + _idleLoopStdev.addValue(secondsSinceLastUpdate); - // Record standard deviation and reset counter if needed - const int STDEV_SAMPLES = 500; - if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { - _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); - _idleLoopStdev.reset(); - } - } - - float secondsSinceLastUpdate = (float)timeSinceLastUpdate / 1000.0f; - _overlayConductor.update(secondsSinceLastUpdate); - - // depending on whether we're throttling or not. - // Once rendering is off on another thread we should be able to have Application::idle run at start(0) in - // perpetuity and not expect events to get backed up. - - bool isThrottled = getActiveDisplayPlugin()->isThrottled(); - static const int THROTTLED_IDLE_TIMER_DELAY = MSECS_PER_SECOND / 15; - static const int IDLE_TIMER_DELAY_MS = 2; - int desiredInterval = isThrottled ? THROTTLED_IDLE_TIMER_DELAY : IDLE_TIMER_DELAY_MS; - //qDebug() << "isThrottled:" << isThrottled << "desiredInterval:" << desiredInterval; - - if (idleTimer->interval() != desiredInterval) { - idleTimer->start(desiredInterval); + // Record standard deviation and reset counter if needed + const int STDEV_SAMPLES = 500; + if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { + _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); + _idleLoopStdev.reset(); } } + + _overlayConductor.update(secondsSinceLastUpdate); // check for any requested background downloads. emit checkBackgroundDownloads(); - lastIdleEnd = usecTimestampNow(); } float Application::getAverageSimsPerSecond() { @@ -4477,90 +4452,10 @@ void Application::takeSnapshot() { } -void Application::setVSyncEnabled() { - _glWidget->makeCurrent(); -#if defined(Q_OS_WIN) - bool vsyncOn = Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerateVSyncOn); - if (wglewGetExtension("WGL_EXT_swap_control")) { - wglSwapIntervalEXT(vsyncOn); - int swapInterval = wglGetSwapIntervalEXT(); - qCDebug(interfaceapp, "V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); - } else { - qCDebug(interfaceapp, "V-Sync is FORCED ON on this system\n"); - } -#elif defined(Q_OS_LINUX) - // TODO: write the poper code for linux - /* - if (glQueryExtension.... ("GLX_EXT_swap_control")) { - glxSwapIntervalEXT(vsyncOn); - int swapInterval = xglGetSwapIntervalEXT(); - _isVSyncOn = swapInterval; - qCDebug(interfaceapp, "V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); - } else { - qCDebug(interfaceapp, "V-Sync is FORCED ON on this system\n"); - } - */ -#else - qCDebug(interfaceapp, "V-Sync is FORCED ON on this system\n"); -#endif - _offscreenContext->makeCurrent(); -} - void Application::setThrottleFPSEnabled() { _isThrottleFPSEnabled = Menu::getInstance()->isOptionChecked(MenuOption::ThrottleFPSIfNotFocus); } -bool Application::isVSyncOn() const { -#if defined(Q_OS_WIN) - if (wglewGetExtension("WGL_EXT_swap_control")) { - int swapInterval = wglGetSwapIntervalEXT(); - return (swapInterval > 0); - } -#elif defined(Q_OS_LINUX) - // TODO: write the poper code for linux - /* - if (glQueryExtension.... ("GLX_EXT_swap_control")) { - int swapInterval = xglGetSwapIntervalEXT(); - return (swapInterval > 0); - } else { - return true; - } - */ -#endif - return true; -} - -bool Application::isVSyncEditable() const { -#if defined(Q_OS_WIN) - if (wglewGetExtension("WGL_EXT_swap_control")) { - return true; - } -#elif defined(Q_OS_LINUX) - // TODO: write the poper code for linux - /* - if (glQueryExtension.... ("GLX_EXT_swap_control")) { - return true; - } - */ -#endif - return false; -} - -unsigned int Application::getRenderTargetFramerate() const { - if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerateUnlimited)) { - return 0; - } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate60)) { - return 60; - } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate50)) { - return 50; - } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate40)) { - return 40; - } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate30)) { - return 30; - } - return 0; -} - float Application::getRenderResolutionScale() const { if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionOne)) { return 1.0f; diff --git a/interface/src/Application.h b/interface/src/Application.h index 6394aa12d0..7e4e340eee 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -300,9 +300,6 @@ public: float getRenderResolutionScale() const; int getRenderAmbientLight() const; - unsigned int getRenderTargetFramerate() const; - bool isVSyncOn() const; - bool isVSyncEditable() const; bool isAboutToQuit() const { return _aboutToQuit; } // the isHMDmode is true whenever we use the interface from an HMD and not a standard flat display @@ -411,8 +408,6 @@ public slots: void domainSettingsReceived(const QJsonObject& domainSettingsObject); - void setVSyncEnabled(); - void setThrottleFPSEnabled(); bool isThrottleFPSEnabled() { return _isThrottleFPSEnabled; } @@ -626,7 +621,6 @@ private: quint64 _lastNackTime; quint64 _lastSendDownstreamAudioStats; - bool _isVSyncOn; bool _isThrottleFPSEnabled; bool _aboutToQuit; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9d47df1c73..b756236fe0 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -222,7 +222,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(toolsMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel())); - MenuWrapper* displayMenu = addMenu(DisplayPlugin::MENU_PATH); + MenuWrapper* displayMenu = addMenu(DisplayPlugin::MENU_PATH()); { MenuWrapper* displayModeMenu = addMenu(MenuOption::OutputMenu); QActionGroup* displayModeGroup = new QActionGroup(displayModeMenu); @@ -333,25 +333,8 @@ Menu::Menu() { ambientLightGroup->addAction(addCheckableActionToQMenuAndActionHash(ambientLightMenu, MenuOption::RenderAmbientLight8, 0, false)); ambientLightGroup->addAction(addCheckableActionToQMenuAndActionHash(ambientLightMenu, MenuOption::RenderAmbientLight9, 0, false)); - { - MenuWrapper* framerateMenu = renderOptionsMenu->addMenu(MenuOption::RenderTargetFramerate); - QActionGroup* framerateGroup = new QActionGroup(framerateMenu); - framerateGroup->setExclusive(true); - framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerateUnlimited, 0, true)); - framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate60, 0, false)); - framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate50, 0, false)); - framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate40, 0, false)); - framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate30, 0, false)); - -#if defined(Q_OS_MAC) -#else - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::RenderTargetFramerateVSyncOn, 0, true, - qApp, SLOT(setVSyncEnabled())); -#endif - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true, + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true, qApp, SLOT(setThrottleFPSEnabled())); - } - MenuWrapper* resolutionMenu = renderOptionsMenu->addMenu(MenuOption::RenderResolution); QActionGroup* resolutionGroup = new QActionGroup(resolutionMenu); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 4bd1e7f664..98b3fe9892 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -238,13 +238,6 @@ namespace MenuOption { const QString RenderLookAtTargets = "Show Look-at Targets"; const QString RenderLookAtVectors = "Show Look-at Vectors"; const QString RenderSkeletonCollisionShapes = "Show Skeleton Collision Shapes"; - const QString RenderTargetFramerate = "Framerate"; - const QString RenderTargetFramerateUnlimited = "Unlimited"; - const QString RenderTargetFramerate60 = "60"; - const QString RenderTargetFramerate50 = "50"; - const QString RenderTargetFramerate40 = "40"; - const QString RenderTargetFramerate30 = "30"; - const QString RenderTargetFramerateVSyncOn = "V-Sync On"; const QString RenderResolution = "Scale Resolution"; const QString RenderResolutionOne = "1"; const QString RenderResolutionTwoThird = "2/3"; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index bd50215de6..752fb55ce6 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -131,13 +131,20 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // // Thus this should really only be ... else if (_owningAvatar->getHead()->isLookingAtMe()) {... // However, in the !isLookingAtMe case, the eyes aren't rotating the way they should right now. - // (They latch their looking at me position.) We will revisit that as priorities allow. + // We will revisit that as priorities allow, and particularly after the new rig/animation/joints. const FBXGeometry& geometry = _geometry->getFBXGeometry(); Head* head = _owningAvatar->getHead(); - _rig->updateEyeJoints(geometry.leftEyeJointIndex, geometry.rightEyeJointIndex, - getTranslation(), getRotation(), - head->getFinalOrientationInWorldFrame(), head->getCorrectedLookAtPosition()); - } + // If the head is not positioned, updateEyeJoints won't get the math right + glm::quat headOrientation; + _rig->getJointRotation(geometry.headJointIndex, headOrientation); + glm::vec3 eulers = safeEulerAngles(headOrientation); + head->setBasePitch(glm::degrees(-eulers.x)); + head->setBaseYaw(glm::degrees(eulers.y)); + head->setBaseRoll(glm::degrees(-eulers.z)); + _rig->updateEyeJoints(geometry.leftEyeJointIndex, geometry.rightEyeJointIndex, + getTranslation(), getRotation(), + head->getFinalOrientationInWorldFrame(), head->getCorrectedLookAtPosition()); + } } // Called by Avatar::simulate after it has set the joint states (fullUpdate true if changed), diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index da8deefa74..914d30d58a 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -7,21 +7,35 @@ // #include "Basic2DWindowOpenGLDisplayPlugin.h" +#include + +#include +#include +#include + #include -#include -#include const QString Basic2DWindowOpenGLDisplayPlugin::NAME("2D Display"); static const QString FULLSCREEN = "Fullscreen"; +static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; +static const QString FRAMERATE_UNLIMITED = "Unlimited"; +static const QString FRAMERATE_60 = "60"; +static const QString FRAMERATE_50 = "50"; +static const QString FRAMERATE_40 = "40"; +static const QString FRAMERATE_30 = "30"; +static const QString VSYNC_ON = "V-Sync On"; const QString& Basic2DWindowOpenGLDisplayPlugin::getName() const { return NAME; } +std::vector _framerateActions; +QAction* _vsyncAction{ nullptr }; + void Basic2DWindowOpenGLDisplayPlugin::activate() { - CONTAINER->addMenu(MENU_PATH); - CONTAINER->addMenuItem(MENU_PATH, FULLSCREEN, + _framerateActions.clear(); + CONTAINER->addMenuItem(MENU_PATH(), FULLSCREEN, [this](bool clicked) { if (clicked) { CONTAINER->setFullscreen(getFullscreenTarget()); @@ -29,18 +43,65 @@ void Basic2DWindowOpenGLDisplayPlugin::activate() { CONTAINER->unsetFullscreen(); } }, true, false); + CONTAINER->addMenu(FRAMERATE); + _framerateActions.push_back( + CONTAINER->addMenuItem(FRAMERATE, FRAMERATE_UNLIMITED, + [this](bool) { updateFramerate(); }, true, true, FRAMERATE)); + _framerateActions.push_back( + CONTAINER->addMenuItem(FRAMERATE, FRAMERATE_60, + [this](bool) { updateFramerate(); }, true, false, FRAMERATE)); + _framerateActions.push_back( + CONTAINER->addMenuItem(FRAMERATE, FRAMERATE_50, + [this](bool) { updateFramerate(); }, true, false, FRAMERATE)); + _framerateActions.push_back( + CONTAINER->addMenuItem(FRAMERATE, FRAMERATE_40, + [this](bool) { updateFramerate(); }, true, false, FRAMERATE)); + _framerateActions.push_back( + CONTAINER->addMenuItem(FRAMERATE, FRAMERATE_30, + [this](bool) { updateFramerate(); }, true, false, FRAMERATE)); + WindowOpenGLDisplayPlugin::activate(); + + // Vsync detection happens in the parent class activate, so we need to check after that + if (_vsyncSupported) { + _vsyncAction = CONTAINER->addMenuItem(MENU_PATH(), VSYNC_ON, [this](bool) {}, true, true); + } else { + _vsyncAction = nullptr; + } + + updateFramerate(); } void Basic2DWindowOpenGLDisplayPlugin::deactivate() { WindowOpenGLDisplayPlugin::deactivate(); } -int Basic2DWindowOpenGLDisplayPlugin::getDesiredInterval(bool isThrottled) const { - static const int THROTTLED_PAINT_TIMER_DELAY = MSECS_PER_SECOND / 15; - static const int PAINT_TIMER_DELAY_MS = 1; +void Basic2DWindowOpenGLDisplayPlugin::display(GLuint sceneTexture, const glm::uvec2& sceneSize) { + if (_vsyncAction) { + bool wantVsync = _vsyncAction->isChecked(); + bool vsyncEnabed = isVsyncEnabled(); + if (vsyncEnabed ^ wantVsync) { + enableVsync(wantVsync); + } + } - return isThrottled ? THROTTLED_PAINT_TIMER_DELAY : PAINT_TIMER_DELAY_MS; + WindowOpenGLDisplayPlugin::display(sceneTexture, sceneSize); +} + + +int Basic2DWindowOpenGLDisplayPlugin::getDesiredInterval() const { + static const int THROTTLED_PAINT_TIMER_DELAY_MS = MSECS_PER_SECOND / 15; + static const int ULIMIITED_PAINT_TIMER_DELAY_MS = 1; + int result = ULIMIITED_PAINT_TIMER_DELAY_MS; + if (_isThrottled) { + result = THROTTLED_PAINT_TIMER_DELAY_MS; + } + if (0 != _framerateTarget) { + result = MSECS_PER_SECOND / _framerateTarget; + } + + qDebug() << "New interval " << result; + return result; } bool Basic2DWindowOpenGLDisplayPlugin::isThrottled() const { @@ -49,14 +110,42 @@ bool Basic2DWindowOpenGLDisplayPlugin::isThrottled() const { bool shouldThrottle = (!CONTAINER->isForeground() && CONTAINER->isOptionChecked(ThrottleFPSIfNotFocus)); if (_isThrottled != shouldThrottle) { - int desiredInterval = getDesiredInterval(shouldThrottle); - _timer.start(desiredInterval); _isThrottled = shouldThrottle; + _timer.start(getDesiredInterval()); } return shouldThrottle; } + +void Basic2DWindowOpenGLDisplayPlugin::updateFramerate() { + QAction* checkedFramerate{ nullptr }; + foreach(auto action, _framerateActions) { + if (action->isChecked()) { + checkedFramerate = action; + break; + } + } + + _framerateTarget = 0; + if (checkedFramerate) { + QString actionText = checkedFramerate->text(); + if (FRAMERATE_60 == actionText) { + _framerateTarget = 60; + } else if (FRAMERATE_50 == actionText) { + _framerateTarget = 50; + } else if (FRAMERATE_40 == actionText) { + _framerateTarget = 40; + } else if (FRAMERATE_30 == actionText) { + _framerateTarget = 30; + } + } + + int newInterval = getDesiredInterval(); + qDebug() << newInterval; + _timer.start(getDesiredInterval()); +} + // FIXME target the screen the window is currently on QScreen* Basic2DWindowOpenGLDisplayPlugin::getFullscreenTarget() { return qApp->primaryScreen(); diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index 64edfe3600..f4655ab79f 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -14,19 +14,23 @@ class Basic2DWindowOpenGLDisplayPlugin : public WindowOpenGLDisplayPlugin { Q_OBJECT public: + virtual const QString & getName() const override; + virtual void activate() override; virtual void deactivate() override; - virtual const QString & getName() const override; + virtual void display(GLuint sceneTexture, const glm::uvec2& sceneSize) override; virtual bool isThrottled() const override; protected: - int getDesiredInterval(bool isThrottled) const; + int getDesiredInterval() const; mutable bool _isThrottled = false; private: + void updateFramerate(); static const QString NAME; QScreen* getFullscreenTarget(); + uint32_t _framerateTarget{ 0 }; int _fullscreenTarget{ -1 }; }; diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp index 5840b3cbba..598e78e500 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp @@ -18,7 +18,10 @@ #include "oculus/OculusDisplayPlugin.h" #include "oculus/OculusLegacyDisplayPlugin.h" -const QString DisplayPlugin::MENU_PATH{ "Display" }; +const QString& DisplayPlugin::MENU_PATH() { + static const QString value = "Display"; + return value; +} // TODO migrate to a DLL model where plugins are discovered and loaded at runtime by the PluginManager class DisplayPluginList getDisplayPlugins() { diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.h b/libraries/display-plugins/src/display-plugins/DisplayPlugin.h index 0696cc6229..a9220d68f6 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.h @@ -120,7 +120,7 @@ public: virtual void resetSensors() {} virtual float devicePixelRatio() { return 1.0; } - static const QString MENU_PATH; + static const QString& MENU_PATH(); signals: void recommendedFramebufferSizeChanged(const QSize & size); void requestRender(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 0409899739..eb38e1bf4f 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -38,6 +38,11 @@ void OpenGLDisplayPlugin::finishFrame() { void OpenGLDisplayPlugin::customizeContext() { using namespace oglplus; + // TODO: write the poper code for linux +#if defined(Q_OS_WIN) + _vsyncSupported = wglewGetExtension("WGL_EXT_swap_control"); +#endif + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); Context::Disable(Capability::Blend); Context::Disable(Capability::DepthTest); @@ -46,6 +51,8 @@ void OpenGLDisplayPlugin::customizeContext() { _program = loadDefaultShader(); _plane = loadPlane(_program); + + enableVsync(); } void OpenGLDisplayPlugin::activate() { @@ -114,4 +121,24 @@ void OpenGLDisplayPlugin::display( void OpenGLDisplayPlugin::drawUnitQuad() { _program->Bind(); _plane->Draw(); +} + +void OpenGLDisplayPlugin::enableVsync(bool enable) { + if (!_vsyncSupported) { + return; + } +#ifdef Q_OS_WIN + wglSwapIntervalEXT(enable ? 1 : 0); +#endif +} + +bool OpenGLDisplayPlugin::isVsyncEnabled() { + if (!_vsyncSupported) { + return true; + } +#ifdef Q_OS_WIN + return wglGetSwapIntervalEXT() != 0; +#else + return true; +#endif } \ No newline at end of file diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 3152500232..0dc94b72f5 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -38,9 +38,13 @@ protected: virtual void doneCurrent() = 0; virtual void swapBuffers() = 0; + virtual bool isVsyncEnabled(); + virtual void enableVsync(bool enable = true); + mutable QTimer _timer; ProgramPtr _program; ShapeWrapperPtr _plane; + bool _vsyncSupported{ false }; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index df691f06f3..017977bf69 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -69,7 +69,7 @@ void StereoDisplayPlugin::activate() { if (screen == qApp->primaryScreen()) { checked = true; } - auto action = CONTAINER->addMenuItem(MENU_PATH, name, + auto action = CONTAINER->addMenuItem(MENU_PATH(), name, [this](bool clicked) { updateScreen(); }, true, checked, "Screens"); _screenActions[i] = action; } diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 841fdcfad9..3eff3bdec5 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -211,13 +211,17 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { while (true) { switch (tokenizer.nextToken()) { case OBJTokenizer::COMMENT_TOKEN: + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader MTLLIB comment:" << tokenizer.getComment(); + #endif break; case OBJTokenizer::DATUM_TOKEN: break; default: materials[matName] = currentMaterial; + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader Last material shininess:" << currentMaterial.shininess << " opacity:" << currentMaterial.opacity << " diffuse color:" << currentMaterial.diffuseColor << " specular color:" << currentMaterial.specularColor << " diffuse texture:" << currentMaterial.diffuseTextureFilename << " specular texture:" << currentMaterial.specularTextureFilename; + #endif return; } QByteArray token = tokenizer.getDatum(); @@ -229,14 +233,18 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { matName = tokenizer.getDatum(); currentMaterial = materials[matName]; currentMaterial.diffuseTextureFilename = "test"; + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader Starting new material definition " << matName; + #endif currentMaterial.diffuseTextureFilename = ""; } else if (token == "Ns") { currentMaterial.shininess = tokenizer.getFloat(); } else if ((token == "d") || (token == "Tr")) { currentMaterial.opacity = tokenizer.getFloat(); } else if (token == "Ka") { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader Ignoring material Ka " << tokenizer.getVec3(); + #endif } else if (token == "Kd") { currentMaterial.diffuseColor = tokenizer.getVec3(); } else if (token == "Ks") { @@ -244,7 +252,9 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } else if ((token == "map_Kd") || (token == "map_Ks")) { QByteArray filename = QUrl(tokenizer.getLineAsDatum()).fileName().toUtf8(); if (filename.endsWith(".tga")) { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << _url; + #endif break; } if (isValidTexture(filename)) { @@ -254,7 +264,9 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { currentMaterial.specularTextureFilename = filename; } } else { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader WARNING: " << _url << " ignoring missing texture " << filename; + #endif } } } @@ -318,7 +330,6 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi } QByteArray groupName = tokenizer.getDatum(); currentGroup = groupName; - //qCDebug(modelformat) << "new group:" << groupName; } else if (token == "mtllib" && _url) { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { break; @@ -330,13 +341,17 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi librariesSeen[libraryName] = true; // Throw away any path part of libraryName, and merge against original url. QUrl libraryUrl = _url->resolved(QUrl(libraryName).fileName()); + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader new library:" << libraryName << " at:" << libraryUrl; + #endif QNetworkReply* netReply = request(libraryUrl, false); if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) { parseMaterialLibrary(netReply); } else { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got " << netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + #endif } netReply->deleteLater(); } else if (token == "usemtl") { @@ -344,7 +359,9 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi break; } currentMaterialName = tokenizer.getDatum(); + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader new current material:" << currentMaterialName; + #endif } else if (token == "v") { vertices.append(tokenizer.getVec3()); } else if (token == "vn") { @@ -394,7 +411,6 @@ done: } else { faceGroups.append(faces); // We're done with this group. Add the faces. } - //qCDebug(modelformat) << "end group:" << meshPart.materialID << " original faces:" << originalFaceCountForDebugging << " triangles:" << faces.count() << " keep going:" << result; return result; } @@ -475,13 +491,17 @@ FBXGeometry* OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, OBJFace leadFace = faceGroup[0]; // All the faces in the same group will have the same name and material. QString groupMaterialName = leadFace.materialName; if (groupMaterialName.isEmpty() && (leadFace.textureUVIndices.count() > 0)) { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " needs a texture that isn't specified. Using default mechanism."; + #endif groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; } else if (!groupMaterialName.isEmpty() && !materials.contains(groupMaterialName)) { + #ifdef WANT_DEBUG qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " specifies a material " << groupMaterialName << " that is not defined. Using default mechanism."; + #endif groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; } if (!groupMaterialName.isEmpty()) { @@ -496,7 +516,6 @@ FBXGeometry* OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, meshPart._material->setGloss(material->shininess); meshPart._material->setOpacity(material->opacity); } - // qCDebug(modelformat) << "OBJ Reader part:" << meshPartCount << "name:" << leadFace.groupName << "material:" << groupMaterialName << "diffuse:" << meshPart._material->getDiffuse() << "faces:" << faceGroup.count() << "triangle indices will start with:" << mesh.vertices.count(); foreach(OBJFace face, faceGroup) { glm::vec3 v0 = vertices[face.vertexIndices[0]]; glm::vec3 v1 = vertices[face.vertexIndices[1]]; diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 33d490c960..88a3ccf8ff 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -138,7 +138,7 @@ public: SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool canAdjustLocks, bool canRez, + bool canAdjustLocks = false, bool canRez = false, const QUuid& connectionSecret = QUuid()); bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } diff --git a/libraries/networking/src/UUIDHasher.h b/libraries/networking/src/UUIDHasher.h index 5429e151c7..0bdfe9792a 100644 --- a/libraries/networking/src/UUIDHasher.h +++ b/libraries/networking/src/UUIDHasher.h @@ -20,10 +20,17 @@ class UUIDHasher { public: size_t operator()(const QUuid& uuid) const { - return uuid.data1 ^ uuid.data2 ^ (uuid.data3 << 16) - ^ ((uuid.data4[0] << 24) | (uuid.data4[1] << 16) | (uuid.data4[2] << 8) | uuid.data4[3]) - ^ ((uuid.data4[4] << 24) | (uuid.data4[5] << 16) | (uuid.data4[6] << 8) | uuid.data4[7]); + return qHash(uuid); } }; -#endif // hifi_UUIDHasher_h \ No newline at end of file +namespace std { + template <> struct hash { + size_t operator()(const QUuid& uuid) const { + return qHash(uuid); + } + }; +} + + +#endif // hifi_UUIDHasher_h diff --git a/libraries/octree/src/OctreeEditPacketSender.h b/libraries/octree/src/OctreeEditPacketSender.h index 8c77a1e388..65d5315206 100644 --- a/libraries/octree/src/OctreeEditPacketSender.h +++ b/libraries/octree/src/OctreeEditPacketSender.h @@ -18,14 +18,6 @@ #include "JurisdictionMap.h" #include "SentPacketHistory.h" -namespace std { - template <> struct hash { - size_t operator()(const QUuid& uuid) const { - return qHash(uuid); - } - }; -} - /// Utility for processing, packing, queueing and sending of outbound edit messages. class OctreeEditPacketSender : public PacketSender { Q_OBJECT diff --git a/libraries/shared/src/NumericalConstants.h b/libraries/shared/src/NumericalConstants.h index b632147641..9a946e35f7 100644 --- a/libraries/shared/src/NumericalConstants.h +++ b/libraries/shared/src/NumericalConstants.h @@ -31,6 +31,7 @@ const float METERS_PER_DECIMETER = 0.1f; const float METERS_PER_CENTIMETER = 0.01f; const float METERS_PER_MILLIMETER = 0.001f; const float MILLIMETERS_PER_METER = 1000.0f; +const quint64 NSECS_PER_USEC = 1000; const quint64 USECS_PER_MSEC = 1000; const quint64 MSECS_PER_SECOND = 1000; const quint64 USECS_PER_SECOND = USECS_PER_MSEC * MSECS_PER_SECOND;