diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 2c8f8a9e37..fa9c73b12d 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -16,6 +16,10 @@ #include #include #include +#include +#include +#include +#include #include "AssignmentParentFinder.h" #include "EntityNodeData.h" @@ -29,15 +33,19 @@ const char* LOCAL_MODELS_PERSIST_FILE = "resources/models.svo"; EntityServer::EntityServer(ReceivedMessage& message) : OctreeServer(message), - _entitySimulation(NULL) + _entitySimulation(NULL), + _dynamicDomainVerificationTimer(this) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase, PacketType::EntityPhysics }, + packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase, PacketType::EntityPhysics, PacketType::ChallengeOwnership }, this, "handleEntityPacket"); + + connect(&_dynamicDomainVerificationTimer, &QTimer::timeout, this, &EntityServer::startDynamicDomainVerification); + _dynamicDomainVerificationTimer.setSingleShot(true); } EntityServer::~EntityServer() { @@ -93,6 +101,9 @@ void EntityServer::beforeRun() { connect(_pruneDeletedEntitiesTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedEntities())); const int PRUNE_DELETED_MODELS_INTERVAL_MSECS = 1 * 1000; // once every second _pruneDeletedEntitiesTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS); + + DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); + connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &EntityServer::domainSettingsRequestFailed); } void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) { @@ -296,6 +307,18 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME); } + int minTime; + if (readOptionInt("dynamicDomainVerificationTimeMin", settingsSectionObject, minTime)) { + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = minTime * 1000; + } + + int maxTime; + if (readOptionInt("dynamicDomainVerificationTimeMax", settingsSectionObject, maxTime)) { + _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = maxTime * 1000; + } + + startDynamicDomainVerification(); + tree->setWantEditLogging(wantEditLogging); tree->setWantTerseEditLogging(wantTerseEditLogging); @@ -410,3 +433,79 @@ QString EntityServer::serverSubclassStats() { return statsString; } + +void EntityServer::domainSettingsRequestFailed() { + auto nodeList = DependencyManager::get(); + qCDebug(entities) << "The EntityServer couldn't get the Domain Settings. Starting dynamic domain verification with default values..."; + + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; + _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; + startDynamicDomainVerification(); +} + +void EntityServer::startDynamicDomainVerification() { + qCDebug(entities) << "Starting Dynamic Domain Verification..."; + + QString thisDomainID = DependencyManager::get()->getDomainId().remove(QRegExp("\\{|\\}")); + + EntityTreePointer tree = std::static_pointer_cast(_tree); + QHash localMap(tree->getEntityCertificateIDMap()); + + QHashIterator i(localMap); + qCDebug(entities) << localMap.size() << "entities in _entityCertificateIDMap"; + while (i.hasNext()) { + i.next(); + + EntityItemPointer entity = tree->findEntityByEntityItemID(i.value()); + + if (entity) { + if (!entity->verifyStaticCertificateProperties()) { + qCDebug(entities) << "During Dynamic Domain Verification, a certified entity with ID" << i.value() << "failed" + << "static certificate verification."; + // Delete the entity if it doesn't pass static certificate verification + tree->deleteEntity(i.value(), true); + } else { + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); + QJsonObject request; + request["certificate_id"] = i.key(); + networkRequest.setUrl(requestURL); + + QNetworkReply* networkReply = NULL; + networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + + connect(networkReply, &QNetworkReply::finished, [=]() { + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + + if (networkReply->error() == QNetworkReply::NoError) { + if (jsonObject["domain_id"].toString() != thisDomainID) { + qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() + << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << i.value(); + tree->deleteEntity(i.value(), true); + } else { + qCDebug(entities) << "Entity passed dynamic domain verification:" << i.value(); + } + } else { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << i.value() + << "More info:" << jsonObject; + tree->deleteEntity(i.value(), true); + } + + networkReply->deleteLater(); + }); + } + } else { + qCWarning(entities) << "During DDV, an entity with ID" << i.value() << "was NOT found in the Entity Tree!"; + } + } + + int nextInterval = qrand() % ((_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS + 1) - _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS) + _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; + qCDebug(entities) << "Restarting Dynamic Domain Verification timer for" << nextInterval / 1000 << "seconds"; + _dynamicDomainVerificationTimer.start(nextInterval); +} diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 26c2f149aa..05404b28c8 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -73,6 +73,7 @@ protected: private slots: void handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode); + void domainSettingsRequestFailed(); private: SimpleEntitySimulationPointer _entitySimulation; @@ -80,6 +81,13 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; + + static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m + static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h + int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m + int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h + QTimer _dynamicDomainVerificationTimer; + void startDynamicDomainVerification(); }; #endif // hifi_EntityServer_h diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index 04409b3b21..3f835678ac 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -92,7 +92,11 @@ void OctreeInboundPacketProcessor::processPacket(QSharedPointer // Ask our tree subclass if it can handle the incoming packet... PacketType packetType = message->getType(); - if (_myServer->getOctree()->handlesEditPacketType(packetType)) { + if (packetType == PacketType::ChallengeOwnership) { + _myServer->getOctree()->withWriteLock([&] { + _myServer->getOctree()->processChallengeOwnershipPacket(*message, sendingNode); + }); + } else if (_myServer->getOctree()->handlesEditPacketType(packetType)) { PerformanceWarning warn(debugProcessPacket, "processPacket KNOWN TYPE", debugProcessPacket); _receivedPacketCount++; diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 4a1aade59d..c535c48dda 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 19f1718370..cade6b25dc 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.8, + "version": 1.9, "settings": [ { "name": "metaverse", @@ -207,7 +207,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which types of users can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ @@ -216,8 +216,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -253,6 +253,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -283,7 +297,7 @@ } ], "non-deletable-row-key": "permissions_id", - "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] + "non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ] }, { "name": "group_permissions", @@ -300,8 +314,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -362,6 +376,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -383,7 +411,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -407,8 +435,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -466,6 +494,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -487,7 +529,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -507,8 +549,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -544,6 +586,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -565,7 +621,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -585,8 +641,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -622,6 +678,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -643,7 +713,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -663,8 +733,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -700,6 +770,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -721,7 +805,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -741,8 +825,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 8 + "label": "Permissions ?", + "span": 10 } ], "columns": [ @@ -778,6 +862,20 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_certified", + "label": "Rez Certified", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp_certified", + "label": "Rez Temporary Certified", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_write_to_asset_server", "label": "Write Assets", @@ -799,7 +897,7 @@ "editable": true, "default": false }, - { + { "name": "id_can_replace_content", "label": "Replace Content", "type": "checkbox", @@ -841,7 +939,7 @@ { "name": "asset_server", "label": "Asset Server (ATP)", - "assignment-types": [3], + "assignment-types": [ 3 ], "settings": [ { "name": "enabled", @@ -872,7 +970,7 @@ { "name": "entity_script_server", "label": "Entity Script Server (ESS)", - "assignment-types": [5], + "assignment-types": [ 5 ], "settings": [ { "name": "entity_pps_per_script", @@ -895,7 +993,7 @@ { "name": "avatars", "label": "Avatars", - "assignment-types": [1, 2], + "assignment-types": [ 1, 2 ], "settings": [ { "name": "min_avatar_scale", @@ -934,7 +1032,7 @@ { "name": "audio_threading", "label": "Audio Threading", - "assignment-types": [0], + "assignment-types": [ 0 ], "settings": [ { "name": "auto_threads", @@ -957,7 +1055,7 @@ { "name": "audio_env", "label": "Audio Environment", - "assignment-types": [0], + "assignment-types": [ 0 ], "settings": [ { "name": "attenuation_per_doubling_in_distance", @@ -1164,6 +1262,22 @@ "default": "3600", "advanced": true }, + { + "name": "dynamicDomainVerificationTimeMin", + "label": "Dynamic Domain Verification Time (seconds) - Minimum", + "help": "The lower limit on the amount of time that passes before Dynamic Domain Verification on entities occurs. Units are seconds.", + "placeholder": "2700", + "default": "2700", + "advanced": true + }, + { + "name": "dynamicDomainVerificationTimeMax", + "label": "Dynamic Domain Verification Time (seconds) - Maximum", + "help": "The upper limit on the amount of time that passes before Dynamic Domain Verification on entities occurs. Units are seconds.", + "placeholder": "3600", + "default": "3600", + "advanced": true + }, { "name": "entityScriptSourceWhitelist", "label": "Entity Scripts Allowed from:", diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 14347b6be1..c4da828bb1 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1,5 +1,7 @@ var Settings = { showAdvanced: false, + // STABLE METAVERSE_URL: https://metaverse.highfidelity.com + // STAGING METAVERSE_URL: https://staging.highfidelity.com METAVERSE_URL: 'https://metaverse.highfidelity.com', ADVANCED_CLASS: 'advanced-setting', DEPRECATED_CLASS: 'deprecated-setting', diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index c19e16cd36..60d2bacc62 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -87,7 +87,7 @@ Window { onReceivedHifiSchemeURL: resetAfterTeleport(); // Update location after using back and forward buttons. - onMetaverseServerUrlChanged: updateLocationTextTimer.start(); + onHostChanged: updateLocationTextTimer.start(); ListModel { id: suggestions } diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controls-uit/Key.qml index eda9c03fa2..e54250c872 100644 --- a/interface/resources/qml/controls-uit/Key.qml +++ b/interface/resources/qml/controls-uit/Key.qml @@ -55,8 +55,21 @@ Item { mouse.accepted = true; } + property var _HAPTIC_STRENGTH: 0.1; + property var _HAPTIC_DURATION: 3.0; + property var leftHand: 0; + property var rightHand: 1; + onEntered: { keyItem.state = "mouseOver"; + + var globalPosition = keyItem.mapToGlobal(mouseArea1.mouseX, mouseArea1.mouseY); + var deviceId = Web3DOverlay.deviceIdByTouchPoint(globalPosition.x, globalPosition.y); + var hand = deviceId - 1; // based on touchEventUtils.js, deviceId is 'hand + 1', so 'hand' is 'deviceId' - 1 + + if (hand == leftHand || hand == rightHand) { + Controller.triggerHapticPulse(_HAPTIC_STRENGTH, _HAPTIC_DURATION, hand); + } } onExited: { diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index e2a012ad46..5949adffca 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -33,7 +33,7 @@ Window { BaseWebView { id: webview - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + url: Account.metaverseServerURL + "/marketplace?category=avatars" focus: true anchors { diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 65517f5a73..fcfff02b72 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -17,7 +17,7 @@ import "../styles-uit" import "../controls-uit" as HifiControls import "toolbars" -// references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager from root context +// references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager, Account from root context Item { id: thisNameCard @@ -30,7 +30,6 @@ Item { // Properties property string profileUrl: ""; - property string defaultBaseUrl: AddressManager.metaverseServerUrl; property string connectionStatus : "" property string uuid: "" property string displayName: "" @@ -59,7 +58,7 @@ Item { clip: true Image { id: userImage - source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (defaultBaseUrl + profileUrl)) : ""; + source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (Account.metaverseServerURL + profileUrl)) : ""; mipmap: true; // Anchors anchors.fill: parent @@ -95,7 +94,7 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = defaultBaseUrl + "/users/" + userName; + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } onEntered: infoHoverImage.visible = true; @@ -366,7 +365,7 @@ Item { enabled: selected hoverEnabled: true onClicked: { - userInfoViewer.url = defaultBaseUrl + "/users/" + userName; + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } onEntered: { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index c98cfba1ba..89f18e5ce2 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -23,9 +23,6 @@ import "../controls" as HifiControls Rectangle { id: pal; - // Size - width: parent.width; - height: parent.height; // Style color: "#E3E3E3"; // Properties diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 8ea9ce494c..dfe0c319e5 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -585,7 +585,7 @@ Rectangle { // "Rez" button HifiControlsUit.Button { id: rezNowButton; - enabled: root.canRezCertifiedItems; + enabled: root.canRezCertifiedItems || root.isWearable; buttonGlyph: hifi.glyphs.lightning; color: hifi.buttons.red; colorScheme: hifi.colorSchemes.light; @@ -634,7 +634,7 @@ Rectangle { } RalewaySemiBold { id: explainRezText; - //visible: !root.isWearable; + visible: !root.isWearable; text: 'What does "Rez" mean?' // Text size size: 16; diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index cc316a70e9..8b3f017fd2 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -25,7 +25,7 @@ Item { HifiConstants { id: hifi; } id: root; - property string referrerURL: "https://metaverse.highfidelity.com/marketplace?"; + property string referrerURL: (Account.metaverseServerURL + "/marketplace?"); readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; property alias usernameDropdownVisible: usernameDropdown.visible; diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index e8752b01e7..4d9a83817a 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -134,7 +134,7 @@ StackView { bottom: parent.bottom } - onMetaverseServerUrlChanged: updateLocationTextTimer.start(); + onHostChanged: updateLocationTextTimer.start(); Rectangle { id: navBar width: parent.width diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml index cab76bf818..34d587a3f5 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml @@ -31,7 +31,7 @@ Item { BaseWebView { id: webview - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + url: (Account.metaverseServerURL + "/marketplace?category=avatars)" focus: true anchors { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f2bb52ea47..4f051697ad 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1018,6 +1018,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // connect to the packet sent signal of the _entityEditSender connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); + connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); const char** constArgv = const_cast(argv); QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); @@ -5739,6 +5740,11 @@ int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer void Application::packetSent(quint64 length) { } +void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { + auto ledger = DependencyManager::get(); + ledger->updateLocation(certificateID, placeName); +} + void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { scriptEngine->setEmitScriptUpdatesFunction([this]() { diff --git a/interface/src/Application.h b/interface/src/Application.h index ab7acc8166..fbfb3979be 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -428,6 +428,7 @@ private slots: void nodeActivated(SharedNodePointer node); void nodeKilled(SharedNodePointer node); static void packetSent(quint64 length); + static void addingEntityWithCertificate(const QString& certificateID, const QString& placeName); void updateDisplayMode(); void domainConnectionRefused(const QString& reasonMessage, int reason, const QString& extraInfo); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 80e599fb24..904847cb5f 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -220,18 +220,26 @@ void Ledger::account() { } // The api/failResponse is called just for the side effect of logging. -void Ledger::updateLocationSuccess(QNetworkReply& reply) { apiResponse("reset", reply); } -void Ledger::updateLocationFailure(QNetworkReply& reply) { failResponse("reset", reply); } +void Ledger::updateLocationSuccess(QNetworkReply& reply) { apiResponse("updateLocation", reply); } +void Ledger::updateLocationFailure(QNetworkReply& reply) { failResponse("updateLocation", reply); } void Ledger::updateLocation(const QString& asset_id, const QString location, const bool controlledFailure) { auto wallet = DependencyManager::get(); - QStringList keys = wallet->listPublicKeys(); - QString key = keys[0]; - QJsonObject transaction; - transaction["asset_id"] = asset_id; - transaction["location"] = location; - QJsonDocument transactionDoc{ transaction }; - auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); - signedSend("transaction", transactionString, key, "location", "updateLocationSuccess", "updateLocationFailure", controlledFailure); + auto walletScriptingInterface = DependencyManager::get(); + uint walletStatus = walletScriptingInterface->getWalletStatus(); + + if (walletStatus != (uint)wallet->WALLET_STATUS_READY) { + emit walletScriptingInterface->walletNotSetup(); + qDebug(commerce) << "User attempted to update the location of a certificate, but their wallet wasn't ready. Status:" << walletStatus; + } else { + QStringList keys = wallet->listPublicKeys(); + QString key = keys[0]; + QJsonObject transaction; + transaction["certificate_id"] = asset_id; + transaction["place_name"] = location; + QJsonDocument transactionDoc{ transaction }; + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + signedSend("transaction", transactionString, key, "location", "updateLocationSuccess", "updateLocationFailure", controlledFailure); + } } void Ledger::certificateInfoSuccess(QNetworkReply& reply) { diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index faa95b2a40..c7c09d8b03 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -717,32 +717,52 @@ bool Wallet::changePassphrase(const QString& newPassphrase) { } void Wallet::handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - QString decryptedText; - quint64 encryptedTextSize; - quint64 publicKeySize; + unsigned char decryptedText[64]; + int certIDByteArraySize; + int encryptedTextByteArraySize; - if (verifyOwnerChallenge(packet->read(packet->readPrimitive(&encryptedTextSize)), packet->read(packet->readPrimitive(&publicKeySize)), decryptedText)) { - auto nodeList = DependencyManager::get(); - // setup the packet - auto decryptedTextPacket = NLPacket::create(PacketType::ChallengeOwnership, NUM_BYTES_RFC4122_UUID + decryptedText.size(), true); + packet->readPrimitive(&certIDByteArraySize); + packet->readPrimitive(&encryptedTextByteArraySize); - // write the decrypted text to the packet - decryptedTextPacket->write(decryptedText.toUtf8()); + QByteArray certID = packet->read(certIDByteArraySize); + QByteArray encryptedText = packet->read(encryptedTextByteArraySize); - qCDebug(commerce) << "Sending ChallengeOwnership Packet containing decrypted text"; + RSA* rsa = readKeys(keyFilePath().toStdString().c_str()); - nodeList->sendPacket(std::move(decryptedTextPacket), *sendingNode); + if (rsa) { + const int decryptionStatus = RSA_private_decrypt(encryptedTextByteArraySize, + reinterpret_cast(encryptedText.constData()), + decryptedText, + rsa, + RSA_PKCS1_OAEP_PADDING); + + RSA_free(rsa); + + if (decryptionStatus != -1) { + auto nodeList = DependencyManager::get(); + + QByteArray decryptedTextByteArray = QByteArray(reinterpret_cast(decryptedText), decryptionStatus); + int decryptedTextByteArraySize = decryptedTextByteArray.size(); + int certIDSize = certID.size(); + // setup the packet + auto decryptedTextPacket = NLPacket::create(PacketType::ChallengeOwnership, certIDSize + decryptedTextByteArraySize + 2 * sizeof(int), true); + + decryptedTextPacket->writePrimitive(certIDSize); + decryptedTextPacket->writePrimitive(decryptedTextByteArraySize); + decryptedTextPacket->write(certID); + decryptedTextPacket->write(decryptedTextByteArray); + + qCDebug(commerce) << "Sending ChallengeOwnership Packet containing decrypted text" << decryptedTextByteArray << "for CertID" << certID; + + nodeList->sendPacket(std::move(decryptedTextPacket), *sendingNode); + } else { + qCDebug(commerce) << "During entity ownership challenge, decrypting the encrypted text failed."; + } } else { - qCDebug(commerce) << "verifyOwnerChallenge() returned false"; + qCDebug(commerce) << "During entity ownership challenge, creating the RSA object failed."; } } -bool Wallet::verifyOwnerChallenge(const QByteArray& encryptedText, const QString& publicKey, QString& decryptedText) { - // I have no idea how to do this yet, so here's some dummy code that may not even work. - decryptedText = QString("hello"); - return true; -} - void Wallet::account() { auto ledger = DependencyManager::get(); ledger->account(); diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 31df2df918..ed145df451 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -82,8 +82,6 @@ private: bool readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); bool writeBackupInstructions(); - bool verifyOwnerChallenge(const QByteArray& encryptedText, const QString& publicKey, QString& decryptedText); - void account(); }; diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index 4f87e02dce..4ff250e926 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -25,6 +25,11 @@ class AccountScriptingInterface : public QObject { * @property username {String} username if user is logged in, otherwise it returns "Unknown user" */ +public: + + Q_PROPERTY(QUrl metaverseServerURL READ getMetaverseServerURL) + QUrl getMetaverseServerURL() { return DependencyManager::get()->getMetaverseServerURL(); } + signals: /**jsdoc diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index d7b59ba912..8b5e255b06 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -39,7 +39,7 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare }); _backEnabled = !(DependencyManager::get()->getBackStack().isEmpty()); _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); - connect(addressManager.data(), &AddressManager::hostChanged, this, &AddressBarDialog::metaverseServerUrlChanged); + connect(addressManager.data(), &AddressManager::hostChanged, this, &AddressBarDialog::hostChanged); connect(DependencyManager::get().data(), &DialogsManager::setUseFeed, this, &AddressBarDialog::setUseFeed); connect(qApp, &Application::receivedHifiSchemeURL, this, &AddressBarDialog::receivedHifiSchemeURL); } diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index 921e808abb..cab533cce3 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -22,7 +22,7 @@ class AddressBarDialog : public OffscreenQmlDialog { Q_PROPERTY(bool backEnabled READ backEnabled NOTIFY backEnabledChanged) Q_PROPERTY(bool forwardEnabled READ forwardEnabled NOTIFY forwardEnabledChanged) Q_PROPERTY(bool useFeed READ useFeed WRITE setUseFeed NOTIFY useFeedChanged) - Q_PROPERTY(QString metaverseServerUrl READ metaverseServerUrl NOTIFY metaverseServerUrlChanged) + Q_PROPERTY(QString metaverseServerUrl READ metaverseServerUrl) public: AddressBarDialog(QQuickItem* parent = nullptr); @@ -37,7 +37,7 @@ signals: void forwardEnabledChanged(); void useFeedChanged(); void receivedHifiSchemeURL(const QString& url); - void metaverseServerUrlChanged(); + void hostChanged(); protected: void displayAddressOfflineMessage(); diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 515215a8d3..7ce2a0146d 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include #include diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index b3bd6fbd34..023d3e6e17 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 0c75803d35..d16950b0eb 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -222,6 +222,8 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); + _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); @@ -250,6 +252,20 @@ void Web3DOverlay::onResizeWebSurface() { _webSurface->resize(QSize(_resolution.x, _resolution.y)); } +const int INVALID_DEVICE_ID = -1; + +Q_INVOKABLE int Web3DOverlay::deviceIdByTouchPoint(qreal x, qreal y) { + auto mapped = _webSurface->getRootItem()->mapFromGlobal(QPoint(x, y)); + + for (auto pair : _activeTouchPoints) { + if (mapped.x() == (int)pair.second.pos().x() && mapped.y() == (int)pair.second.pos().y()) { + return pair.first; + } + } + + return INVALID_DEVICE_ID; +} + void Web3DOverlay::render(RenderArgs* args) { if (!_visible || !getParentVisible()) { return; diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index c7f338f0e6..2fc63df76a 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -67,6 +67,8 @@ public: void destroyWebSurface(); void onResizeWebSurface(); + Q_INVOKABLE int deviceIdByTouchPoint(qreal x, qreal y); + public slots: void emitScriptEvent(const QVariant& scriptMessage); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f674a2206e..142e57c9e5 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -286,6 +286,13 @@ void Avatar::updateAvatarEntities() { properties.setScript(noScript); } + auto specifiedHref = properties.getHref(); + if (!isMyAvatar() && !specifiedHref.isEmpty()) { + qCDebug(avatars_renderer) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref; + QString noHref; + properties.setHref(noHref); + } + // When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed // they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear. // The thinking here is the local position was noticed as changing, but not the parentID (since it is now diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index f93b6a49ec..e82ed82093 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -18,6 +18,7 @@ #include "EntitiesLogging.h" #include "EntityItem.h" #include "EntityItemProperties.h" +#include EntityEditPacketSender::EntityEditPacketSender() { auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -93,9 +94,9 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0); bool success; + auto nodeList = DependencyManager::get(); if (properties.parentIDChanged() && properties.getParentID() == AVATAR_SELF_ID) { EntityItemProperties propertiesCopy = properties; - auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); propertiesCopy.setParentID(myNodeID); success = EntityItemProperties::encodeEntityEditPacket(type, entityItemID, propertiesCopy, bufferOut); @@ -110,6 +111,9 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, qCDebug(entities) << " properties:" << properties; #endif queueOctreeEditMessage(type, bufferOut); + if (type == PacketType::EntityAdd && !properties.getCertificateID().isEmpty()) { + emit addingEntityWithCertificate(properties.getCertificateID(), DependencyManager::get()->getPlaceName()); + } } } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 81efaa865c..bf79bb9203 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -43,6 +43,9 @@ public: virtual char getMyNodeType() const override { return NodeType::EntityServer; } virtual void adjustEditPacketForClockSkew(PacketType type, QByteArray& buffer, qint64 clockSkew) override; +signals: + void addingEntityWithCertificate(const QString& certificateID, const QString& placeName); + public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 008ec9769f..4f5db991c8 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -14,9 +14,14 @@ #include #include #include -#include // see comments for DEBUG_CERT +#include +#include #include #include +#include +#include +#include +#include #include @@ -41,6 +46,7 @@ int entityItemPointernMetaTypeId = qRegisterMetaType(); int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; +QString EntityItem::_marketplacePublicKey; EntityItem::EntityItem(const EntityItemID& entityItemID) : SpatiallyNestable(NestableType::Entity, entityItemID) @@ -1583,16 +1589,16 @@ QByteArray EntityItem::getStaticCertificateJSON() const { // It is important that this be reproducible in the same order each time. Since we also generate these on the server, we do it alphabetically // to help maintainence in two different code bases. if (!propertySet.getAnimation().getURL().isEmpty()) { - json["animation.url"] = propertySet.getAnimation().getURL(); + json["animationURL"] = propertySet.getAnimation().getURL(); } ADD_STRING_PROPERTY(collisionSoundURL, CollisionSoundURL); ADD_STRING_PROPERTY(compoundShapeURL, CompoundShapeURL); ADD_INT_PROPERTY(editionNumber, EditionNumber); - ADD_INT_PROPERTY(entityInstanceNumber, EntityInstanceNumber); + ADD_INT_PROPERTY(instanceNumber, EntityInstanceNumber); ADD_STRING_PROPERTY(itemArtist, ItemArtist); ADD_STRING_PROPERTY(itemCategories, ItemCategories); ADD_STRING_PROPERTY(itemDescription, ItemDescription); - ADD_STRING_PROPERTY(itemLicense, ItemLicense); + ADD_STRING_PROPERTY(itemLicenseUrl, ItemLicense); ADD_STRING_PROPERTY(itemName, ItemName); ADD_INT_PROPERTY(limitedRun, LimitedRun); ADD_STRING_PROPERTY(marketplaceID, MarketplaceID); @@ -1607,39 +1613,6 @@ QByteArray EntityItem::getStaticCertificateHash() const { return QCryptographicHash::hash(getStaticCertificateJSON(), QCryptographicHash::Sha256); } -#ifdef DEBUG_CERT -QString EntityItem::computeCertificateID() { - // Until the marketplace generates it, compute and answer the certificateID here. - // Does not set it, as that will have to be done from script engine in order to update server, etc. - const auto hash = getStaticCertificateHash(); - const auto text = reinterpret_cast(hash.constData()); - const unsigned int textLength = hash.length(); - - const char privateKey[] = "-----BEGIN RSA PRIVATE KEY-----\n\ -MIIBOQIBAAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7Zbi\n\ -MjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQJABvOlwhYwIhL+gr12jm2R\n\ -yPPzZ9nVEQ6kFxLlZfIT09119fd6OU1X5d4sHWfMfSIEgjwQIDS3ZU1kY3XKo87X\n\ -zQIhAOPHlYa1OC7BLhaTouy68qIU2vCKLP8mt4S31/TT0UOnAiEAxor6gU6yupTQ\n\ -yuyV3yHvr5LkZKBGqhjmOTmDfgtX7ncCIChGbgX3nQuHVOLhD/nTxHssPNozVGl5\n\ -KxHof+LmYSYZAiB4U+yEh9SsXdq40W/3fpLMPuNq1PRezJ5jGidGMcvF+wIgUNec\n\ -3Kg2U+CVZr8/bDT/vXRrsKj1zfobYuvbfVH02QY=\n\ ------END RSA PRIVATE KEY-----"; - BIO* bio = BIO_new_mem_buf((void*)privateKey, sizeof(privateKey)); - RSA* rsa = PEM_read_bio_RSAPrivateKey(bio, NULL, NULL, NULL); - - QByteArray signature(RSA_size(rsa), 0); - unsigned int signatureLength = 0; - const int signOK = RSA_sign(NID_sha256, text, textLength, reinterpret_cast(signature.data()), &signatureLength, rsa); - BIO_free(bio); - RSA_free(rsa); - if (!signOK) { - qCWarning(entities) << "Unable to compute signature for" << getName() << getEntityItemID(); - return ""; - } - return signature.toBase64(); -#endif -} - bool EntityItem::verifyStaticCertificateProperties() { // True IIF a non-empty certificateID matches the static certificate json. // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. @@ -1647,27 +1620,69 @@ bool EntityItem::verifyStaticCertificateProperties() { if (getCertificateID().isEmpty()) { return false; } - const auto signatureBytes = QByteArray::fromBase64(getCertificateID().toLatin1()); - const auto signature = reinterpret_cast(signatureBytes.constData()); - const unsigned int signatureLength = signatureBytes.length(); - const auto hash = getStaticCertificateHash(); - const auto text = reinterpret_cast(hash.constData()); - const unsigned int textLength = hash.length(); + const QByteArray marketplacePublicKeyByteArray = EntityItem::_marketplacePublicKey.toUtf8(); + const unsigned char* marketplacePublicKey = reinterpret_cast(marketplacePublicKeyByteArray.constData()); + int marketplacePublicKeyLength = marketplacePublicKeyByteArray.length(); - // After DEBUG_CERT ends, we will get/cache this once from the marketplace when needed, and it likely won't be RSA. - const char publicKey[] = "-----BEGIN PUBLIC KEY-----\n\ -MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgp\n\ -AqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ==\n\ ------END PUBLIC KEY-----"; - BIO *bio = BIO_new_mem_buf((void*)publicKey, sizeof(publicKey)); + BIO *bio = BIO_new_mem_buf((void*)marketplacePublicKey, marketplacePublicKeyLength); EVP_PKEY* evp_key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); - RSA* rsa = EVP_PKEY_get1_RSA(evp_key); - bool answer = RSA_verify(NID_sha256, text, textLength, signature, signatureLength, rsa); - BIO_free(bio); - RSA_free(rsa); - EVP_PKEY_free(evp_key); - return answer; + if (evp_key) { + RSA* rsa = EVP_PKEY_get1_RSA(evp_key); + if (rsa) { + const QByteArray digestByteArray = getStaticCertificateHash(); + const unsigned char* digest = reinterpret_cast(digestByteArray.constData()); + int digestLength = digestByteArray.length(); + + const QByteArray signatureByteArray = QByteArray::fromBase64(getCertificateID().toUtf8()); + const unsigned char* signature = reinterpret_cast(signatureByteArray.constData()); + int signatureLength = signatureByteArray.length(); + + ERR_clear_error(); + bool answer = RSA_verify(NID_sha256, + digest, + digestLength, + signature, + signatureLength, + rsa); + long error = ERR_get_error(); + if (error != 0) { + const char* error_str = ERR_error_string(error, NULL); + qCWarning(entities) << "ERROR while verifying static certificate properties! RSA error:" << error_str + << "\nStatic Cert JSON:" << getStaticCertificateJSON() + << "\nKey:" << EntityItem::_marketplacePublicKey << "\nKey Length:" << marketplacePublicKeyLength + << "\nDigest:" << digest << "\nDigest Length:" << digestLength + << "\nSignature:" << signature << "\nSignature Length:" << signatureLength; + } + RSA_free(rsa); + if (bio) { + BIO_free(bio); + } + if (evp_key) { + EVP_PKEY_free(evp_key); + } + return answer; + } else { + if (bio) { + BIO_free(bio); + } + if (evp_key) { + EVP_PKEY_free(evp_key); + } + long error = ERR_get_error(); + const char* error_str = ERR_error_string(error, NULL); + qCWarning(entities) << "Failed to verify static certificate properties! RSA error:" << error_str; + return false; + } + } else { + if (bio) { + BIO_free(bio); + } + long error = ERR_get_error(); + const char* error_str = ERR_error_string(error, NULL); + qCWarning(entities) << "Failed to verify static certificate properties! RSA error:" << error_str; + return false; + } } void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const { @@ -2986,3 +3001,34 @@ void EntityItem::somethingChangedNotification() { } }); } + +void EntityItem::retrieveMarketplacePublicKey() { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL.setPath("/api/v1/commerce/marketplace_key"); + QJsonObject request; + networkRequest.setUrl(requestURL); + + QNetworkReply* networkReply = NULL; + networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, [=]() { + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + + if (networkReply->error() == QNetworkReply::NoError) { + if (!jsonObject["public_key"].toString().isEmpty()) { + EntityItem::_marketplacePublicKey = jsonObject["public_key"].toString(); + qCWarning(entities) << "Marketplace public key has been set to" << _marketplacePublicKey; + } else { + qCWarning(entities) << "Marketplace public key is empty!"; + } + } else { + qCWarning(entities) << "Call to" << networkRequest.url() << "failed! Error:" << networkReply->error(); + } + + networkReply->deleteLater(); + }); +} diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index ce4bf13896..79862b9bd2 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -36,9 +36,6 @@ #include "SimulationFlags.h" #include "EntityDynamicInterface.h" -// FIXME: The server-side marketplace will soon create the certificateID. At that point, all of the DEBUG_CERT stuff will go away. -#define DEBUG_CERT 1 - class EntitySimulation; class EntityTreeElement; class EntityTreeElementExtraEncodeData; @@ -334,9 +331,6 @@ public: QByteArray getStaticCertificateJSON() const; QByteArray getStaticCertificateHash() const; bool verifyStaticCertificateProperties(); -#ifdef DEBUG_CERT - QString computeCertificateID(); -#endif // TODO: get rid of users of getRadius()... float getRadius() const; @@ -483,6 +477,9 @@ public: ChangeHandlerId registerChangeHandler(const ChangeHandlerCallback& handler); void deregisterChangeHandler(const ChangeHandlerId& changeHandlerId); + static QString _marketplacePublicKey; + static void retrieveMarketplacePublicKey(); + protected: QHash _changeHandlers; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index ed7a1469b3..c83a5f60a1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1833,18 +1833,3 @@ bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& en } return result; } - -#ifdef DEBUG_CERT -QString EntityScriptingInterface::computeCertificateID(const QUuid& entityID) { - QString result { "" }; - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - result = entity->computeCertificateID(); - } - }); - } - return result; -} -#endif diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 141289884b..cc5d8ff16f 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -424,9 +424,6 @@ public slots: Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); Q_INVOKABLE bool verifyStaticCertificateProperties(const QUuid& entityID); -#ifdef DEBUG_CERT - Q_INVOKABLE QString computeCertificateID(const QUuid& entityID); -#endif signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index bde82d96a1..9397f38cdd 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -13,6 +13,16 @@ #include #include +#include +#include +#include +#include +#include +#include "AccountManager.h" +#include +#include +#include + #include #include @@ -62,6 +72,8 @@ EntityTree::EntityTree(bool shouldReaverage) : Octree(shouldReaverage) { resetClientEditStats(); + + EntityItem::retrieveMarketplacePublicKey(); } EntityTree::~EntityTree() { @@ -228,6 +240,31 @@ bool EntityTree::handlesEditPacketType(PacketType packetType) const { /// Adds a new entity item to the tree void EntityTree::postAddEntity(EntityItemPointer entity) { assert(entity); + + if (getIsServer()) { + QString certID(entity->getCertificateID()); + EntityItemID entityItemID = entity->getEntityItemID(); + EntityItemID existingEntityItemID; + + { + QWriteLocker locker(&_entityCertificateIDMapLock); + existingEntityItemID = _entityCertificateIDMap.value(certID); + if (!certID.isEmpty()) { + _entityCertificateIDMap.insert(certID, entityItemID); + qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID; + } + } + + // Delete an already-existing entity from the tree if it has the same + // CertificateID as the entity we're trying to add. + if (!existingEntityItemID.isNull()) { + qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" + << existingEntityItemID << ". Deleting existing entity."; + deleteEntity(existingEntityItemID, true); + return; + } + } + // check to see if we need to simulate this entity.. if (_simulation) { _simulation->addEntity(entity); @@ -643,8 +680,16 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) theEntity->die(); if (getIsServer()) { + { + QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); + QString certID = theEntity->getCertificateID(); + if (theEntity->getEntityItemID() == _entityCertificateIDMap.value(certID)) { + _entityCertificateIDMap.remove(certID); + } + } + // set up the deleted entities ID - QWriteLocker locker(&_recentlyDeletedEntitiesLock); + QWriteLocker recentlyDeletedEntitiesLocker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(deletedAt, theEntity->getEntityItemID()); } else { // on the client side, we also remember that we deleted this entity, we don't care about the time @@ -1090,6 +1135,203 @@ bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) { return false; } +void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) { + QTimer* _challengeOwnershipTimeoutTimer = new QTimer(this); + connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const QString& certID) { + QReadLocker locker(&_entityCertificateIDMapLock); + EntityItemID id = _entityCertificateIDMap.value(certID); + if (entityItemID == id && _challengeOwnershipTimeoutTimer) { + _challengeOwnershipTimeoutTimer->stop(); + _challengeOwnershipTimeoutTimer->deleteLater(); + } + }); + connect(_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { + qCDebug(entities) << "Ownership challenge timed out, deleting entity" << entityItemID; + deleteEntity(entityItemID, true); + if (_challengeOwnershipTimeoutTimer) { + _challengeOwnershipTimeoutTimer->stop(); + _challengeOwnershipTimeoutTimer->deleteLater(); + } + }); + _challengeOwnershipTimeoutTimer->setSingleShot(true); + _challengeOwnershipTimeoutTimer->start(5000); +} + +void EntityTree::startPendingTransferStatusTimer(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode) { + qCDebug(entities) << "'transfer_status' is 'pending', checking again in 90 seconds..." << entityItemID; + QTimer* transferStatusRetryTimer = new QTimer(this); + connect(transferStatusRetryTimer, &QTimer::timeout, this, [=]() { + validatePop(certID, entityItemID, senderNode, true); + }); + transferStatusRetryTimer->setSingleShot(true); + transferStatusRetryTimer->start(90000); +} + +QByteArray EntityTree::computeEncryptedNonce(const QString& certID, const QString ownerKey) { + QString ownerKeyWithHeaders = ("-----BEGIN RSA PUBLIC KEY-----\n" + ownerKey + "\n-----END RSA PUBLIC KEY-----"); + BIO* bio = BIO_new_mem_buf((void*)ownerKeyWithHeaders.toUtf8().constData(), -1); + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // NO NEWLINE + RSA* rsa = PEM_read_bio_RSAPublicKey(bio, NULL, NULL, NULL); + + if (rsa) { + QUuid nonce = QUuid::createUuid(); + const unsigned int textLength = nonce.toString().length(); + QByteArray encryptedText(RSA_size(rsa), 0); + const int encryptStatus = RSA_public_encrypt(textLength, + reinterpret_cast(qPrintable(nonce.toString())), + reinterpret_cast(encryptedText.data()), + rsa, + RSA_PKCS1_OAEP_PADDING); + if (bio) { + BIO_free(bio); + } + RSA_free(rsa); + if (encryptStatus == -1) { + long error = ERR_get_error(); + const char* error_str = ERR_error_string(error, NULL); + qCWarning(entities) << "Unable to compute encrypted nonce for" << certID << "\nRSA error:" << error_str; + return ""; + } + + QWriteLocker locker(&_certNonceMapLock); + _certNonceMap.insert(certID, nonce); + qCDebug(entities) << "Challenging ownership of Cert ID" << certID << "by encrypting and sending nonce" << nonce << "to owner."; + + return encryptedText; + } else { + if (bio) { + BIO_free(bio); + } + return ""; + } +} + +bool EntityTree::verifyDecryptedNonce(const QString& certID, const QString& decryptedNonce) { + + EntityItemID id; + { + QReadLocker certIdMapLocker(&_entityCertificateIDMapLock); + id = _entityCertificateIDMap.value(certID); + } + + QString actualNonce; + { + QWriteLocker locker(&_certNonceMapLock); + actualNonce = _certNonceMap.take(certID).toString(); + } + + bool verificationSuccess = (actualNonce == decryptedNonce); + if (!verificationSuccess) { + if (!id.isNull()) { + qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "failed; deleting entity" << id + << "\nActual nonce:" << actualNonce << "\nDecrypted nonce:" << decryptedNonce; + deleteEntity(id, true); + } + } else { + qCDebug(entities) << "Ownership challenge for Cert ID" << certID << "succeeded; keeping entity" << id; + } + + return verificationSuccess; +} + +void EntityTree::validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode, bool isRetryingValidation) { + // Start owner verification. + auto nodeList = DependencyManager::get(); + // First, asynchronously hit "proof_of_purchase_status?transaction_type=transfer" endpoint. + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer"); + QJsonObject request; + request["certificate_id"] = certID; + networkRequest.setUrl(requestURL); + + QNetworkReply* networkReply = NULL; + networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson()); + + connect(networkReply, &QNetworkReply::finished, [=]() { + QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object(); + jsonObject = jsonObject["data"].toObject(); + + if (networkReply->error() == QNetworkReply::NoError) { + if (!jsonObject["invalid_reason"].toString().isEmpty()) { + qCDebug(entities) << "invalid_reason not empty, deleting entity" << entityItemID; + deleteEntity(entityItemID, true); + } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { + qCDebug(entities) << "'transfer_status' is 'failed', deleting entity" << entityItemID; + deleteEntity(entityItemID, true); + } else if (jsonObject["transfer_status"].toArray().first().toString() == "pending") { + if (isRetryingValidation) { + qCDebug(entities) << "'transfer_status' is 'pending' after retry, deleting entity" << entityItemID; + deleteEntity(entityItemID, true); + } else { + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "startPendingTransferStatusTimer", + Q_ARG(const QString&, certID), + Q_ARG(const EntityItemID&, entityItemID), + Q_ARG(const SharedNodePointer&, senderNode)); + return; + } else { + startPendingTransferStatusTimer(certID, entityItemID, senderNode); + } + } + } else { + // Second, challenge ownership of the PoP cert + // 1. Encrypt a nonce with the owner's public key + QByteArray encryptedText = computeEncryptedNonce(certID, jsonObject["transfer_recipient_key"].toString()); + + if (encryptedText == "") { + qCDebug(entities) << "CRITICAL ERROR: Couldn't compute encrypted nonce. Deleting entity..."; + deleteEntity(entityItemID, true); + } else { + // 2. Send the encrypted text to the rezzing avatar's node + QByteArray certIDByteArray = certID.toUtf8(); + int certIDByteArraySize = certIDByteArray.size(); + auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership, + certIDByteArraySize + encryptedText.length() + 2 * sizeof(int), + true); + challengeOwnershipPacket->writePrimitive(certIDByteArraySize); + challengeOwnershipPacket->writePrimitive(encryptedText.length()); + challengeOwnershipPacket->write(certIDByteArray); + challengeOwnershipPacket->write(encryptedText); + nodeList->sendPacket(std::move(challengeOwnershipPacket), *senderNode); + + // 3. Kickoff a 10-second timeout timer that deletes the entity if we don't get an ownership response in time + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer", Q_ARG(const EntityItemID&, entityItemID)); + return; + } else { + startChallengeOwnershipTimer(entityItemID); + } + } + } + } else { + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << entityItemID + << "More info:" << jsonObject; + deleteEntity(entityItemID, true); + } + + networkReply->deleteLater(); + }); +} + +void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { + int certIDByteArraySize; + int decryptedTextByteArraySize; + + message.readPrimitive(&certIDByteArraySize); + message.readPrimitive(&decryptedTextByteArraySize); + + QString certID(message.read(certIDByteArraySize)); + QString decryptedText(message.read(decryptedTextByteArraySize)); + + emit killChallengeOwnershipTimeoutTimer(certID); + + verifyDecryptedNonce(certID, decryptedText); +} + int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode) { @@ -1265,13 +1507,17 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c _totalUpdates++; } else if (isAdd) { bool failedAdd = !allowed; + bool isCertified = !properties.getCertificateID().isEmpty(); if (!allowed) { qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; - } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { + } else if (!isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; - qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity ID:" << entityItemID; - + qCDebug(entities) << "User without 'uncertified rez rights' [" << senderNode->getUUID() + << "] attempted to add an uncertified entity with ID:" << entityItemID; + } else if (isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { + failedAdd = true; + qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() + << "] attempted to add a certified entity with ID:" << entityItemID; } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); @@ -1280,6 +1526,19 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c EntityItemPointer newEntity = addEntity(entityItemID, properties); endCreate = usecTimestampNow(); _totalCreates++; + + if (newEntity && isCertified && getIsServer()) { + if (!newEntity->verifyStaticCertificateProperties()) { + qCDebug(entities) << "User" << senderNode->getUUID() + << "attempted to add a certified entity with ID" << entityItemID << "which failed" + << "static certificate verification."; + // Delete the entity we just added if it doesn't pass static certificate verification + deleteEntity(entityItemID, true); + } else { + validatePop(properties.getCertificateID(), entityItemID, senderNode, false); + } + } + if (newEntity) { newEntity->markAsChangedOnServer(); notifyNewlyCreatedEntity(*newEntity, senderNode); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 25e36c8ffb..518cde9a59 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -93,6 +93,7 @@ public: void fixupTerseEditLogging(EntityItemProperties& properties, QList& changedProperties); virtual int processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode) override; + virtual void processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, @@ -181,6 +182,11 @@ public: return _recentlyDeletedEntityItemIDs; } + QHash getEntityCertificateIDMap() const { + QReadLocker locker(&_entityCertificateIDMapLock); + return _entityCertificateIDMap; + } + void forgetEntitiesDeletedBefore(quint64 sinceTime); int processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode); @@ -276,6 +282,7 @@ signals: void entityServerScriptChanging(const EntityItemID& entityItemID, const bool reload); void newCollisionSoundURL(const QUrl& url, const EntityItemID& entityID); void clearingEntities(); + void killChallengeOwnershipTimeoutTimer(const QString& certID); protected: @@ -316,6 +323,12 @@ protected: mutable QReadWriteLock _entityMapLock; QHash _entityMap; + mutable QReadWriteLock _entityCertificateIDMapLock; + QHash _entityCertificateIDMap; + + mutable QReadWriteLock _certNonceMapLock; + QHash _certNonceMap; + EntitySimulationPointer _simulation; bool _wantEditLogging = false; @@ -357,6 +370,14 @@ protected: MovingEntitiesOperator _entityMover; QHash _entitiesToAdd; + + Q_INVOKABLE void startChallengeOwnershipTimer(const EntityItemID& entityItemID); + Q_INVOKABLE void startPendingTransferStatusTimer(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode); + +private: + QByteArray computeEncryptedNonce(const QString& certID, const QString ownerKey); + bool verifyDecryptedNonce(const QString& certID, const QString& decryptedNonce); + void validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode, bool isRetryingValidation); }; #endif // hifi_EntityTree_h diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index 3f0a629c8c..cc4a919445 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include "FSTReader.h" diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index fa6b49597d..f6264a19c4 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -28,7 +28,6 @@ #include -#include "NetworkingConstants.h" #include "NetworkLogging.h" #include "NodeList.h" #include "udt/PacketHeaders.h" @@ -240,7 +239,7 @@ void AccountManager::sendRequest(const QString& path, QUrl requestURL = _authURL; if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL. - requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + requestURL = getMetaverseServerURL(); } if (path.startsWith("/")) { diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index b37846ec1b..7d97687d0b 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -18,6 +18,7 @@ #include #include +#include "NetworkingConstants.h" #include "NetworkAccessManager.h" #include "DataServerAccountInfo.h" @@ -96,6 +97,8 @@ public: void setTemporaryDomain(const QUuid& domainID, const QString& key); const QString& getTemporaryDomainKey(const QUuid& domainID) { return _accountInfo.getTemporaryDomainKey(domainID); } + QUrl getMetaverseServerURL() { return NetworkingConstants::METAVERSE_SERVER_URL; } + public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 2376a3b2b6..1a51440e91 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -24,7 +24,6 @@ #include "AddressManager.h" #include "NodeList.h" -#include "NetworkingConstants.h" #include "NetworkLogging.h" #include "UserActivityLogger.h" #include "udt/PacketHeaders.h" @@ -770,10 +769,6 @@ QString AddressManager::getDomainId() const { return DependencyManager::get()->getDomainHandler().getUUID().toString(); } -const QUrl AddressManager::getMetaverseServerUrl() const { - return NetworkingConstants::METAVERSE_SERVER_URL; -} - void AddressManager::handleShareableNameAPIResponse(QNetworkReply& requestReply) { // make sure that this response is for the domain we're currently connected to auto domainID = DependencyManager::get()->getDomainHandler().getUUID(); diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 83eedfc82f..366fc5dfab 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -41,7 +41,6 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString pathname READ currentPath) Q_PROPERTY(QString placename READ getPlaceName) Q_PROPERTY(QString domainId READ getDomainId) - Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl NOTIFY metaverseServerUrlChanged) public: Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; @@ -71,7 +70,6 @@ public: const QUuid& getRootPlaceID() const { return _rootPlaceID; } const QString& getPlaceName() const { return _shareablePlaceName.isEmpty() ? _placeName : _shareablePlaceName; } QString getDomainId() const; - const QUrl getMetaverseServerUrl() const; const QString& getHost() const { return _host; } @@ -123,8 +121,6 @@ signals: void goBackPossible(bool isPossible); void goForwardPossible(bool isPossible); - void metaverseServerUrlChanged(); - protected: AddressManager(); private slots: diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index a512ae8887..0bb0cee5d2 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -15,7 +15,12 @@ #include namespace NetworkingConstants { - const QUrl METAVERSE_SERVER_URL = QUrl("https://metaverse.highfidelity.com"); + // If you want to use STAGING instead of STABLE, + // don't forget to ALSO change the Domain Server Metaverse Server URL, which is at the top of: + // \domain-server\resources\web\settings\js\settings.js + const QUrl METAVERSE_SERVER_URL_STABLE("https://metaverse.highfidelity.com"); + const QUrl METAVERSE_SERVER_URL_STAGING("https://staging.highfidelity.com"); + const QUrl METAVERSE_SERVER_URL = METAVERSE_SERVER_URL_STABLE; } #endif // hifi_NetworkingConstants_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index ef2768b0af..97d750a7a6 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::StrokeColorProperty); + return static_cast(EntityVersion::HasDynamicOwnershipTests); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 9443ee570d..4132c499dd 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -195,7 +195,8 @@ uint qHash(const PacketType& key, uint seed); QDebug operator<<(QDebug debug, const PacketType& type); enum class EntityVersion : PacketVersion { - StrokeColorProperty = 77 + StrokeColorProperty = 77, + HasDynamicOwnershipTests }; enum class EntityScriptCallMethodVersion : PacketVersion { diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index a252011ba0..2761dffb1b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -212,6 +212,7 @@ public: virtual bool handlesEditPacketType(PacketType packetType) const { return false; } virtual int processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& sourceNode) { return 0; } + virtual void processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { return; } virtual bool recurseChildrenWithData() const { return true; } virtual bool rootElementHasData() const { return false; } diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index f9686d2311..b1deec47c8 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -20,15 +20,16 @@ namespace { bool isAuthableHighFidelityURL(const QUrl& url) { + auto metaverseServerURL = NetworkingConstants::METAVERSE_SERVER_URL; static const QStringList HF_HOSTS = { "highfidelity.com", "highfidelity.io", - "metaverse.highfidelity.com", "metaverse.highfidelity.io" + metaverseServerURL.toString(), "metaverse.highfidelity.io" }; const auto& scheme = url.scheme(); const auto& host = url.host(); return (scheme == "https" && HF_HOSTS.contains(host)) || - ((scheme == NetworkingConstants::METAVERSE_SERVER_URL.scheme()) && (host == NetworkingConstants::METAVERSE_SERVER_URL.host())); + ((scheme == metaverseServerURL.scheme()) && (host == metaverseServerURL.host())); } bool isScript(const QString filename) { diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 04b67ec14f..8e4a3215fd 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -16,7 +16,7 @@ (function () { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/accountUtils.js"); - var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; + var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; // Function Name: onButtonClicked() // diff --git a/scripts/system/directory.js b/scripts/system/directory.js index ac65615a68..8b9ec17f05 100644 --- a/scripts/system/directory.js +++ b/scripts/system/directory.js @@ -15,7 +15,7 @@ Script.include([ var toolIconUrl = Script.resolvePath("assets/images/tools/"); -var DIRECTORY_WINDOW_URL = "https://metaverse.highfidelity.com/directory"; +var DIRECTORY_WINDOW_URL = Account.metaverseServerURL + "/directory"; var directoryWindow = new OverlayWebWindow({ title: 'Directory', source: "about:blank", diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 2ae1e142a0..e76a02b6f5 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -163,7 +163,7 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", { visible: false }); -var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var marketplaceWindow = new OverlayWebWindow({ title: 'Marketplace', source: "about:blank", @@ -415,7 +415,7 @@ var toolBar = (function () { } }); - var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); + var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()); var createButtonIconRsrc = (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ @@ -434,7 +434,7 @@ var toolBar = (function () { tablet.fromQml.connect(fromQml); createButton.clicked.connect(function() { - if ( ! (Entities.canRez() || Entities.canRezTmp()) ) { + if ( ! (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()) ) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); return; } @@ -634,7 +634,7 @@ var toolBar = (function () { if (active === isActive) { return; } - if (active && !Entities.canRez() && !Entities.canRezTmp()) { + if (active && !Entities.canRez() && !Entities.canRezTmp() && !Entities.canRezCertified() && !Entities.canRezTmpCertified()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); return; } @@ -789,7 +789,7 @@ function handleDomainChange() { return; } - var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); + var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()); createButton.editProperties({ icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON), captionColorOverride: (hasRezPermissions ? "" : "#888888"), @@ -1491,7 +1491,7 @@ function onFileOpenChanged(filename) { } } if (importURL) { - if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp() && Entities.canRezCertified() && Entities.canRezTmpCertified())) { toolBar.toggle(); } importSVO(importURL); @@ -1501,7 +1501,7 @@ function onFileOpenChanged(filename) { function onPromptTextChanged(prompt) { Window.promptTextChanged.disconnect(onPromptTextChanged); if (prompt !== "") { - if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp() && Entities.canRezCertified() && Entities.canRezTmpCertified())) { toolBar.toggle(); } importSVO(prompt); @@ -1570,7 +1570,8 @@ function getPositionToCreateEntity(extra) { } function importSVO(importURL) { - if (!Entities.canRez() && !Entities.canRezTmp()) { + if (!Entities.canRez() && !Entities.canRezTmp() && + !Entities.canRezCertified() && !Entities.canRezTmpCertified()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); return; } diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 29bbb4576e..8deb5c0bbd 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -29,6 +29,7 @@ var commerceMode = false; var userIsLoggedIn = false; var walletNeedsSetup = false; + var metaverseServerURL = "https://metaverse.highfidelity.com"; function injectCommonCode(isDirectoryPage) { @@ -57,7 +58,7 @@ ); // Footer. - var isInitialHiFiPage = location.href === "https://metaverse.highfidelity.com/marketplace?"; + var isInitialHiFiPage = location.href === metaverseServerURL + "/marketplace?"; $("body").append( '
' + (!isInitialHiFiPage ? '' : '') + @@ -69,7 +70,7 @@ // Footer actions. $("#back-button").on("click", function () { - (document.referrer !== "") ? window.history.back() : window.location = "https://metaverse.highfidelity.com/marketplace?"; + (document.referrer !== "") ? window.history.back() : window.location = (metaverseServerURL + "/marketplace?"); }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(GOTO_DIRECTORY); @@ -641,6 +642,7 @@ commerceMode = !!parsedJsonMessage.data.commerceMode; userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn; walletNeedsSetup = !!parsedJsonMessage.data.walletNeedsSetup; + metaverseServerURL = parsedJsonMessage.data.metaverseServerURL; injectCode(); } } diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index bfad959ffc..7dddd46562 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -444,7 +444,7 @@ }, WAITING_INTERVAL); } - var pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; + var pollCount = 0, requestUrl = Account.metaverseServerURL + '/api/v1/user/connection_request'; // As currently implemented, we select the closest waiting avatar (if close enough) and send // them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a // connectionRequest. If the 2 people who want to connect are both somewhat out of range when they @@ -569,7 +569,7 @@ // IWBNI we also did some fail sound/visual effect. Window.makeConnection(false, result.connection); if (Account.isLoggedIn()) { // Give extra failure info - request(location.metaverseServerUrl + '/api/v1/users/' + Account.username + '/location', function (error, response) { + request(Account.metaverseServerURL + '/api/v1/users/' + Account.username + '/location', function (error, response) { var message = ''; if (error || response.status !== 'success') { message = 'Unable to get location.'; diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index f365ca5d4c..d90695c767 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -15,7 +15,7 @@ Script.include("../libraries/WebTablet.js"); var toolIconUrl = Script.resolvePath("../assets/images/tools/"); -var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var marketplaceWindow = new OverlayWebWindow({ title: "Marketplace", source: "about:blank", diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 3bf6435a25..f0044084f6 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -15,7 +15,8 @@ Script.include("../libraries/WebTablet.js"); - var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; + var METAVERSE_SERVER_URL = Account.metaverseServerURL; + var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); @@ -135,7 +136,7 @@ function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { wireEventBridge(true); - var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID + "\n"); + var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); tablet.sendToQml({ method: 'inspectionCertificate_setCertificateId', certificateId: certificateId @@ -155,7 +156,8 @@ data: { commerceMode: Settings.getValue("commerce", false), userIsLoggedIn: Account.loggedIn, - walletNeedsSetup: Wallet.walletStatus === 1 + walletNeedsSetup: Wallet.walletStatus === 1, + metaverseServerURL: Account.metaverseServerURL } })); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 2a8a89ae7d..44ff7c2acd 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -334,7 +334,7 @@ function updateUser(data) { // User management services // // These are prototype versions that will be changed when the back end changes. -var METAVERSE_BASE = location.metaverseServerUrl; +var METAVERSE_BASE = Account.metaverseServerURL; function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. request({ diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8f08b29cff..9afdb4ec53 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -35,7 +35,7 @@ var imageData = []; var storyIDsToMaybeDelete = []; var shareAfterLogin = false; var snapshotToShareAfterLogin = []; -var METAVERSE_BASE = location.metaverseServerUrl; +var METAVERSE_BASE = Account.metaverseServerURL; var isLoggedIn; var numGifSnapshotUploadsPending = 0; var numStillSnapshotUploadsPending = 0; diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 96ed3e3f59..929c6e0e5c 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -115,7 +115,7 @@ var stories = {}, pingPong = false; function expire(id) { var options = { - uri: location.metaverseServerUrl + '/api/v1/user_stories/' + id, + uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, method: 'PUT', json: true, body: {expire: "true"} @@ -139,7 +139,7 @@ 'protocol=' + encodeURIComponent(location.protocolVersion()), 'per_page=' + count ]; - var url = location.metaverseServerUrl + '/api/v1/user_stories?' + options.join('&'); + var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); request({ uri: url }, function (error, data) { diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index 591537f9bc..6f37cd55eb 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -14,7 +14,7 @@ var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; - var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; + var FRIENDS_WINDOW_URL = Account.metaverseServerURL + "/user/friends"; var FRIENDS_WINDOW_WIDTH = 290; var FRIENDS_WINDOW_HEIGHT = 500; var FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js index 54c356ae7b..1af801f2d6 100644 --- a/scripts/tutorials/getDomainMetadata.js +++ b/scripts/tutorials/getDomainMetadata.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var SERVER = 'https://metaverse.highfidelity.com/api/v1'; +var SERVER = Account.metaverseServerURL + '/api/v1'; var OVERLAY = null;