diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c9d7ea77d5..84408d2e9a 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -380,6 +380,14 @@ "default": "0", "advanced": false }, + { + "name": "maximum_user_capacity_redirect_location", + "label": "Redirect to Location on Maximum Capacity", + "help": "Is there another domain, you'd like to redirect clients to when the maximum number of avatars are connected.", + "placeholder": "", + "default": "", + "advanced": false + }, { "name": "standard_permissions", "type": "table", diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 23a53c3eb0..e33cbe1755 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -317,6 +317,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; +const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, @@ -363,7 +364,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers); + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorized); #ifdef WANT_DEBUG qDebug() << "stalling login due to permissions:" << username; #endif @@ -372,8 +373,16 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) { // we can't allow this user to connect because we are at max capacity + QString redirectOnMaxCapacity; + const QVariant* redirectOnMaxCapacityVariant = + valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); + if (redirectOnMaxCapacityVariant && redirectOnMaxCapacityVariant->canConvert()) { + redirectOnMaxCapacity = redirectOnMaxCapacityVariant->toString(); + qDebug() << "Redirection domain:" << redirectOnMaxCapacity; + } + sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr, - DomainHandler::ConnectionRefusedReason::TooManyUsers); + DomainHandler::ConnectionRefusedReason::TooManyUsers, redirectOnMaxCapacity); #ifdef WANT_DEBUG qDebug() << "stalling login due to max capacity:" << username; #endif @@ -623,22 +632,30 @@ void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& } void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, - DomainHandler::ConnectionRefusedReason reasonCode) { + DomainHandler::ConnectionRefusedReason reasonCode, + QString extraInfo) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection - QByteArray utfString = reason.toUtf8(); - quint16 payloadSize = utfString.size(); + QByteArray utfReasonString = reason.toUtf8(); + quint16 reasonSize = utfReasonString.size(); + + QByteArray utfExtraInfo = extraInfo.toUtf8(); + quint16 extraInfoSize = utfExtraInfo.size(); // setup the DomainConnectionDenied packet auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, - payloadSize + sizeof(payloadSize) + sizeof(uint8_t)); + sizeof(uint8_t) + // reasonCode + reasonSize + sizeof(reasonSize) + + extraInfoSize + sizeof(extraInfoSize)); // pack in the reason the connection was denied (the client displays this) - if (payloadSize > 0) { - uint8_t reasonCodeWire = (uint8_t)reasonCode; - connectionDeniedPacket->writePrimitive(reasonCodeWire); - connectionDeniedPacket->writePrimitive(payloadSize); - connectionDeniedPacket->write(utfString); - } + uint8_t reasonCodeWire = (uint8_t)reasonCode; + connectionDeniedPacket->writePrimitive(reasonCodeWire); + connectionDeniedPacket->writePrimitive(reasonSize); + connectionDeniedPacket->write(utfReasonString); + + // write the extra info as well + connectionDeniedPacket->writePrimitive(extraInfoSize); + connectionDeniedPacket->write(utfExtraInfo); // send the packet off DependencyManager::get()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 06ecfcf285..b7d2a03af6 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -88,7 +88,8 @@ private: void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr); static void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, - DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); + DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown, + QString extraInfo = QString()); void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b18451a833..99bbffa750 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -66,8 +67,11 @@ DomainServer::DomainServer(int argc, char* argv[]) : _webAuthenticationStateSet(), _cookieSessionHash(), _automaticNetworkingSetting(), - _settingsManager() + _settingsManager(), + _iceServerAddr(ICE_SERVER_DEFAULT_HOSTNAME), + _iceServerPort(ICE_SERVER_DEFAULT_PORT) { + parseCommandLine(); qInstallMessageHandler(LogHandler::verboseMessageHandler); LogUtils::init(); @@ -159,6 +163,46 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "domain-server is running"; } +void DomainServer::parseCommandLine() { + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity Domain Server"); + parser.addHelpOption(); + + const QCommandLineOption iceServerAddressOption("i", "ice-server address", "IP:PORT or HOSTNAME:PORT"); + parser.addOption(iceServerAddressOption); + + const QCommandLineOption domainIDOption("d", "domain-server uuid"); + parser.addOption(domainIDOption); + + if (!parser.parse(QCoreApplication::arguments())) { + qWarning() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(iceServerAddressOption)) { + // parse the IP and port combination for this target + QString hostnamePortString = parser.value(iceServerAddressOption); + + _iceServerAddr = hostnamePortString.left(hostnamePortString.indexOf(':')); + _iceServerPort = (quint16) hostnamePortString.mid(hostnamePortString.indexOf(':') + 1).toUInt(); + if (_iceServerPort == 0) { + _iceServerPort = ICE_SERVER_DEFAULT_PORT; + } + + if (_iceServerAddr.isEmpty()) { + qWarning() << "Could not parse an IP address and port combination from" << hostnamePortString; + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + } + } + + if (parser.isSet(domainIDOption)) { + _overridingDomainID = QUuid(parser.value(domainIDOption)); + _overrideDomainID = true; + qDebug() << "domain-server ID is" << _overridingDomainID; + } +} + DomainServer::~DomainServer() { // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); @@ -166,7 +210,7 @@ DomainServer::~DomainServer() { void DomainServer::queuedQuit(QString quitMessage, int exitCode) { if (!quitMessage.isEmpty()) { - qCritical() << qPrintable(quitMessage); + qWarning() << qPrintable(quitMessage); } QCoreApplication::exit(exitCode); @@ -307,7 +351,7 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { auto domainObject = jsonObject[DATA_KEY].toObject()[DOMAIN_KEY].toObject(); if (!domainObject.isEmpty()) { - auto id = domainObject[ID_KEY].toString(); + auto id = _overrideDomainID ? _overridingDomainID.toString() : domainObject[ID_KEY].toString(); auto name = domainObject[NAME_KEY].toString(); auto key = domainObject[KEY_KEY].toString(); @@ -415,24 +459,30 @@ void DomainServer::setupNodeListAndAssignments() { quint16 localHttpsPort = DOMAIN_SERVER_HTTPS_PORT; nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_HTTPS_PORT_SMEM_KEY, this, localHttpsPort); - // set our LimitedNodeList UUID to match the UUID from our config // nodes will currently use this to add resources to data-web that relate to our domain - const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - nodeList->setSessionUUID(idValueVariant->toString()); + bool isMetaverseDomain = false; + if (_overrideDomainID) { + nodeList->setSessionUUID(_overridingDomainID); + isMetaverseDomain = true; // assume metaverse domain + } else { + const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); + if (idValueVariant) { + nodeList->setSessionUUID(idValueVariant->toString()); + isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain + } else { + nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID + } + } - // if we have an ID, we'll assume we're a metaverse domain - // now see if we think we're a temp domain (we have an API key) or a full domain + if (isMetaverseDomain) { + // see if we think we're a temp domain (we have an API key) or a full domain const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); if (temporaryDomainKey.isEmpty()) { _type = MetaverseDomain; } else { _type = MetaverseTemporaryDomain; } - - } else { - nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); @@ -548,7 +598,6 @@ void DomainServer::setupAutomaticNetworking() { } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." << "Please add an ID to your config file or via the web interface."; - return; } } @@ -606,12 +655,11 @@ void DomainServer::setupICEHeartbeatForFullNetworking() { void DomainServer::updateICEServerAddresses() { if (_iceAddressLookupID == -1) { - _iceAddressLookupID = QHostInfo::lookupHost(ICE_SERVER_DEFAULT_HOSTNAME, this, SLOT(handleICEHostInfo(QHostInfo))); + _iceAddressLookupID = QHostInfo::lookupHost(_iceServerAddr, this, SLOT(handleICEHostInfo(QHostInfo))); } } void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) { - // check for configs from the command line, these take precedence const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index e30f4515cc..066f2be0d1 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -105,6 +105,7 @@ signals: private: const QUuid& getID(); + void parseCommandLine(); void setupNodeListAndAssignments(); bool optionallySetupOAuth(); @@ -205,6 +206,11 @@ private: friend class DomainGatekeeper; friend class DomainMetadata; + + QString _iceServerAddr; + int _iceServerPort; + bool _overrideDomainID { false }; // should we override the domain-id from settings? + QUuid _overridingDomainID { QUuid() }; // what should we override it with? }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8a37662ca9..97a10ea232 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1239,8 +1239,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : firstRun.set(false); } -void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { - switch (static_cast(reasonCode)) { +void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); + + if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { + DependencyManager::get()->handleLookupString(extraInfo); + return; + } + + switch (reasonCode) { case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: case DomainHandler::ConnectionRefusedReason::TooManyUsers: case DomainHandler::ConnectionRefusedReason::Unknown: { diff --git a/interface/src/Application.h b/interface/src/Application.h index 02682defca..4c52ff8526 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -375,7 +375,7 @@ private slots: void nodeKilled(SharedNodePointer node); static void packetSent(quint64 length); void updateDisplayMode(); - void domainConnectionRefused(const QString& reasonMessage, int reason); + void domainConnectionRefused(const QString& reasonMessage, int reason, const QString& extraInfo); private: static void initDisplay(); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 715d0657a3..9303636a1f 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -58,7 +58,7 @@ public slots: signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); - void domainConnectionRefused(const QString& reasonMessage, int reasonCode); + void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); void snapshotTaken(const QString& path, bool notify); void snapshotShared(const QString& error); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 2a0094de29..ab9cc45740 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -278,12 +278,18 @@ void setupPreferences() { preferences->addPreference(preference); } #if DEV_BUILD || PR_BUILD + { + auto getter = []()->bool { return DependencyManager::get()->isSimulatingJitter(); }; + auto setter = [](bool value) { return DependencyManager::get()->setIsSimulatingJitter(value); }; + auto preference = new CheckPreference(AUDIO, "Packet jitter simulator", getter, setter); + preferences->addPreference(preference); + } { auto getter = []()->float { return DependencyManager::get()->getGateThreshold(); }; auto setter = [](float value) { return DependencyManager::get()->setGateThreshold(value); }; - auto preference = new SpinnerPreference(AUDIO, "Debug gate threshold", getter, setter); + auto preference = new SpinnerPreference(AUDIO, "Packet throttle threshold", getter, setter); preference->setMin(1); - preference->setMax((float)100); + preference->setMax(200); preference->setStep(1); preferences->addPreference(preference); } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index a37a208072..b03672063f 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -56,8 +56,6 @@ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 100; static const auto DEFAULT_POSITION_GETTER = []{ return Vectors::ZERO; }; static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY; }; -static const int DEFAULT_AUDIO_OUTPUT_GATE_THRESHOLD = 1; - Setting::Handle dynamicJitterBuffers("dynamicJitterBuffers", DEFAULT_DYNAMIC_JITTER_BUFFERS); Setting::Handle maxFramesOverDesired("maxFramesOverDesired", DEFAULT_MAX_FRAMES_OVER_DESIRED); Setting::Handle staticDesiredJitterBufferFrames("staticDesiredJitterBufferFrames", @@ -102,8 +100,7 @@ private: AudioClient::AudioClient() : AbstractAudioInterface(), - _gateThreshold("audioOutputGateThreshold", DEFAULT_AUDIO_OUTPUT_GATE_THRESHOLD), - _gate(this, _gateThreshold.get()), + _gate(this), _audioInput(NULL), _desiredInputFormat(), _inputFormat(), @@ -551,31 +548,53 @@ void AudioClient::handleAudioDataPacket(QSharedPointer message) } } -AudioClient::Gate::Gate(AudioClient* audioClient, int threshold) : - _audioClient(audioClient), - _threshold(threshold) {} +AudioClient::Gate::Gate(AudioClient* audioClient) : + _audioClient(audioClient) {} + +void AudioClient::Gate::setIsSimulatingJitter(bool enable) { + std::lock_guard lock(_mutex); + flush(); + _isSimulatingJitter = enable; +} void AudioClient::Gate::setThreshold(int threshold) { + std::lock_guard lock(_mutex); flush(); _threshold = std::max(threshold, 1); } void AudioClient::Gate::insert(QSharedPointer message) { + std::lock_guard lock(_mutex); + // Short-circuit for normal behavior - if (_threshold == 1) { + if (_threshold == 1 && !_isSimulatingJitter) { _audioClient->_receivedAudioStream.parseData(*message); return; } + // Throttle the current packet until the next flush _queue.push(message); _index++; - if (_index % _threshold == 0) { + // When appropriate, flush all held packets to the received audio stream + if (_isSimulatingJitter) { + // The JITTER_FLUSH_CHANCE defines the discrete probability density function of jitter (ms), + // where f(t) = pow(1 - JITTER_FLUSH_CHANCE, (t / 10) * JITTER_FLUSH_CHANCE + // for t (ms) = 10, 20, ... (because typical packet timegap is 10ms), + // because there is a JITTER_FLUSH_CHANCE of any packet instigating a flush of all held packets. + static const float JITTER_FLUSH_CHANCE = 0.6f; + // It is set at 0.6 to give a low chance of spikes (>30ms, 2.56%) so that they are obvious, + // but settled within the measured 5s window in audio network stats. + if (randFloat() < JITTER_FLUSH_CHANCE) { + flush(); + } + } else if (!(_index % _threshold)) { flush(); } } void AudioClient::Gate::flush() { + // Send all held packets to the received audio stream to be (eventually) played while (!_queue.empty()) { _audioClient->_receivedAudioStream.parseData(*_queue.front()); _queue.pop(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index c4d32e8694..926212cf47 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -132,6 +132,9 @@ public: int getOutputStarveDetectionThreshold() { return _outputStarveDetectionThreshold.get(); } void setOutputStarveDetectionThreshold(int threshold) { _outputStarveDetectionThreshold.set(threshold); } + bool isSimulatingJitter() { return _gate.isSimulatingJitter(); } + void setIsSimulatingJitter(bool enable) { _gate.setIsSimulatingJitter(enable); } + int getGateThreshold() { return _gate.getThreshold(); } void setGateThreshold(int threshold) { _gate.setThreshold(threshold); } @@ -230,7 +233,10 @@ private: class Gate { public: - Gate(AudioClient* audioClient, int threshold); + Gate(AudioClient* audioClient); + + bool isSimulatingJitter() { return _isSimulatingJitter; } + void setIsSimulatingJitter(bool enable); int getThreshold() { return _threshold; } void setThreshold(int threshold); @@ -242,11 +248,13 @@ private: AudioClient* _audioClient; std::queue> _queue; + std::mutex _mutex; + int _index{ 0 }; - int _threshold; + int _threshold{ 1 }; + bool _isSimulatingJitter{ false }; }; - Setting::Handle _gateThreshold; Gate _gate; Mutex _injectorsMutex; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index cc022d9df2..1c177cffc4 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -198,15 +198,10 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - #if defined(Q_OS_LINUX) - // these don't seem to work on Linux - return; - #else if (!buildWebSurface(static_cast(args->_renderer))) { return; } _fadeStartTime = usecTimestampNow(); - #endif } _lastRenderTime = usecTimestampNow(); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 739c0f8f4a..eecc1515f5 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -402,13 +402,18 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointerreadWithoutCopy(reasonSize); QString reasonMessage = QString::fromUtf8(reasonText); + quint16 extraInfoSize; + message->readPrimitive(&extraInfoSize); + auto extraInfoUtf8= message->readWithoutCopy(extraInfoSize); + QString extraInfo = QString::fromUtf8(extraInfoUtf8); + // output to the log so the user knows they got a denied connection request // and check and signal for an access token so that we can make sure they are logged in - qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage; + qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage << " extraInfo:" << extraInfo; if (!_domainConnectionRefusals.contains(reasonMessage)) { _domainConnectionRefusals.insert(reasonMessage); - emit domainConnectionRefused(reasonMessage, (int)reasonCode); + emit domainConnectionRefused(reasonMessage, (int)reasonCode, extraInfo); } auto accountManager = DependencyManager::get(); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 50639a4817..7f89b47197 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -123,7 +123,7 @@ signals: void settingsReceived(const QJsonObject& domainSettingsObject); void settingsReceiveFail(); - void domainConnectionRefused(QString reasonMessage, int reason); + void domainConnectionRefused(QString reasonMessage, int reason, const QString& extraInfo); private: bool reasonSuggestsLogin(ConnectionRefusedReason reasonCode); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 5aa31efea4..ec4b2c3573 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -745,8 +745,32 @@ void LimitedNodeList::removeSilentNodes() { const uint32_t RFC_5389_MAGIC_COOKIE = 0x2112A442; const int NUM_BYTES_STUN_HEADER = 20; -void LimitedNodeList::sendSTUNRequest() { +void LimitedNodeList::makeSTUNRequestPacket(char* stunRequestPacket) { + int packetIndex = 0; + + const uint32_t RFC_5389_MAGIC_COOKIE_NETWORK_ORDER = htonl(RFC_5389_MAGIC_COOKIE); + + // leading zeros + message type + const uint16_t REQUEST_MESSAGE_TYPE = htons(0x0001); + memcpy(stunRequestPacket + packetIndex, &REQUEST_MESSAGE_TYPE, sizeof(REQUEST_MESSAGE_TYPE)); + packetIndex += sizeof(REQUEST_MESSAGE_TYPE); + + // message length (no additional attributes are included) + uint16_t messageLength = 0; + memcpy(stunRequestPacket + packetIndex, &messageLength, sizeof(messageLength)); + packetIndex += sizeof(messageLength); + + memcpy(stunRequestPacket + packetIndex, &RFC_5389_MAGIC_COOKIE_NETWORK_ORDER, sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)); + packetIndex += sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); + + // transaction ID (random 12-byte unsigned integer) + const uint NUM_TRANSACTION_ID_BYTES = 12; + QUuid randomUUID = QUuid::createUuid(); + memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); +} + +void LimitedNodeList::sendSTUNRequest() { if (!_stunSockAddr.getAddress().isNull()) { const int NUM_INITIAL_STUN_REQUESTS_BEFORE_FAIL = 10; @@ -762,36 +786,14 @@ void LimitedNodeList::sendSTUNRequest() { } char stunRequestPacket[NUM_BYTES_STUN_HEADER]; - - int packetIndex = 0; - - const uint32_t RFC_5389_MAGIC_COOKIE_NETWORK_ORDER = htonl(RFC_5389_MAGIC_COOKIE); - - // leading zeros + message type - const uint16_t REQUEST_MESSAGE_TYPE = htons(0x0001); - memcpy(stunRequestPacket + packetIndex, &REQUEST_MESSAGE_TYPE, sizeof(REQUEST_MESSAGE_TYPE)); - packetIndex += sizeof(REQUEST_MESSAGE_TYPE); - - // message length (no additional attributes are included) - uint16_t messageLength = 0; - memcpy(stunRequestPacket + packetIndex, &messageLength, sizeof(messageLength)); - packetIndex += sizeof(messageLength); - - memcpy(stunRequestPacket + packetIndex, &RFC_5389_MAGIC_COOKIE_NETWORK_ORDER, sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)); - packetIndex += sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); - - // transaction ID (random 12-byte unsigned integer) - const uint NUM_TRANSACTION_ID_BYTES = 12; - QUuid randomUUID = QUuid::createUuid(); - memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); - + makeSTUNRequestPacket(stunRequestPacket); flagTimeForConnectionStep(ConnectionStep::SendSTUNRequest); - _nodeSocket.writeDatagram(stunRequestPacket, sizeof(stunRequestPacket), _stunSockAddr); } } -void LimitedNodeList::processSTUNResponse(std::unique_ptr packet) { +bool LimitedNodeList::parseSTUNResponse(udt::BasePacket* packet, + QHostAddress& newPublicAddress, uint16_t& newPublicPort) { // check the cookie to make sure this is actually a STUN response // and read the first attribute and make sure it is a XOR_MAPPED_ADDRESS const int NUM_BYTES_MESSAGE_TYPE_AND_LENGTH = 4; @@ -803,71 +805,79 @@ void LimitedNodeList::processSTUNResponse(std::unique_ptr packe if (memcmp(packet->getData() + NUM_BYTES_MESSAGE_TYPE_AND_LENGTH, &RFC_5389_MAGIC_COOKIE_NETWORK_ORDER, - sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)) == 0) { + sizeof(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER)) != 0) { + return false; + } - // enumerate the attributes to find XOR_MAPPED_ADDRESS_TYPE - while (attributeStartIndex < packet->getDataSize()) { + // enumerate the attributes to find XOR_MAPPED_ADDRESS_TYPE + while (attributeStartIndex < packet->getDataSize()) { + if (memcmp(packet->getData() + attributeStartIndex, &XOR_MAPPED_ADDRESS_TYPE, sizeof(XOR_MAPPED_ADDRESS_TYPE)) == 0) { + const int NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH = 4; + const int NUM_BYTES_FAMILY_ALIGN = 1; + const uint8_t IPV4_FAMILY_NETWORK_ORDER = htons(0x01) >> 8; - if (memcmp(packet->getData() + attributeStartIndex, &XOR_MAPPED_ADDRESS_TYPE, sizeof(XOR_MAPPED_ADDRESS_TYPE)) == 0) { - const int NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH = 4; - const int NUM_BYTES_FAMILY_ALIGN = 1; - const uint8_t IPV4_FAMILY_NETWORK_ORDER = htons(0x01) >> 8; + int byteIndex = attributeStartIndex + NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH + NUM_BYTES_FAMILY_ALIGN; - int byteIndex = attributeStartIndex + NUM_BYTES_STUN_ATTR_TYPE_AND_LENGTH + NUM_BYTES_FAMILY_ALIGN; + uint8_t addressFamily = 0; + memcpy(&addressFamily, packet->getData() + byteIndex, sizeof(addressFamily)); - uint8_t addressFamily = 0; - memcpy(&addressFamily, packet->getData() + byteIndex, sizeof(addressFamily)); + byteIndex += sizeof(addressFamily); - byteIndex += sizeof(addressFamily); + if (addressFamily == IPV4_FAMILY_NETWORK_ORDER) { + // grab the X-Port + uint16_t xorMappedPort = 0; + memcpy(&xorMappedPort, packet->getData() + byteIndex, sizeof(xorMappedPort)); - if (addressFamily == IPV4_FAMILY_NETWORK_ORDER) { - // grab the X-Port - uint16_t xorMappedPort = 0; - memcpy(&xorMappedPort, packet->getData() + byteIndex, sizeof(xorMappedPort)); + newPublicPort = ntohs(xorMappedPort) ^ (ntohl(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER) >> 16); - uint16_t newPublicPort = ntohs(xorMappedPort) ^ (ntohl(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER) >> 16); + byteIndex += sizeof(xorMappedPort); - byteIndex += sizeof(xorMappedPort); + // grab the X-Address + uint32_t xorMappedAddress = 0; + memcpy(&xorMappedAddress, packet->getData() + byteIndex, sizeof(xorMappedAddress)); - // grab the X-Address - uint32_t xorMappedAddress = 0; - memcpy(&xorMappedAddress, packet->getData() + byteIndex, sizeof(xorMappedAddress)); + uint32_t stunAddress = ntohl(xorMappedAddress) ^ ntohl(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); - uint32_t stunAddress = ntohl(xorMappedAddress) ^ ntohl(RFC_5389_MAGIC_COOKIE_NETWORK_ORDER); - - QHostAddress newPublicAddress(stunAddress); - - if (newPublicAddress != _publicSockAddr.getAddress() || newPublicPort != _publicSockAddr.getPort()) { - _publicSockAddr = HifiSockAddr(newPublicAddress, newPublicPort); - - qCDebug(networking, "New public socket received from STUN server is %s:%hu", - _publicSockAddr.getAddress().toString().toLocal8Bit().constData(), - _publicSockAddr.getPort()); - - if (!_hasCompletedInitialSTUN) { - // if we're here we have definitely completed our initial STUN sequence - stopInitialSTUNUpdate(true); - } - - emit publicSockAddrChanged(_publicSockAddr); - - flagTimeForConnectionStep(ConnectionStep::SetPublicSocketFromSTUN); - } - - // we're done reading the packet so we can return now - return; - } - } else { - // push forward attributeStartIndex by the length of this attribute - const int NUM_BYTES_ATTRIBUTE_TYPE = 2; - - uint16_t attributeLength = 0; - memcpy(&attributeLength, packet->getData() + attributeStartIndex + NUM_BYTES_ATTRIBUTE_TYPE, - sizeof(attributeLength)); - attributeLength = ntohs(attributeLength); - - attributeStartIndex += NUM_BYTES_MESSAGE_TYPE_AND_LENGTH + attributeLength; + // QHostAddress newPublicAddress(stunAddress); + newPublicAddress = QHostAddress(stunAddress); + return true; } + } else { + // push forward attributeStartIndex by the length of this attribute + const int NUM_BYTES_ATTRIBUTE_TYPE = 2; + + uint16_t attributeLength = 0; + memcpy(&attributeLength, packet->getData() + attributeStartIndex + NUM_BYTES_ATTRIBUTE_TYPE, + sizeof(attributeLength)); + attributeLength = ntohs(attributeLength); + + attributeStartIndex += NUM_BYTES_MESSAGE_TYPE_AND_LENGTH + attributeLength; + } + } + return false; +} + + +void LimitedNodeList::processSTUNResponse(std::unique_ptr packet) { + uint16_t newPublicPort; + QHostAddress newPublicAddress; + if (parseSTUNResponse(packet.get(), newPublicAddress, newPublicPort)) { + + if (newPublicAddress != _publicSockAddr.getAddress() || newPublicPort != _publicSockAddr.getPort()) { + _publicSockAddr = HifiSockAddr(newPublicAddress, newPublicPort); + + qCDebug(networking, "New public socket received from STUN server is %s:%hu", + _publicSockAddr.getAddress().toString().toLocal8Bit().constData(), + _publicSockAddr.getPort()); + + if (!_hasCompletedInitialSTUN) { + // if we're here we have definitely completed our initial STUN sequence + stopInitialSTUNUpdate(true); + } + + emit publicSockAddrChanged(_publicSockAddr); + + flagTimeForConnectionStep(ConnectionStep::SetPublicSocketFromSTUN); } } } diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index cd343a5232..e74a6c49f8 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -146,6 +146,7 @@ public: const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS, const QUuid& connectionSecret = QUuid()); + static bool parseSTUNResponse(udt::BasePacket* packet, QHostAddress& newPublicAddress, uint16_t& newPublicPort); bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } const HifiSockAddr& getLocalSockAddr() const { return _localSockAddr; } @@ -166,8 +167,8 @@ public: std::unique_ptr constructPingPacket(PingType_t pingType = PingType::Agnostic); std::unique_ptr constructPingReplyPacket(ReceivedMessage& message); - std::unique_ptr constructICEPingPacket(PingType_t pingType, const QUuid& iceID); - std::unique_ptr constructICEPingReplyPacket(ReceivedMessage& message, const QUuid& iceID); + static std::unique_ptr constructICEPingPacket(PingType_t pingType, const QUuid& iceID); + static std::unique_ptr constructICEPingReplyPacket(ReceivedMessage& message, const QUuid& iceID); void sendPeerQueryToIceServer(const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerID); @@ -232,6 +233,9 @@ public: bool packetVersionMatch(const udt::Packet& packet); bool isPacketVerified(const udt::Packet& packet); + static void makeSTUNRequestPacket(char* stunRequestPacket); + + public slots: void reset(); void eraseAllNodes(); @@ -275,7 +279,7 @@ protected: LimitedNodeList(int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT); LimitedNodeList(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton void operator=(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton - + qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode, const HifiSockAddr& overridenSockAddr); qint64 writePacket(const NLPacket& packet, const HifiSockAddr& destinationSockAddr, @@ -284,7 +288,7 @@ protected: void fillPacketHeader(const NLPacket& packet, const QUuid& connectionSecret = QUuid()); void setLocalSocket(const HifiSockAddr& sockAddr); - + bool packetSourceAndHashMatchAndTrackBandwidth(const udt::Packet& packet); void processSTUNResponse(std::unique_ptr packet); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 0f3d5885ff..ec4e724c1b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -64,7 +64,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return 18; // Introduction of node ignore request (which replaced an unused packet tpye) case PacketType::DomainConnectionDenied: - return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); + return static_cast(DomainConnectionDeniedVersion::IncludesExtraInfo); case PacketType::DomainConnectRequest: return static_cast(DomainConnectRequestVersion::HasProtocolVersions); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 25500b984f..aa775b9f53 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -206,7 +206,8 @@ enum class DomainConnectRequestVersion : PacketVersion { enum class DomainConnectionDeniedVersion : PacketVersion { ReasonMessageOnly = 17, - IncludesReasonCode + IncludesReasonCode, + IncludesExtraInfo }; enum class DomainServerAddedNodeVersion : PacketVersion { diff --git a/scripts/system/edit.js b/scripts/system/edit.js index d90566c619..d673bb4653 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -184,7 +184,7 @@ var toolBar = (function () { properties.position = position; entityID = Entities.addEntity(properties); } else { - Window.alert("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); + Window.notifyEditError("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); } selectionManager.clearSelections(); @@ -445,7 +445,7 @@ var toolBar = (function () { return; } if (active && !Entities.canRez() && !Entities.canRezTmp()) { - Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG); + Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); return; } Messages.sendLocalMessage("edit-events", JSON.stringify({ @@ -1082,13 +1082,13 @@ function handeMenuEvent(menuItem) { deleteSelectedEntities(); } else if (menuItem === "Export Entities") { if (!selectionManager.hasSelection()) { - Window.alert("No entities have been selected."); + Window.notifyEditError("No entities have been selected."); } else { var filename = Window.save("Select Where to Save", "", "*.json"); if (filename) { var success = Clipboard.exportEntities(filename, selectionManager.selections); if (!success) { - Window.alert("Export failed."); + Window.notifyEditError("Export failed."); } } } @@ -1156,7 +1156,7 @@ function getPositionToImportEntity() { } function importSVO(importURL) { if (!Entities.canAdjustLocks()) { - Window.alert(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); + Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); return; } @@ -1188,10 +1188,10 @@ function importSVO(importURL) { Window.raiseMainWindow(); } else { - Window.alert("Can't import objects: objects would be out of bounds."); + Window.notifyEditError("Can't import objects: objects would be out of bounds."); } } else { - Window.alert("There was an error importing the entity file."); + Window.notifyEditError("There was an error importing the entity file."); } Overlays.editOverlay(importingSVOTextOverlay, { @@ -1481,7 +1481,7 @@ var PropertiesTool = function (opts) { // If any of the natural dimensions are not 0, resize if (properties.type === "Model" && naturalDimensions.x === 0 && naturalDimensions.y === 0 && naturalDimensions.z === 0) { - Window.alert("Cannot reset entity to its natural dimensions: Model URL" + + Window.notifyEditError("Cannot reset entity to its natural dimensions: Model URL" + " is invalid or the model has not yet been loaded."); } else { Entities.editEntity(selectionManager.selections[i], { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index f41b0502c8..f3ba466342 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -58,6 +58,8 @@ // } // } +/* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ + (function() { // BEGIN LOCAL_SCOPE Script.include("./libraries/soundArray.js"); @@ -76,11 +78,9 @@ var fontSize = 12.0; var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades var PERSIST_TIME_3D = 15.0; var persistTime = PERSIST_TIME_2D; -var clickedText = false; var frame = 0; var ourWidth = Window.innerWidth; var ourHeight = Window.innerHeight; -var text = "placeholder"; var ctrlIsPressed = false; var ready = true; var MENU_NAME = 'Tools > Notifications'; @@ -97,12 +97,14 @@ var NotificationType = { WINDOW_RESIZE: 3, LOD_WARNING: 4, CONNECTION_REFUSED: 5, + EDIT_ERROR: 6, properties: [ { text: "Mute Toggle" }, { text: "Snapshot" }, { text: "Window Resize" }, { text: "Level of Detail" }, - { text: "Connection Refused" } + { text: "Connection Refused" }, + { text: "Edit error" } ], getTypeFromMenuItem: function(menuItemName) { if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { @@ -253,6 +255,9 @@ function notify(notice, button, height, imageProperties, image) { positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + if (!image) { notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; @@ -270,6 +275,8 @@ function notify(notice, button, height, imageProperties, image) { button.url = button.imageURL; button.scale = button.width * NOTIFICATION_3D_SCALE; button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ @@ -279,6 +286,34 @@ function notify(notice, button, height, imageProperties, image) { width: noticeWidth, height: noticeHeight }); + + + var defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition; + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { position: notificationPosition, + rotation: notificationOrientation }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); + } + } + } else { if (!image) { notificationText = Overlays.addOverlay("text", notice); @@ -429,11 +464,6 @@ function update() { noticeOut, buttonOut, arraysOut, - defaultEyePosition, - avatarOrientation, - notificationPosition, - notificationOrientation, - buttonPosition, positions, i, j, @@ -457,7 +487,8 @@ function update() { Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, overlay3DDetails[i].height, locationY); + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; overlay3DDetails[i].notificationPosition = positions.notificationPosition; overlay3DDetails[i].buttonPosition = positions.buttonPosition; @@ -480,22 +511,6 @@ function update() { } } } - - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; - - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { position: notificationPosition, rotation: notificationOrientation }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } - } } var STARTUP_TIMEOUT = 500, // ms @@ -532,12 +547,17 @@ function onDomainConnectionRefused(reason) { createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); } +function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); +} + + function onSnapshotTaken(path, notify) { if (notify) { var imageProperties = { path: "file:///" + path, aspectRatio: Window.innerWidth / Window.innerHeight - } + }; createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); } } @@ -571,8 +591,6 @@ function keyReleaseEvent(key) { // Triggers notification on specific key driven events function keyPressEvent(key) { - var noteString; - if (key.key === 16777249) { ctrlIsPressed = true; } @@ -622,13 +640,13 @@ function menuItemEvent(menuItem) { } LODManager.LODDecreased.connect(function() { - var warningText = "\n" - + "Due to the complexity of the content, the \n" - + "level of detail has been decreased. " - + "You can now see: \n" - + LODManager.getLODFeedbackText(); + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID == false) { + if (lodTextID === false) { lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); } else { Overlays.editOverlay(lodTextID, { text: warningText }); @@ -644,6 +662,7 @@ Script.scriptEnding.connect(scriptEnding); Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.snapshotTaken.connect(onSnapshotTaken); +Window.notifyEditError = onEditError; setup(); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index bf645f25c2..a077efc335 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -7,3 +7,6 @@ set_target_properties(udt-test PROPERTIES FOLDER "Tools") add_subdirectory(vhacd-util) set_target_properties(vhacd-util PROPERTIES FOLDER "Tools") + +add_subdirectory(ice-client) +set_target_properties(ice-client PROPERTIES FOLDER "Tools") diff --git a/tools/ice-client/CMakeLists.txt b/tools/ice-client/CMakeLists.txt new file mode 100644 index 0000000000..a80145974c --- /dev/null +++ b/tools/ice-client/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ice-client) +setup_hifi_project(Core Widgets) +link_hifi_libraries(shared networking) diff --git a/tools/ice-client/src/ICEClientApp.cpp b/tools/ice-client/src/ICEClientApp.cpp new file mode 100644 index 0000000000..992014ad7d --- /dev/null +++ b/tools/ice-client/src/ICEClientApp.cpp @@ -0,0 +1,387 @@ +// +// ICEClientApp.cpp +// tools/ice-client/src +// +// Created by Seth Alves on 3/5/15. +// 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 +#include +#include +#include +#include +#include + +#include "ICEClientApp.h" + +ICEClientApp::ICEClientApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + // parse command-line + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity ICE client"); + parser.addHelpOption(); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + + const QCommandLineOption iceServerAddressOption("i", "ice-server address", "IP:PORT or HOSTNAME:PORT"); + parser.addOption(iceServerAddressOption); + + const QCommandLineOption howManyTimesOption("n", "how many times to cycle", "1"); + parser.addOption(howManyTimesOption); + + const QCommandLineOption domainIDOption("d", "domain-server uuid", "00000000-0000-0000-0000-000000000000"); + parser.addOption(domainIDOption); + + const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); + parser.addOption(cacheSTUNOption); + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + _verbose = parser.isSet(verboseOutput); + if (!_verbose) { + const_cast(&networking())->setEnabled(QtDebugMsg, false); + const_cast(&networking())->setEnabled(QtInfoMsg, false); + const_cast(&networking())->setEnabled(QtWarningMsg, false); + } + + _stunSockAddr = HifiSockAddr(STUN_SERVER_HOSTNAME, STUN_SERVER_PORT, true); + + _cacheSTUNResult = parser.isSet(cacheSTUNOption); + + if (parser.isSet(howManyTimesOption)) { + _actionMax = parser.value(howManyTimesOption).toInt(); + } else { + _actionMax = 1; + } + + if (parser.isSet(domainIDOption)) { + _domainID = QUuid(parser.value(domainIDOption)); + if (_verbose) { + qDebug() << "domain-server ID is" << _domainID; + } + } + + _iceServerAddr = HifiSockAddr("127.0.0.1", ICE_SERVER_DEFAULT_PORT); + if (parser.isSet(iceServerAddressOption)) { + // parse the IP and port combination for this target + QString hostnamePortString = parser.value(iceServerAddressOption); + + QHostAddress address { hostnamePortString.left(hostnamePortString.indexOf(':')) }; + quint16 port { (quint16) hostnamePortString.mid(hostnamePortString.indexOf(':') + 1).toUInt() }; + if (port == 0) { + port = ICE_SERVER_DEFAULT_PORT; + } + + if (address.isNull()) { + qCritical() << "Could not parse an IP address and port combination from" << hostnamePortString << "-" << + "The parsed IP was" << address.toString() << "and the parsed port was" << port; + + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + } else { + _iceServerAddr = HifiSockAddr(address, port); + } + } + + if (_verbose) { + qDebug() << "ICE-server address is" << _iceServerAddr; + } + + setState(lookUpStunServer); + + QTimer* doTimer = new QTimer(this); + connect(doTimer, &QTimer::timeout, this, &ICEClientApp::doSomething); + doTimer->start(200); +} + +ICEClientApp::~ICEClientApp() { + delete _socket; +} + +void ICEClientApp::setState(int newState) { + _state = newState; +} + +void ICEClientApp::closeSocket() { + _domainServerPeerSet = false; + delete _socket; + _socket = nullptr; +} + +void ICEClientApp::openSocket() { + if (_socket) { + return; + } + + _socket = new udt::Socket(); + unsigned int localPort = 0; + _socket->bind(QHostAddress::AnyIPv4, localPort); + _socket->setPacketHandler([this](std::unique_ptr packet) { processPacket(std::move(packet)); }); + _socket->addUnfilteredHandler(_stunSockAddr, + [this](std::unique_ptr packet) { + processSTUNResponse(std::move(packet)); + }); + + if (_verbose) { + qDebug() << "local port is" << _socket->localPort(); + } + _localSockAddr = HifiSockAddr("127.0.0.1", _socket->localPort()); + _publicSockAddr = HifiSockAddr("127.0.0.1", _socket->localPort()); + _domainPingCount = 0; +} + +void ICEClientApp::doSomething() { + if (_actionMax > 0 && _actionCount >= _actionMax) { + // time to stop. + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + + } else if (_state == lookUpStunServer) { + // lookup STUN server address + if (!_stunSockAddr.getAddress().isNull()) { + if (_verbose) { + qDebug() << "stun server is" << _stunSockAddr; + } + setState(sendStunRequestPacket); + } else { + if (_verbose) { + qDebug() << "_stunSockAddr is" << _stunSockAddr.getAddress(); + } + QCoreApplication::exit(stunFailureExitStatus); + } + + } else if (_state == sendStunRequestPacket) { + // send STUN request packet + closeSocket(); + openSocket(); + + if (!_cacheSTUNResult || !_stunResultSet) { + const int NUM_BYTES_STUN_HEADER = 20; + char stunRequestPacket[NUM_BYTES_STUN_HEADER]; + LimitedNodeList::makeSTUNRequestPacket(stunRequestPacket); + if (_verbose) { + qDebug() << "sending STUN request"; + } + _socket->writeDatagram(stunRequestPacket, sizeof(stunRequestPacket), _stunSockAddr); + _stunResponseTimerCanceled = false; + _stunResponseTimer.singleShot(stunResponseTimeoutMilliSeconds, this, [&] { + if (_stunResponseTimerCanceled) { + return; + } + if (_verbose) { + qDebug() << "timeout waiting for stun-server response"; + } + QCoreApplication::exit(stunFailureExitStatus); + }); + + setState(waitForStunResponse); + } else { + if (_verbose) { + qDebug() << "using cached STUN response"; + } + _publicSockAddr.setPort(_socket->localPort()); + setState(talkToIceServer); + } + + } else if (_state == talkToIceServer) { + QUuid peerID; + if (_domainID == QUuid()) { + // pick a random domain-id which will fail + peerID = QUuid::createUuid(); + setState(pause0); + } else { + // use the domain UUID given on the command-line + peerID = _domainID; + setState(waitForIceReply); + } + _sessionUUID = QUuid::createUuid(); + if (_verbose) { + qDebug() << "I am" << _sessionUUID; + } + + sendPacketToIceServer(PacketType::ICEServerQuery, _iceServerAddr, _sessionUUID, peerID); + _iceResponseTimerCanceled = false; + _iceResponseTimer.singleShot(iceResponseTimeoutMilliSeconds, this, [=] { + if (_iceResponseTimerCanceled) { + return; + } + if (_verbose) { + qDebug() << "timeout waiting for ice-server response"; + } + QCoreApplication::exit(iceFailureExitStatus); + }); + } else if (_state == pause0) { + setState(pause1); + } else if (_state == pause1) { + if (_verbose) { + qDebug() << ""; + } + closeSocket(); + setState(sendStunRequestPacket); + _actionCount++; + } +} + +void ICEClientApp::sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr, + const QUuid& clientID, const QUuid& peerID) { + std::unique_ptr icePacket = NLPacket::create(packetType); + + QDataStream iceDataStream(icePacket.get()); + iceDataStream << clientID << _publicSockAddr << _localSockAddr; + + if (packetType == PacketType::ICEServerQuery) { + assert(!peerID.isNull()); + + iceDataStream << peerID; + + if (_verbose) { + qDebug() << "Sending packet to ICE server to request connection info for peer with ID" + << uuidStringWithoutCurlyBraces(peerID); + } + } + + // fillPacketHeader(packet, connectionSecret); + _socket->writePacket(*icePacket, _iceServerAddr); +} + +void ICEClientApp::checkDomainPingCount() { + _domainPingCount++; + if (_domainPingCount > 5) { + if (_verbose) { + qDebug() << "too many unanswered pings to domain-server."; + } + QCoreApplication::exit(domainPingExitStatus); + } +} + +void ICEClientApp::icePingDomainServer() { + if (!_domainServerPeerSet) { + return; + } + + if (_verbose) { + qDebug() << "ice-pinging domain-server: " << _domainServerPeer; + } + + auto localPingPacket = LimitedNodeList::constructICEPingPacket(PingType::Local, _sessionUUID); + _socket->writePacket(*localPingPacket, _domainServerPeer.getLocalSocket()); + + auto publicPingPacket = LimitedNodeList::constructICEPingPacket(PingType::Public, _sessionUUID); + _socket->writePacket(*publicPingPacket, _domainServerPeer.getPublicSocket()); + checkDomainPingCount(); +} + +void ICEClientApp::processSTUNResponse(std::unique_ptr packet) { + if (_verbose) { + qDebug() << "got stun response"; + } + if (_state != waitForStunResponse) { + if (_verbose) { + qDebug() << "got unexpected stun response"; + } + QCoreApplication::exit(stunFailureExitStatus); + } + + _stunResponseTimer.stop(); + _stunResponseTimerCanceled = true; + + uint16_t newPublicPort; + QHostAddress newPublicAddress; + if (LimitedNodeList::parseSTUNResponse(packet.get(), newPublicAddress, newPublicPort)) { + _publicSockAddr = HifiSockAddr(newPublicAddress, newPublicPort); + if (_verbose) { + qDebug() << "My public address is" << _publicSockAddr; + } + _stunResultSet = true; + setState(talkToIceServer); + } else { + QCoreApplication::exit(stunFailureExitStatus); + } +} + + +void ICEClientApp::processPacket(std::unique_ptr packet) { + std::unique_ptr nlPacket = NLPacket::fromBase(std::move(packet)); + + if (nlPacket->getPayloadSize() < NLPacket::localHeaderSize(PacketType::ICEServerHeartbeat)) { + if (_verbose) { + qDebug() << "got a short packet."; + } + return; + } + + QSharedPointer message = QSharedPointer::create(*nlPacket); + const HifiSockAddr& senderAddr = message->getSenderSockAddr(); + + if (nlPacket->getType() == PacketType::ICEServerPeerInformation) { + // cancel the timeout timer + _iceResponseTimer.stop(); + _iceResponseTimerCanceled = true; + + QDataStream iceResponseStream(message->getMessage()); + if (!_domainServerPeerSet) { + iceResponseStream >> _domainServerPeer; + if (_verbose) { + qDebug() << "got ICEServerPeerInformation from" << _domainServerPeer; + } + _domainServerPeerSet = true; + + icePingDomainServer(); + _pingDomainTimer = new QTimer(this); + connect(_pingDomainTimer, &QTimer::timeout, this, &ICEClientApp::icePingDomainServer); + _pingDomainTimer->start(500); + } else { + NetworkPeer domainServerPeer; + iceResponseStream >> domainServerPeer; + if (_verbose) { + qDebug() << "got repeat ICEServerPeerInformation from" << domainServerPeer; + } + } + + } else if (nlPacket->getType() == PacketType::ICEPing) { + if (_verbose) { + qDebug() << "got packet: " << nlPacket->getType(); + } + auto replyPacket = LimitedNodeList::constructICEPingReplyPacket(*message, _sessionUUID); + _socket->writePacket(*replyPacket, senderAddr); + checkDomainPingCount(); + + } else if (nlPacket->getType() == PacketType::ICEPingReply) { + if (_verbose) { + qDebug() << "got packet: " << nlPacket->getType(); + } + if (_domainServerPeerSet && _state == waitForIceReply && + (senderAddr == _domainServerPeer.getLocalSocket() || + senderAddr == _domainServerPeer.getPublicSocket())) { + + delete _pingDomainTimer; + _pingDomainTimer = nullptr; + + setState(pause0); + } else { + if (_verbose) { + qDebug() << "got unexpected ICEPingReply" << senderAddr; + } + } + + } else { + if (_verbose) { + qDebug() << "got unexpected packet: " << nlPacket->getType(); + } + } +} diff --git a/tools/ice-client/src/ICEClientApp.h b/tools/ice-client/src/ICEClientApp.h new file mode 100644 index 0000000000..3635bc07f4 --- /dev/null +++ b/tools/ice-client/src/ICEClientApp.h @@ -0,0 +1,96 @@ +// +// ICEClientApp.h +// tools/ice-client/src +// +// Created by Seth Alves on 2016-9-16 +// Copyright 2016 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 +// + + +#ifndef hifi_ICEClientApp_h +#define hifi_ICEClientApp_h + +#include +#include +#include +#include +#include + + +class ICEClientApp : public QCoreApplication { + Q_OBJECT +public: + ICEClientApp(int argc, char* argv[]); + ~ICEClientApp(); + + const int stunFailureExitStatus { 1 }; + const int iceFailureExitStatus { 2 }; + const int domainPingExitStatus { 3 }; + + const int stunResponseTimeoutMilliSeconds { 2000 }; + const int iceResponseTimeoutMilliSeconds { 2000 }; + +private: + enum State { + lookUpStunServer, // 0 + sendStunRequestPacket, // 1 + waitForStunResponse, // 2 + talkToIceServer, // 3 + waitForIceReply, // 4 + pause0, // 5 + pause1 // 6 + }; + + void closeSocket(); + void openSocket(); + + void setState(int newState); + + void doSomething(); + void sendPacketToIceServer(PacketType packetType, const HifiSockAddr& iceServerSockAddr, + const QUuid& clientID, const QUuid& peerID); + void icePingDomainServer(); + void processSTUNResponse(std::unique_ptr packet); + void processPacket(std::unique_ptr packet); + void checkDomainPingCount(); + + bool _verbose; + bool _cacheSTUNResult; // should we only talk to stun server once? + bool _stunResultSet { false }; // have we already talked to stun server? + + HifiSockAddr _stunSockAddr; + + unsigned int _actionCount { 0 }; + unsigned int _actionMax { 0 }; + + QUuid _sessionUUID; + QUuid _domainID; + + QTimer* _pingDomainTimer { nullptr }; + + HifiSockAddr _iceServerAddr; + + HifiSockAddr _localSockAddr; + HifiSockAddr _publicSockAddr; + udt::Socket* _socket { nullptr }; + + bool _domainServerPeerSet { false }; + NetworkPeer _domainServerPeer; + + int _state { 0 }; + + QTimer _stunResponseTimer; + bool _stunResponseTimerCanceled { false }; + QTimer _iceResponseTimer; + bool _iceResponseTimerCanceled { false }; + int _domainPingCount { 0 }; +}; + + + + + +#endif //hifi_ICEClientApp_h diff --git a/tools/ice-client/src/main.cpp b/tools/ice-client/src/main.cpp new file mode 100644 index 0000000000..c70a7eb7d7 --- /dev/null +++ b/tools/ice-client/src/main.cpp @@ -0,0 +1,23 @@ +// +// main.cpp +// tools/ice-client/src +// +// Created by Seth Alves on 2016-9-16 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include +#include +#include +#include + +#include "ICEClientApp.h" + +using namespace std; + +int main(int argc, char * argv[]) { + ICEClientApp app(argc, argv); + return app.exec(); +}