Merge pull request #15453 from SimonWalton-HiFi/avatar-verification

Avatar verification
This commit is contained in:
Shannon Romano 2019-05-06 08:04:06 -07:00 committed by GitHub
commit 4c31c6f6e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 676 additions and 20 deletions

View file

@ -82,6 +82,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
this, "handleOctreePacket");
packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnership");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
@ -367,10 +368,13 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
return;
}
bool sendIdentity = false;
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
AvatarData& avatar = nodeData->getAvatar();
const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName();
MixerAvatar& avatar = nodeData->getAvatar();
bool sendIdentity = avatar.needsIdentityUpdate();
if (sendIdentity) {
nodeData->flagIdentityChange();
}
if (nodeData->getAvatarSessionDisplayNameMustChange()) {
const QString& existingBaseDisplayName = avatar.getSessionDisplayName();
if (!existingBaseDisplayName.isEmpty()) {
SessionDisplayName existingDisplayName { existingBaseDisplayName };
@ -414,10 +418,11 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
// since this packet includes a change to either the skeleton model URL or the display name
// it needs a new sequence number
nodeData->getAvatar().pushIdentitySequenceNumber();
avatar.pushIdentitySequenceNumber();
// tell node whose name changed about its new session display name or avatar.
sendIdentityPacket(nodeData, node);
avatar.setNeedsIdentityUpdate(false);
}
}
@ -1123,6 +1128,16 @@ void AvatarMixer::entityChange() {
_dirtyHeroStatus = true;
}
void AvatarMixer::handleChallengeOwnership(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
if (senderNode->getType() == NodeType::Agent && senderNode->getLinkedData()) {
auto clientData = static_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
auto avatar = clientData->getAvatarSharedPointer();
if (avatar) {
avatar->handleChallengeResponse(message.data());
}
}
}
void AvatarMixer::aboutToFinish() {
DependencyManager::destroy<ResourceManager>();
DependencyManager::destroy<ResourceCacheSharedItems>();

View file

@ -65,6 +65,7 @@ private slots:
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleChallengeOwnership(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void start();
private:

View file

@ -81,6 +81,10 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
}
assert(_packetQueue.empty());
if (_avatar) {
_avatar->processCertifyEvents();
}
return packetsProcessed;
}
@ -200,6 +204,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
if (traitType == AvatarTraits::SkeletonModelURL) {
// special handling for skeleton model URL, since we need to make sure it is in the whitelist
checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion);
_avatar->fetchAvatarFST();
}
anyTraitsChanged = true;

View file

@ -157,6 +157,11 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
++simpleReceivedIt;
}
if (bytesWritten > 0 && sendingAvatar->isCertifyFailed()) {
// Resend identity packet if certification failed:
sendingAvatar->setNeedsIdentityUpdate();
}
// enumerate the received instanced trait versions
auto instancedReceivedIt = lastReceivedVersions.instancedCBegin();
while (instancedReceivedIt != lastReceivedVersions.instancedCEnd()) {

View file

@ -0,0 +1,345 @@
//
// MixerAvatar.cpp
// assignment-client/src/avatars
//
// Created by Simon Walton April 2019
// Copyright 2019 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 "MixerAvatar.h"
#include <QRegularExpression>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QCryptographicHash>
#include <QApplication>
#include <ResourceManager.h>
#include <NetworkAccessManager.h>
#include <NetworkingConstants.h>
#include <EntityItem.h>
#include <EntityItemProperties.h>
#include "ClientTraitsHandler.h"
#include "AvatarLogging.h"
void MixerAvatar::fetchAvatarFST() {
_verifyState = nonCertified;
_pendingEvent = false;
QUrl avatarURL = getSkeletonModelURL();
if (avatarURL.isEmpty() || avatarURL.isLocalFile() || avatarURL.scheme() == "qrc") {
// Not network FST.
return;
}
_certificateIdFromURL.clear();
_certificateIdFromFST.clear();
_marketplaceIdFromURL.clear();
_marketplaceIdFromFST.clear();
auto resourceManager = DependencyManager::get<ResourceManager>();
// Match UUID + (optionally) URL cert
static const QRegularExpression marketIdRegex{
"^https://.*?highfidelity\\.com/api/.*?/commerce/entity_edition/([-0-9a-z]{36})(.*?certificate_id=([\\w/+%]+)|.*).*$"
};
auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString());
if (marketIdMatch.hasMatch()) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
_marketplaceIdFromURL = marketIdMatch.captured(1);
if (marketIdMatch.lastCapturedIndex() == 3) {
_certificateIdFromURL = QUrl::fromPercentEncoding(marketIdMatch.captured(3).toUtf8());
}
}
ResourceRequest* fstRequest = resourceManager->createResourceRequest(this, avatarURL);
if (fstRequest) {
QMutexLocker certifyLocker(&_avatarCertifyLock);
_avatarRequest = fstRequest;
_verifyState = requestingFST;
connect(fstRequest, &ResourceRequest::finished, this, &MixerAvatar::fstRequestComplete);
fstRequest->send();
} else {
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL;
_verifyState = error;
}
_needsIdentityUpdate = true;
}
void MixerAvatar::fstRequestComplete() {
ResourceRequest* fstRequest = static_cast<ResourceRequest*>(QObject::sender());
QMutexLocker certifyLocker(&_avatarCertifyLock);
if (fstRequest == _avatarRequest) {
auto result = fstRequest->getResult();
if (result != ResourceRequest::Success) {
_verifyState = error;
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "failed:" << result;
} else {
_avatarFSTContents = fstRequest->getData();
_verifyState = receivedFST;
_pendingEvent = true;
}
_avatarRequest->deleteLater();
_avatarRequest = nullptr;
} else {
qCDebug(avatars) << "Incorrect request for" << getDisplayName();
}
}
bool MixerAvatar::generateFSTHash() {
if (_avatarFSTContents.length() == 0) {
return false;
}
QByteArray hashJson = canonicalJson(_avatarFSTContents);
QCryptographicHash fstHash(QCryptographicHash::Sha256);
fstHash.addData(hashJson);
_certificateHash = fstHash.result();
return true;
}
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
// Guess we should refactor this stuff into a Authorization namespace ...
return EntityItemProperties::verifySignature(publicKey, _certificateHash,
QByteArray::fromBase64(_certificateIdFromFST.toUtf8()));
}
QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts);
static const QString fstKeywordsReg {
"(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|"
"filename|texdir|script|editionNumber|certificateID)"
};
QRegularExpression fstLineRegExp { QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" };
QStringListIterator fstLineIter(fstLines);
QJsonObject certifiedItems;
QStringList scripts;
while (fstLineIter.hasNext()) {
auto line = fstLineIter.next();
auto lineMatch = fstLineRegExp.match(line);
if (lineMatch.hasMatch()) {
QString key = lineMatch.captured(1);
if (key == "certificateID") {
_certificateIdFromFST = lineMatch.captured(2);
} else if (key == "itemDescription") {
// Item description can be multiline - intermediate lines end in <CR>
QString itemDesc = lineMatch.captured(2);
while (itemDesc.endsWith('\r') && fstLineIter.hasNext()) {
itemDesc += '\n' + fstLineIter.next();
}
certifiedItems[key] = QJsonValue(itemDesc);
} else if (key == "limitedRun" || key == "editionNumber") {
double value = lineMatch.captured(2).toDouble();
if (value != 0.0) {
certifiedItems[key] = QJsonValue(value);
}
} else if (key == "script") {
scripts.append(lineMatch.captured(2).trimmed());
} else {
certifiedItems[key] = QJsonValue(lineMatch.captured(2));
if (key == "marketplaceID") {
_marketplaceIdFromFST = lineMatch.captured(2);
}
}
}
}
if (!scripts.empty()) {
scripts.sort();
certifiedItems["script"] = QJsonArray::fromStringList(scripts);
}
QJsonDocument jsonDocCertifiedItems(certifiedItems);
//Example working form:
//return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax",
//"itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,
//"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
}
void MixerAvatar::ownerRequestComplete() {
QMutexLocker certifyLocker(&_avatarCertifyLock);
QNetworkReply* networkReply = static_cast<QNetworkReply*>(QObject::sender());
if (networkReply->error() == QNetworkReply::NoError) {
_dynamicMarketResponse = networkReply->readAll();
_verifyState = ownerResponse;
_pendingEvent = true;
} else {
auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
<< jsonData.toObject()["message"].toString();
_verifyState = error;
_pendingEvent = false;
}
}
networkReply->deleteLater();
}
void MixerAvatar::processCertifyEvents() {
if (!_pendingEvent) {
return;
}
QMutexLocker certifyLocker(&_avatarCertifyLock);
switch (_verifyState) {
case receivedFST:
{
generateFSTHash();
if (_certificateIdFromFST.length() != 0) {
QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
bool staticVerification = validateFSTHash(marketplacePublicKey);
_verifyState = staticVerification ? staticValidation : verificationFailed;
if (_verifyState == staticValidation) {
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath(POP_MARKETPLACE_API);
networkRequest.setUrl(requestURL);
QJsonObject request;
request["certificate_id"] = _certificateIdFromFST;
_verifyState = requestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
} else {
_needsIdentityUpdate = true;
_pendingEvent = false;
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
}
} else { // FST doesn't have a certificate, so noncertified rather than failed:
_pendingEvent = false;
_verifyState = nonCertified;
}
break;
}
case ownerResponse:
{
QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
QString ownerPublicKey;
bool ownerValid = false;
if (responseJson["status"].toString() == "success") {
QJsonValue jsonData = responseJson["data"];
if (jsonData.isObject()) {
auto ownerJson = jsonData["transfer_recipient_key"];
if (ownerJson.isString()) {
ownerPublicKey = ownerJson.toString();
}
auto transferStatusJson = jsonData["transfer_status"];
if (transferStatusJson.isArray() && transferStatusJson.toArray()[0].toString() == "confirmed") {
ownerValid = true;
}
}
if (ownerValid && !ownerPublicKey.isEmpty()) {
if (ownerPublicKey.startsWith("-----BEGIN ")){
_ownerPublicKey = ownerPublicKey;
} else {
_ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n"
+ ownerPublicKey
+ "\n-----END PUBLIC KEY-----\n";
}
sendOwnerChallenge();
_verifyState = challengeClient;
} else {
_verifyState = error;
}
} else {
qCDebug(avatars) << "Get owner status failed for " << getDisplayName() << _marketplaceIdFromURL <<
"message:" << responseJson["message"].toString();
_verifyState = error;
}
_pendingEvent = false;
break;
}
case challengeResponse:
{
if (_challengeResponse.length() < 8) {
_verifyState = error;
_pendingEvent = false;
break;
}
int avatarIDLength;
int signedNonceLength;
{
QDataStream responseStream(_challengeResponse);
responseStream.setByteOrder(QDataStream::LittleEndian);
responseStream >> avatarIDLength >> signedNonceLength;
}
QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength);
QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength);
bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
QByteArray::fromBase64(signedNonce));
_verifyState = challengeResult ? verificationSucceeded : verificationFailed;
_needsIdentityUpdate = true;
if (_verifyState == verificationFailed) {
qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID();
} else {
qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID();
}
_pendingEvent = false;
break;
}
case requestingOwner:
{ // Qt networking done on this thread:
QCoreApplication::processEvents();
break;
}
default:
qCDebug(avatars) << "Unexpected verify state" << _verifyState;
break;
} // close switch
}
void MixerAvatar::sendOwnerChallenge() {
auto nodeList = DependencyManager::get<NodeList>();
QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
QByteArray nonce = QUuid::createUuid().toByteArray();
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
2 * sizeof(int) + nonce.length() + avatarID.length(), true);
challengeOwnershipPacket->writePrimitive(avatarID.length());
challengeOwnershipPacket->writePrimitive(nonce.length());
challengeOwnershipPacket->write(avatarID);
challengeOwnershipPacket->write(nonce);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
nonceHash.addData(nonce);
_challengeNonceHash = nonceHash.result();
static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
_challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS);
_challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() {
_verifyState = verificationFailed;
_needsIdentityUpdate = true;
});
}
void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) {
QByteArray avatarID;
QByteArray encryptedNonce;
QMutexLocker certifyLocker(&_avatarCertifyLock);
if (_verifyState == challengeClient) {
_challengeTimeout.stop();
_challengeResponse = response->readAll();
_verifyState = challengeResponse;
_pendingEvent = true;
}
}

View file

@ -17,14 +17,55 @@
#include <AvatarData.h>
class ResourceRequest;
class MixerAvatar : public AvatarData {
public:
bool getNeedsHeroCheck() const { return _needsHeroCheck; }
void setNeedsHeroCheck(bool needsHeroCheck = true)
{ _needsHeroCheck = needsHeroCheck; }
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; }
bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; }
void processCertifyEvents();
void handleChallengeResponse(ReceivedMessage* response);
private:
bool _needsHeroCheck { false };
// Avatar certification/verification:
enum VerifyState { nonCertified, requestingFST, receivedFST, staticValidation, requestingOwner, ownerResponse,
challengeClient, challengeResponse, verified, verificationFailed, verificationSucceeded, error };
Q_ENUM(VerifyState);
VerifyState _verifyState { nonCertified };
std::atomic<bool> _pendingEvent { false };
QMutex _avatarCertifyLock;
ResourceRequest* _avatarRequest { nullptr };
QString _marketplaceIdFromURL;
QString _marketplaceIdFromFST;
QByteArray _avatarFSTContents;
QByteArray _certificateHash;
QString _certificateIdFromURL;
QString _certificateIdFromFST;
QString _dynamicMarketResponse;
QString _ownerPublicKey;
QByteArray _challengeNonceHash;
QByteArray _challengeResponse;
QTimer _challengeTimeout;
bool _needsIdentityUpdate { false };
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey);
QByteArray canonicalJson(const QString fstFile);
void sendOwnerChallenge();
static const QString VERIFY_FAIL_MODEL;
private slots:
void fstRequestComplete();
void ownerRequestComplete();
};
using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

View file

@ -0,0 +1,86 @@
name = mannequin2
type = body+head
scale = 1
filename = mannequin/man_stolen.fbx
texdir = textures
joint = jointEyeLeft = LeftEye
joint = jointRightHand = RightHand
joint = jointHead = Head
joint = jointEyeRight = RightEye
joint = jointNeck = Neck
joint = jointLeftHand = LeftHand
joint = jointLean = Spine
joint = jointRoot = Hips
freeJoint = LeftArm
freeJoint = LeftForeArm
freeJoint = RightArm
freeJoint = RightForeArm
jointIndex = RightHandPinky2 = 19
jointIndex = LeftHandPinky3 = 44
jointIndex = RightToeBase = 9
jointIndex = LeftHandRing4 = 49
jointIndex = LeftHandPinky1 = 42
jointIndex = LeftHandRing1 = 46
jointIndex = LeftLeg = 2
jointIndex = RightHandIndex4 = 29
jointIndex = LeftHandRing3 = 48
jointIndex = RightShoulder = 14
jointIndex = RightArm = 15
jointIndex = Neck = 62
jointIndex = RightHandMiddle2 = 35
jointIndex = HeadTop_End = 66
jointIndex = LeftHandRing2 = 47
jointIndex = RightHandThumb1 = 30
jointIndex = RightHandRing3 = 24
jointIndex = LeftHandIndex3 = 52
jointIndex = LeftForeArm = 40
jointIndex = face = 68
jointIndex = LeftToe_End = 5
jointIndex = RightHandThumb3 = 32
jointIndex = RightEye = 65
jointIndex = Spine = 11
jointIndex = LeftEye = 64
jointIndex = LeftToeBase = 4
jointIndex = LeftHandIndex4 = 53
jointIndex = RightHandPinky4 = 21
jointIndex = RightHandMiddle1 = 34
jointIndex = Spine1 = 12
jointIndex = LeftHandIndex2 = 51
jointIndex = RightToe_End = 10
jointIndex = RightHand = 17
jointIndex = LeftUpLeg = 1
jointIndex = RightHandRing1 = 22
jointIndex = RightUpLeg = 6
jointIndex = RightHandMiddle4 = 37
jointIndex = Head = 63
jointIndex = RightHandMiddle3 = 36
jointIndex = RightHandIndex1 = 26
jointIndex = LeftHandMiddle4 = 61
jointIndex = LeftHandPinky4 = 45
jointIndex = Hips = 0
jointIndex = body = 67
jointIndex = RightHandThumb2 = 31
jointIndex = LeftHandThumb2 = 55
jointIndex = RightHandThumb4 = 33
jointIndex = RightHandPinky3 = 20
jointIndex = LeftHandPinky2 = 43
jointIndex = LeftShoulder = 38
jointIndex = RightHandIndex3 = 28
jointIndex = LeftHandThumb4 = 57
jointIndex = RightLeg = 7
jointIndex = RightHandIndex2 = 27
jointIndex = LeftHandMiddle3 = 60
jointIndex = RightHandRing4 = 25
jointIndex = LeftHandThumb1 = 54
jointIndex = LeftArm = 39
jointIndex = LeftHandThumb3 = 56
jointIndex = LeftHandMiddle1 = 58
jointIndex = RightHandPinky1 = 18
jointIndex = Spine2 = 13
jointIndex = RightHandRing2 = 23
jointIndex = RightForeArm = 16
jointIndex = LeftHandIndex1 = 50
jointIndex = RightFoot = 8
jointIndex = LeftHandMiddle2 = 59
jointIndex = LeftHand = 41
jointIndex = LeftFoot = 3

View file

@ -3390,6 +3390,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get<KeyboardScriptingInterface>().data());
if (setAdditionalContextProperties) {
qDebug() << "setting additional context properties!";
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
auto flags = tabletScriptingInterface->getFlags();

View file

@ -46,6 +46,7 @@
#include "MyAvatar.h"
#include "DebugDraw.h"
#include "SceneScriptingInterface.h"
#include "ui/AvatarCertifyBanner.h"
// 50 times per second - target is 45hz, but this helps account for any small deviations
// in the update loop - this also results in ~30hz when in desktop mode which is essentially
@ -178,6 +179,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) {
_lastSendAvatarDataTime = now;
_myAvatarSendRate.increment();
}
static AvatarCertifyBanner theftBanner;
if (_myAvatar->isCertifyFailed()) {
theftBanner.show(_myAvatar->getSessionUUID());
} else {
theftBanner.clear();
}
}

View file

@ -0,0 +1,76 @@
//
// AvatarCertifyBanner.h
// interface/src/ui
//
// Created by Simon Walton, April 2019
// Copyright 2019 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 "AvatarCertifyBanner.h"
#include <QtGui/QDesktopServices>
#include "ui/TabletScriptingInterface.h"
#include "EntityTreeRenderer.h"
namespace {
const QUrl AVATAR_THEFT_BANNER_IMAGE = PathUtils::resourcesUrl("images/AvatarTheftBanner.png");
const QString AVATAR_THEFT_BANNER_SCRIPT { "/system/clickToAvatarApp.js" };
}
AvatarCertifyBanner::AvatarCertifyBanner(QQuickItem* parent) {
}
void AvatarCertifyBanner::show(const QUuid& avatarID) {
if (!_active) {
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = entityTreeRenderer->getTree();
if (!entityTree) {
return;
}
const bool tabletShown = DependencyManager::get<TabletScriptingInterface>()->property("tabletShown").toBool();
const auto& position = tabletShown ? glm::vec3(0.0f, 0.0f, -1.8f) : glm::vec3(0.0f, 0.0f, -0.7f);
const float scaleFactor = tabletShown ? 2.6f : 1.0f;
EntityItemProperties entityProperties;
entityProperties.setType(EntityTypes::Image);
entityProperties.setEntityHostType(entity::HostType::LOCAL);
entityProperties.setImageURL(AVATAR_THEFT_BANNER_IMAGE.toString());
entityProperties.setName("hifi-avatar-notification-banner");
entityProperties.setParentID(avatarID);
entityProperties.setParentJointIndex(CAMERA_MATRIX_INDEX);
entityProperties.setLocalPosition(position);
entityProperties.setDimensions(glm::vec3(1.0f, 1.0f, 0.3f) * scaleFactor);
entityProperties.setRenderLayer(tabletShown ? RenderLayer::WORLD : RenderLayer::FRONT);
entityProperties.getGrab().setGrabbable(false);
QString scriptPath = QUrl(PathUtils::defaultScriptsLocation("")).toString() + AVATAR_THEFT_BANNER_SCRIPT;
entityProperties.setScript(scriptPath);
entityProperties.setVisible(true);
entityTree->withWriteLock([&] {
auto entityTreeItem = entityTree->addEntity(_bannerID, entityProperties);
entityTreeItem->setLocalPosition(position);
});
_active = true;
}
}
void AvatarCertifyBanner::clear() {
if (_active) {
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = entityTreeRenderer->getTree();
if (!entityTree) {
return;
}
entityTree->withWriteLock([&] {
entityTree->deleteEntity(_bannerID);
});
_active = false;
}
}

View file

@ -0,0 +1,34 @@
//
// AvatarCertifyBanner.h
// interface/src/ui
//
// Created by Simon Walton, April 2019
// Copyright 2019 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_AvatarCertifyBanner_h
#define hifi_AvatarCertifyBanner_h
#include <QUuid>
#include "OffscreenQmlElement.h"
#include "EntityItemID.h"
class EntityItemID;
class AvatarCertifyBanner : QObject {
Q_OBJECT
HIFI_QML_DECL
public:
AvatarCertifyBanner(QQuickItem* parent = nullptr);
void show(const QUuid& avatarID);
void clear();
private:
const EntityItemID _bannerID { QUuid::createUuid() };
bool _active { false };
};
#endif // hifi_AvatarCertifyBanner_h

View file

@ -1955,8 +1955,7 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
>> identity.attachmentData
>> identity.displayName
>> identity.sessionDisplayName
>> identity.isReplicated
>> identity.lookAtSnappingEnabled
>> identity.identityFlags
;
if (incomingSequenceNumber > _identitySequenceNumber) {
@ -1971,8 +1970,22 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
}
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
if (identity.isReplicated != _isReplicated) {
_isReplicated = identity.isReplicated;
bool flagValue;
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::isReplicated);
if ( flagValue != _isReplicated) {
_isReplicated = flagValue;
identityChanged = true;
}
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::lookAtSnapping);
if ( flagValue != _lookAtSnappingEnabled) {
setProperty("lookAtSnappingEnabled", flagValue);
identityChanged = true;
}
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::verificationFailed);
if (flagValue != _verificationFailed) {
_verificationFailed = flagValue;
identityChanged = true;
}
@ -1981,11 +1994,6 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
identityChanged = true;
}
if (identity.lookAtSnappingEnabled != _lookAtSnappingEnabled) {
setProperty("lookAtSnappingEnabled", identity.lookAtSnappingEnabled);
identityChanged = true;
}
#ifdef WANT_DEBUG
qCDebug(avatars) << __FUNCTION__
<< "identity.uuid:" << identity.uuid
@ -2195,17 +2203,27 @@ void AvatarData::prepareResetTraitInstances() {
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
QByteArray identityData;
QDataStream identityStream(&identityData, QIODevice::Append);
using namespace AvatarDataPacket;
// when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received
// whereas agents send a fresh outgoing sequence number when identity data has changed
IdentityFlags identityFlags = IdentityFlag::none;
if (_isReplicated || setIsReplicated) {
identityFlags.setFlag(IdentityFlag::isReplicated);
}
if (_lookAtSnappingEnabled) {
identityFlags.setFlag(IdentityFlag::lookAtSnapping);
}
if (isCertifyFailed()) {
identityFlags.setFlag(IdentityFlag::verificationFailed);
}
identityStream << getSessionUUID()
<< (udt::SequenceNumber::Type) _identitySequenceNumber
<< _attachmentData
<< _displayName
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
<< (_isReplicated || setIsReplicated)
<< _lookAtSnappingEnabled;
<< identityFlags;
return identityData;
}

View file

@ -378,6 +378,10 @@ namespace AvatarDataPacket {
static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE;
// AvatarIdentity packet:
enum class IdentityFlag: quint32 {none, isReplicated = 0x1, lookAtSnapping = 0x2, verificationFailed = 0x4};
Q_DECLARE_FLAGS(IdentityFlags, IdentityFlag)
struct SendStatus {
HasFlags itemFlags { 0 };
bool sendUUID { false };
@ -1182,6 +1186,7 @@ public:
QString sessionDisplayName;
bool isReplicated;
bool lookAtSnappingEnabled;
AvatarDataPacket::IdentityFlags identityFlags;
};
// identityChanged returns true if identity has changed, false otherwise.
@ -1213,6 +1218,7 @@ public:
_sessionDisplayName = sessionDisplayName;
markIdentityDataChanged();
}
virtual bool isCertifyFailed() const { return _verificationFailed; }
/**jsdoc
* Gets information about the models currently attached to your avatar.
@ -1694,6 +1700,7 @@ protected:
QString _displayName;
QString _sessionDisplayName { };
bool _lookAtSnappingEnabled { true };
bool _verificationFailed { false };
quint64 _errorLogExpiry; ///< time in future when to log an error

View file

@ -23,6 +23,8 @@
#include "Profile.h"
static const QString VERIFY_FAIL_MODEL { "/meshes/verifyFailed.fst" };
void AvatarReplicas::addReplica(const QUuid& parentID, AvatarSharedPointer replica) {
if (parentID == QUuid()) {
return;
@ -324,6 +326,10 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
bool displayNameChanged = false;
// In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged);
if (avatar->isCertifyFailed() && identityUUID != EMPTY) {
qCDebug(avatars) << "Avatar" << avatar->getSessionDisplayName() << "marked as VERIFY-FAILED";
avatar->setSkeletonModelURL(PathUtils::resourcesUrl(VERIFY_FAIL_MODEL));
}
_replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged);
}
}

View file

@ -38,10 +38,10 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums);
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerSection);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendVerificationFailed);
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerSection);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendVerificationFailed);
case PacketType::MessagesData:
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
// ICE packets

View file

@ -333,6 +333,7 @@ enum class AvatarMixerPacketVersion : PacketVersion {
SendMaxTranslationDimension,
FBXJointOrderChange,
HandControllerSection,
SendVerificationFailed
};
enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -0,0 +1,7 @@
(function () {
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
this.clickDownOnEntity = function (entityID, mouseEvent) {
tablet.loadQMLSource("hifi/AvatarApp.qml");
};
}
);