diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index e5c22c3d34..724571c111 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -9,9 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include #include +#include +#include #include "EntityServer.h" #include "EntityServerConsts.h" @@ -26,6 +29,10 @@ EntityServer::EntityServer(ReceivedMessage& message) : OctreeServer(message), _entitySimulation(NULL) { + ResourceManager::init(); + DependencyManager::set(); + DependencyManager::set(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase }, this, "handleEntityPacket"); @@ -286,11 +293,96 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio tree->setEntityScriptSourceWhitelist(""); } - QString entityEditFilter; - if (readOptionString("entityEditFilter", settingsSectionObject, entityEditFilter)) { - tree->setEntityEditFilter(entityEditFilter); + if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) { + // Fetch script from file synchronously. We don't want the server processing edits while a restarting entity server is fetching from a DOS'd source. + QUrl scriptURL(_entityEditFilter); + + // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) + if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { + qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; + scriptRequestFinished(); + return; + } + auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL); + if (!scriptRequest) { + qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString(); + scriptRequestFinished(); + return; + } + // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own. + connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished); + // FIXME: handle atp rquests setup here. See Agent::requestScript() + qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); + scriptRequest->send(); + _scriptRequestLoop.exec(); // Block here, but allow the request to be processed and its signals to be handled. + } +} + +// Copied from ScriptEngine.cpp. We should make this a class method for reuse. +// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point. +static bool hasCorrectSyntax(const QScriptProgram& program) { + const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); + if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { + const auto error = syntaxCheck.errorMessage(); + const auto line = QString::number(syntaxCheck.errorLineNumber()); + const auto column = QString::number(syntaxCheck.errorColumnNumber()); + const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); + qCritical() << qPrintable(message); + return false; + } + return true; +} +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; + auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qCritical() << qPrintable(message); + return true; + } + return false; +} +void EntityServer::scriptRequestFinished() { + auto scriptRequest = qobject_cast(sender()); + const QString urlString = scriptRequest->getUrl().toString(); + if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { + auto scriptContents = scriptRequest->getData(); + qInfo() << "Downloaded script:" << scriptContents; + QScriptProgram program(scriptContents, urlString); + if (hasCorrectSyntax(program)) { + _entityEditFilterEngine.evaluate(scriptContents); + if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) { + std::static_pointer_cast(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() { + return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter); + }); + scriptRequest->deleteLater(); + if (_scriptRequestLoop.isRunning()) { + _scriptRequestLoop.quit(); + } + return; + } + } + } else if (scriptRequest) { + qCritical() << "Failed to download script at" << urlString; + // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. + qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); + } else { + qCritical() << "Failed to create script request."; + } + // Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't. + // Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities. + stop(); + if (_scriptRequestLoop.isRunning()) { + _scriptRequestLoop.quit(); } - tree->initEntityEditFilterEngine(); // whether supplied or not. } void EntityServer::nodeAdded(SharedNodePointer node) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 0486a97ede..25270c9dd5 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -69,6 +69,7 @@ protected: private slots: void handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode); + void scriptRequestFinished(); private: SimpleEntitySimulationPointer _entitySimulation; @@ -76,6 +77,10 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; + + QString _entityEditFilter{}; + QScriptEngine _entityEditFilterEngine{}; + QEventLoop _scriptRequestLoop{}; }; #endif // hifi_EntityServer_h diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 3d5b17dffe..3ceb425ccb 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1294,7 +1294,7 @@ "name": "entityEditFilter", "label": "Filter Entity Edits", "help": "Check all entity edits against this filter function.", - "placeholder": "function filter(properties) { return properties; }", + "placeholder": "url whose content is like: function filter(properties) { return properties; }", "default": "", "advanced": true }, diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e330ec5533..7c3eb7bec3 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -922,15 +922,16 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList entityEditFilterHadUncaughtExceptions) { + _entityEditFilterEngine = engine; + _entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions; + auto global = _entityEditFilterEngine->globalObject(); _entityEditFilterFunction = global.property("filter"); _hasEntityEditFilter = _entityEditFilterFunction.isFunction(); } bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) { - if (!_hasEntityEditFilter) { + if (!_hasEntityEditFilter || !_entityEditFilterEngine) { propertiesOut = propertiesIn; wasChanged = false; // not changed return true; // allowed @@ -938,7 +939,7 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem auto oldProperties = propertiesIn.getDesiredProperties(); auto specifiedProperties = propertiesIn.getChangedProperties(); propertiesIn.setDesiredProperties(specifiedProperties); - QScriptValue inputValues = propertiesIn.copyToScriptValue(&_entityEditFilterEngine, false, true, true); + QScriptValue inputValues = propertiesIn.copyToScriptValue(_entityEditFilterEngine, false, true, true); propertiesIn.setDesiredProperties(oldProperties); auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. @@ -946,22 +947,16 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem args << inputValues; QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args); + if (_entityEditFilterHadUncaughtExceptions()) { + result = QScriptValue(); + } - propertiesOut.copyFromScriptValue(result, false); bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add if (accepted) { + propertiesOut.copyFromScriptValue(result, false); // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. - auto out = QJsonValue::fromVariant(result.toVariant()); wasChanged = in != out; - if (wasChanged) { - // Logging will be removed eventually, but for now, the behavior is so fragile that it's worth logging. - qCDebug(entities) << "filter accepted. changed: true"; - qCDebug(entities) << " in:" << in; - qCDebug(entities) << " out:" << out; - } - } else { - qCDebug(entities) << "filter rejected. in:" << in; } return accepted; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 1ccc5db4b6..cc179e7de0 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -68,7 +68,6 @@ public: void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; } void setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist); - void setEntityEditFilter(const QString& entityEditFilter) { _entityEditFilter = entityEditFilter; } /// Implements our type specific root element factory virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override; @@ -267,7 +266,7 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); - void initEntityEditFilterEngine(); + void initEntityEditFilterEngine(QScriptEngine* engine, std::function entityEditFilterHadUncaughtExceptions); static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; @@ -358,11 +357,11 @@ protected: float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged); - QString _entityEditFilter; bool _hasEntityEditFilter{ false }; - QScriptEngine _entityEditFilterEngine; - QScriptValue _entityEditFilterFunction; - QScriptValue _nullObjectForFilter; + QScriptEngine* _entityEditFilterEngine{}; + QScriptValue _entityEditFilterFunction{}; + QScriptValue _nullObjectForFilter{}; + std::function _entityEditFilterHadUncaughtExceptions; QStringList _entityScriptSourceWhitelist; };