diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 870149f1bc..c8b68a740c 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -132,7 +132,7 @@ void AvatarMixer::start() { auto start = usecTimestampNow(); nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [&](const SharedNodePointer& node) { - manageDisplayName(node); + manageIdentityData(node); ++_sumListeners; }); }, &lockWait, &nodeTransform, &functor); @@ -183,8 +183,9 @@ void AvatarMixer::start() { // NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData // is guaranteed to not be accessed by other thread -void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { +void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + bool sendIdentity = false; if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { AvatarData& avatar = nodeData->getAvatar(); const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); @@ -210,9 +211,39 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { soFar.second++; // refcount nodeData->flagIdentityChange(); nodeData->setAvatarSessionDisplayNameMustChange(false); - sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. + sendIdentity = true; qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); } + if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist + nodeData->setAvatarSkeletonModelUrlMustChange(false); + AvatarData& avatar = nodeData->getAvatar(); + static const QUrl emptyURL(""); + QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL); + if (!isAvatarInWhitelist(url)) { + qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar); + avatar.setSkeletonModelURL(_replacementAvatar); + sendIdentity = true; + } + } + if (sendIdentity) { + sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar. + } +} + +bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) { + // The avatar is in the whitelist if: + // 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND + // 2. The avatar's URL's path starts with the path of that same URL in the whitelist + for (const auto& whiteListedPrefix : _avatarWhitelist) { + auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix); + // check if this script URL matches the whitelist domain and, optionally, is beneath the path + if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 && + url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) { + return true; + } + } + + return false; } void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) { @@ -402,13 +433,17 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); bool identityChanged = false; bool displayNameChanged = false; - avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged); + bool skeletonModelUrlChanged = false; + avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged); if (identityChanged) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); if (displayNameChanged) { nodeData->setAvatarSessionDisplayNameMustChange(true); } + if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) { + nodeData->setAvatarSkeletonModelUrlMustChange(true); + } } } } @@ -764,4 +799,19 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale << "and a maximum avatar scale of" << _domainMaximumScale; + const QString AVATAR_WHITELIST_DEFAULT{ "" }; + static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; + _avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts); + + static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; + _replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT); + + if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) { + _avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok). + } + if (_avatarWhitelist.isEmpty()) { + qCDebug(avatars) << "All avatars are allowed."; + } else { + qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar); + } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 1925ec1ebd..f8ebe419a9 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -59,7 +59,12 @@ private: void parseDomainServerSettings(const QJsonObject& domainSettings); void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); - void manageDisplayName(const SharedNodePointer& node); + void manageIdentityData(const SharedNodePointer& node); + bool isAvatarInWhitelist(const QUrl& url); + + const QString REPLACEMENT_AVATAR_DEFAULT{ "" }; + QStringList _avatarWhitelist { }; + QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT }; p_high_resolution_clock::time_point _lastFrameTimestamp; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index c905b10251..12b0286088 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -65,6 +65,8 @@ public: void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); } bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } + bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; } + void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; } void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; } void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; } @@ -146,6 +148,7 @@ private: uint64_t _identityChangeTimestamp; bool _avatarSessionDisplayNameMustChange{ true }; + bool _avatarSkeletonModelUrlMustChange{ false }; int _numAvatarsSentLastFrame = 0; int _numFramesSinceAdjustment = 0; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index a8c6dd84e7..c5e9b08143 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -866,6 +866,22 @@ "help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.", "placeholder": 3.0, "default": 3.0 + }, + { + "name": "avatar_whitelist", + "label": "Avatars Allowed from:", + "help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.", + "placeholder": "", + "default": "", + "advanced": true + }, + { + "name": "replacement_avatar", + "label": "Replacement Avatar for disallowed avatars", + "help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.", + "placeholder": "", + "default": "", + "advanced": true } ] }, diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 56e8c8e2fb..5e168c6620 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5296,6 +5296,11 @@ void Application::nodeActivated(SharedNodePointer node) { if (node->getType() == NodeType::AvatarMixer) { // new avatar mixer, send off our identity packet on next update loop + // Reset skeletonModelUrl if the last server modified our choice. + static const QUrl empty{}; + if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->cannonicalSkeletonModelURL(empty)) { + getMyAvatar()->resetFullAvatarURL(); + } getMyAvatar()->markIdentityDataChanged(); getMyAvatar()->resetLastSent(); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 4407e12295..7731d53ec3 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1504,7 +1504,7 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; } -void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) { +void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged) { if (identity.sequenceId < _identitySequenceId) { qCDebug(avatars) << "Ignoring older identity packet for avatar" << getSessionUUID() @@ -1517,6 +1517,7 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { setSkeletonModelURL(identity.skeletonModelURL); identityChanged = true; + skeletonModelUrlChanged = true; if (_firstSkeletonCheck) { displayNameChanged = true; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 4104615cfe..8941d9d95f 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -368,6 +368,7 @@ public: virtual ~AvatarData(); static const QUrl& defaultFullAvatarModelUrl(); + QUrl cannonicalSkeletonModelURL(const QUrl& empty) const; virtual bool isMyAvatar() const { return false; } @@ -536,9 +537,8 @@ public: static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); - // identityChanged returns true if identity has changed, false otherwise. - // displayNameChanged returns true if displayName has changed, false otherwise. - void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged); + // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. + void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, bool& skeletonModelUrlChanged); QByteArray identityByteArray() const; @@ -697,7 +697,6 @@ protected: QVector _attachmentData; QString _displayName; QString _sessionDisplayName { }; - QUrl cannonicalSkeletonModelURL(const QUrl& empty) const; QHash _jointIndices; ///< 1-based, since zero is returned for missing keys QStringList _jointNames; ///< in order of depth-first traversal diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 2ccc64fee2..fb954f4731 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -148,8 +148,9 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); bool identityChanged = false; bool displayNameChanged = false; + bool skeletonModelUrlChanged = false; // In this case, the "sendingNode" is the Avatar Mixer. - avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged); + avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged, skeletonModelUrlChanged); } }