diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index ac633d9388..01f3454292 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -32,24 +32,41 @@ MixerAvatar::MixerAvatar() { _challengeTimer.setSingleShot(true); _challengeTimer.setInterval(CHALLENGE_TIMEOUT_MS); - - _challengeTimer.callOnTimeout(this, [this]() { - if (_verifyState == challengeClient) { - _pendingEvent = false; - _verifyState = verificationFailed; - _needsIdentityUpdate = true; - qCDebug(avatars) << "Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID(); - } else { - qCDebug(avatars) << "Ignoring timeout of avatar challenge"; - } - }); - + _challengeTimer.callOnTimeout(this, &MixerAvatar::challengeTimeout); + // QTimer::start is a set of overloaded functions. + connect(this, &MixerAvatar::startChallengeTimer, &_challengeTimer, static_cast(&QTimer::start)); } const char* MixerAvatar::stateToName(VerifyState state) { return QMetaEnum::fromType().valueToKey(state); } +void MixerAvatar::challengeTimeout() { + switch (_verifyState) { + case challengeClient: + _verifyState = staticValidation; + _pendingEvent = true; + if (++_numberChallenges < NUM_CHALLENGES_BEFORE_FAIL) { + qCDebug(avatars) << "Retrying (" << _numberChallenges << ") timed-out challenge for" << getDisplayName() + << getSessionUUID(); + } else { + _certifyFailed = true; + _needsIdentityUpdate = true; + qCDebug(avatars) << "Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID(); + } + break; + + case verificationFailed: + qCDebug(avatars) << "Retrying failed challenge for" << getDisplayName() << getSessionUUID(); + _verifyState = staticValidation; + _pendingEvent = true; + break; + + default: + qCDebug(avatars) << "Ignoring timeout of avatar challenge"; + } +} + void MixerAvatar::fetchAvatarFST() { if (_verifyState >= requestingFST && _verifyState <= challengeClient) { qCDebug(avatars) << "WARNING: Avatar verification restarted; old state:" << stateToName(_verifyState); @@ -210,6 +227,23 @@ void MixerAvatar::ownerRequestComplete() { networkReply->deleteLater(); } +void MixerAvatar::requestCurrentOwnership() { + // Get registered owner's public key from metaverse. + 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; + QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete); +} + void MixerAvatar::processCertifyEvents() { if (!_pendingEvent) { return; @@ -221,26 +255,15 @@ void MixerAvatar::processCertifyEvents() { case receivedFST: { generateFSTHash(); + _numberChallenges = 0; 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; + requestCurrentOwnership(); _verifyState = requestingOwner; - QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); - connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete); } else { _needsIdentityUpdate = true; _pendingEvent = false; @@ -253,6 +276,13 @@ void MixerAvatar::processCertifyEvents() { break; } + case staticValidation: + { + requestCurrentOwnership(); + _verifyState = requestingOwner; + break; + } + case ownerResponse: { QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8()); @@ -325,8 +355,7 @@ void MixerAvatar::sendOwnerChallenge() { _challengeNonceHash = nonceHash.result(); _pendingEvent = false; - // QTimer::start is a set of overloaded functions. - QMetaObject::invokeMethod(&_challengeTimer, static_cast(&QTimer::start)); + emit startChallengeTimer(); } void MixerAvatar::processChallengeResponse(ReceivedMessage& response) { @@ -337,7 +366,7 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) { QByteArray responseData = response.readAll(); if (responseData.length() < 8) { _verifyState = error; - qCDebug(avatars) << "Avatar challenge response packet too small, length:" << responseData.length(); + qCWarning(avatars) << "ALERT: Avatar challenge response packet too small, length:" << responseData.length(); return; } @@ -354,9 +383,11 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) { bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash, QByteArray::fromBase64(signedNonce)); _verifyState = challengeResult ? verificationSucceeded : verificationFailed; + _certifyFailed = !challengeResult; _needsIdentityUpdate = true; - if (_verifyState == verificationFailed) { + if (_certifyFailed) { qCDebug(avatars) << "Dynamic verification FAILED for" << getDisplayName() << getSessionUUID(); + emit startChallengeTimer(); } else { qCDebug(avatars) << "Dynamic verification SUCCEEDED for" << getDisplayName() << getSessionUUID(); } diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 39095def50..bb6b859984 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -27,7 +27,7 @@ public: void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } void fetchAvatarFST(); - virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; } + virtual bool isCertifyFailed() const override { return _certifyFailed; } bool needsIdentityUpdate() const { return _needsIdentityUpdate; } void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; } @@ -60,11 +60,15 @@ private: QString _ownerPublicKey; QByteArray _challengeNonceHash; QTimer _challengeTimer; + static constexpr int NUM_CHALLENGES_BEFORE_FAIL = 1; + int _numberChallenges { 0 }; + bool _certifyFailed { false }; bool _needsIdentityUpdate { false }; bool generateFSTHash(); bool validateFSTHash(const QString& publicKey) const; QByteArray canonicalJson(const QString fstFile); + void requestCurrentOwnership(); void sendOwnerChallenge(); static const QString VERIFY_FAIL_MODEL; @@ -72,6 +76,10 @@ private: private slots: void fstRequestComplete(); void ownerRequestComplete(); + void challengeTimeout(); + + signals: + void startChallengeTimer(); }; using MixerAvatarSharedPointer = std::shared_ptr;