diff options
| author | Jonathan "Geenz" Goodman <geenz@lindenlab.com> | 2026-10-13 14:36:53 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-13 14:36:53 -0400 |
| commit | fac07196d86e899686c4f1ce88776b11a906cd58 (patch) | |
| tree | f3a3a530b80b1b17e71c7b6d7fa8b981b52c61f9 | |
| parent | 083db782e29434ab17963fcac76b0ec3cd93b9d0 (diff) | |
| parent | 181fd93f799e15a6ff924362c4a05be8db3639af (diff) | |
Merge pull request #6438 from secondlife/geenz/main-26.5
Main -> 26.5
77 files changed, 2537 insertions, 1301 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index 0c32c043fe..80cc135522 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -65,9 +65,14 @@ This document describes all the message interfaces defined for WebSocket communi 1. **Connection Establishment:** + - The viewer refuses a WebSocket upgrade request that carries an `Origin` header (HTTP 403): + browsers send one with every socket a page opens, and a program connecting need not - Viewer sends `session.handshake` call with `SessionHandshake` data - - Extension responds with `SessionHandshakeResponse` + - Extension responds with `SessionHandshakeResponse`, answering the challenge - Viewer confirms with `session.ok` notification + - Until `session.ok`, every request the extension makes is refused with `-32002` + (Authentication required) and the viewer sends it no notifications. A wrong challenge + response, an error in place of one, or no response within 30 seconds closes the connection. 2. **Language Information Exchange:** @@ -291,7 +296,7 @@ interface SessionHandshake { viewer_version: string; agent_id: string; agent_name: string; - challenge?: string; + challenge: string; languages: string[]; syntax_id: string; features: { [feature: string]: boolean }; @@ -306,7 +311,7 @@ interface SessionHandshake { - `viewer_version`: Version string of the viewer - `agent_id`: Unique identifier for the user/agent - `agent_name`: Human-readable name of the agent -- `challenge` (optional): Path to a temporary file on the local filesystem containing a UUID. The client must read this file and return the UUID as `challenge_response` to authenticate the connection. +- `challenge`: Path to a temporary file on the local filesystem containing a UUID, and nothing else. The client must read this file and return its contents as `challenge_response` to authenticate the connection. The file is named at random, readable only by the user running the viewer, and deleted once the handshake is answered or abandoned, so only a client running as that user can answer. - `languages`: Array of supported scripting languages (e.g., `["lsl", "luau"]`) - `syntax_id`: Current active syntax identifier as a UUID string - `features`: Dictionary of feature flags indicating viewer capabilities. Known flags: @@ -327,7 +332,7 @@ interface SessionHandshakeResponse { client_name: string; client_version: "1.0"; protocol_version: string; - challenge_response?: string; + challenge_response: string; languages: string[]; features: { [feature: string]: boolean }; script_name?: string; @@ -340,7 +345,7 @@ interface SessionHandshakeResponse { - `client_name`: Name of the client (VS Code extension) - `client_version`: Fixed version "1.0" of the client - `protocol_version`: Protocol version the client supports -- `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed. +- `challenge_response`: The UUID read from the temporary file identified by the `challenge` field in the handshake. Without it, or with any other value, the connection is closed. - `languages`: Array of languages supported by the client - `features`: Dictionary of features supported by the client. Known flags: - `live_sync`: Client supports live script synchronisation @@ -1084,7 +1089,7 @@ interface LinkedObject { interface ObjectPermissions { owner: number; - next_owner: number; + next_owner?: number; // Absent until the viewer receives ObjectProperties for the prim } /** Root of a linkset, as published to the extension */ @@ -1235,19 +1240,30 @@ Sent when an explored object's inventory, properties, or linkset membership chan | ------- | ------- | | Root prim inventory changed | `object_id`, `inventory` (complete replacement array) | | Child prim inventory changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `inventory` (complete replacement array for that child) | -| Root prim name/description changed | `object_id`, `object_name` and/or `object_description` | -| Child prim name/description changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `link_name` and/or `link_description` | +| Root prim properties changed | `object_id`, plus any changed subset of `object_name`, `object_description`, `owner_id`, `permissions`, `can_save_back` | +| Child prim properties changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` plus any changed subset of `link_name`, `link_description`, `permissions` | | Linkset membership changed | `object_id`, `linked_objects` (complete replacement array covering every child prim) | `inventory` and `linked_objects` are always complete replacements of the prior state, never increments. Linkset membership changes are coalesced behind a short flush delay, so several rapid link or unlink operations may arrive as a single update. +**Deferred permission fields.** `object.publish` is gated on inventory loading, which can finish +before the `ObjectProperties` reply arrives. Until it does the viewer derives `permissions.owner` +from the object's update flags, which describe the current agent's rights, and omits `next_owner`. +`owner_id` may be absent or the null UUID, and `can_save_back` may be `false` because it depends on +selection state. Once properties arrive the viewer sends an `object.update` carrying the full +`owner`/`next_owner` block and the real `owner_id`, so clients must merge these fields on update +rather than assuming the publish payload is final. + ```typescript interface ObjectUpdateMessage { object_id: string; object_name?: string; object_description?: string; + owner_id?: string; + permissions?: ObjectPermissions; + can_save_back?: boolean; // Full replacement inventory?: ObjectInventoryItem[]; linked_objects?: LinkedObject[]; @@ -1264,6 +1280,7 @@ interface LinkedObjectChanges { link_id: string; link_name?: string; link_description?: string; + permissions?: ObjectPermissions; inventory?: ObjectInventoryItem[]; // Complete replacement array, not a delta }[]; } diff --git a/indra/cmake/BuildPackagesInfo.cmake b/indra/cmake/BuildPackagesInfo.cmake index 659ba9d579..9a615ad3cd 100644 --- a/indra/cmake/BuildPackagesInfo.cmake +++ b/indra/cmake/BuildPackagesInfo.cmake @@ -12,9 +12,10 @@ add_custom_command(OUTPUT packages-info.txt COMMENT "Generating packages-info.txt for the about box" MAIN_DEPENDENCY ${CMAKE_SOURCE_DIR}/../autobuild.xml DEPENDS ${CMAKE_SOURCE_DIR}/../scripts/packages-formatter.py + ${CMAKE_SOURCE_DIR}/newview/luau.txt ${CMAKE_SOURCE_DIR}/../autobuild.xml COMMAND ${PYTHON_EXECUTABLE} - ${CMAKE_SOURCE_DIR}/cmake/run_build_test.py -DAUTOBUILD_ADDRSIZE=${ADDRESS_SIZE} -DAUTOBUILD=${AUTOBUILD_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/cmake/run_build_test.py -DAUTOBUILD_ADDRSIZE=${ADDRESS_SIZE} -DAUTOBUILD=${AUTOBUILD_EXECUTABLE} -DPYTHONIOENCODING=utf-8 ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../scripts/packages-formatter.py "${VIEWER_CHANNEL}" "${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}" "${AUTOBUILD_INSTALL_DIR}" > packages-info.txt ) diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index fbd92d089e..4f19ab2cc2 100644 --- a/indra/llcommon/workqueue.cpp +++ b/indra/llcommon/workqueue.cpp @@ -319,6 +319,12 @@ bool LL::WorkQueue::tryPost(const Work& callable) } } +bool LL::WorkQueue::postToMainLoop(const Work& work) +{ + auto queue = getInstance("mainloop"); + return queue && queue->tryPost(work); +} + LL::WorkQueue::Work LL::WorkQueue::pop_() { return mQueue.pop(); diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index 69f3286c1b..322baa36e6 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -265,6 +265,16 @@ namespace LL */ bool tryPost(const Work&) override; + /** + * @brief Post work to the well-known main-loop queue without blocking + * @return true if posted, false if the main loop is gone or its + * queue is closed/full + * + * For callers on a secondary thread that must not block waiting for + * the main thread (e.g. a websocket I/O thread). + */ + static bool postToMainLoop(const Work& work); + private: using Queue = LLThreadSafeQueue<Work>; Queue mQueue; diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt index c2c4078ff0..bdc2582eef 100644 --- a/indra/llcorehttp/CMakeLists.txt +++ b/indra/llcorehttp/CMakeLists.txt @@ -120,6 +120,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) tests/test_bufferarray.hpp tests/test_bufferstream.hpp tests/test_jsonrpcws.hpp + tests/test_websocketmgr.hpp ) list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES}) @@ -144,6 +145,10 @@ if (LL_TESTS AND LLCOREHTTP_TESTS) "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llcorehttp_peer.py" ) + LL_ADD_INTEGRATION_TEST(wsserver "" "${test_libs}") + + LL_ADD_INTEGRATION_TEST(jsonrpcws "" "${test_libs}") + # # Example Programs # diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index 5b704e6322..f04738d805 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -31,9 +31,8 @@ #include "llsdjson.h" #include "lldate.h" #include "llcoros.h" -#include "llmainthreadtask.h" -#include "lleventtimer.h" #include "lltimer.h" +#include "workqueue.h" #include <boost/json.hpp> @@ -44,27 +43,10 @@ void LLJSONRPCConnection::onOpen() { LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL; - - // Start the recurring timeout sweep timer on the main thread. The timer - // is canceled in onClose() before the connection can be destroyed, so - // capturing `this` is safe. Keep a weak_ptr so we can safely test - // whether the timer instance still exists at cancellation time. - LLEventTimer* timer = LLEventTimer::run_every(TIMEOUT_SWEEP_INTERVAL, - [this]() { sweepTimeouts(); }); - mTimeoutTimer = timer->getWeak(); } void LLJSONRPCConnection::onClose() { - // Cancel the sweep timer if it is still alive. LLEventTimer's instance - // tracker keeps a shared_ptr with a no-op deleter, so raw `delete` is - // the documented cancellation idiom (see lleventtimer.h). - if (auto timer = mTimeoutTimer.lock()) - { - delete timer.get(); - } - mTimeoutTimer.reset(); - // Move the pending-request map out under the lock so we can invoke the // callbacks without holding it (callbacks may themselves call into this // connection). @@ -132,7 +114,7 @@ void LLJSONRPCConnection::onMessage(const std::string& message) catch (const std::exception& e) { LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL; - sendError(LLSD(), InternalError(e.what())); + sendError(LLSD(), InternalError()); } } @@ -181,6 +163,18 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request") << " for method: " << method << LL_ENDL; + // Nothing for a peer that has not proven itself, and nothing said of + // which methods there are. + if (!mAuthenticated) + { + LL_DEBUGS("JSONRPC") << "Refused " << method << " from a peer not authenticated" << LL_ENDL; + if (!is_notification) + { + sendError(id, UnauthorizedError()); + } + return; + } + // Resolve the handler under the mutex, then invoke it unlocked. MethodHandler handler; bool is_async = false; @@ -219,7 +213,7 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) throw InternalError("Connection expired before method " + method + " could be launched"); } - LLMainThreadTask::dispatch( + bool posted = LL::WorkQueue::postToMainLoop( [handler, method, id, params, conn]() { LLCoros::instance().launch( @@ -255,7 +249,9 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) { if (conn->isConnected()) { - conn->sendError(id, InternalError(e.what())); + LL_WARNS("JSONRPC") << "Exception in async method " << method + << ": " << e.what() << LL_ENDL; + conn->sendError(id, InternalError()); } else { @@ -265,6 +261,11 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) } }); }); + if (!posted) + { + LL_WARNS("JSONRPC") << "Main loop unavailable; failing async method " << method << LL_ENDL; + throw InternalError("Viewer is shutting down; cannot service " + method); + } return; } @@ -302,7 +303,9 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) { if (!is_notification) { - sendError(id, InternalError(e.what())); + LL_WARNS("JSONRPC") << "Exception in handler for " << method + << ": " << e.what() << LL_ENDL; + sendError(id, InternalError()); } else { @@ -446,8 +449,7 @@ void LLJSONRPCConnection::sweepTimeouts() for (auto& [id, callback] : expired) { - LL_WARNS("JSONRPC") << "Request " << id << " timed out after " - << REQUEST_TIMEOUT_SECONDS << " seconds" << LL_ENDL; + LL_WARNS("JSONRPC") << "Request " << id << " timed out" << LL_ENDL; if (callback) { LLSD error; @@ -566,7 +568,7 @@ LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id, return env; } -LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback) +LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback, F64 timeout) { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LLSD id = generateId(); @@ -579,7 +581,7 @@ LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, Re { LLMutexLock lock(&mMutex); mPendingRequests[id_str] = std::move(callback); - mPendingDeadlines.push({ LLTimer::getTotalSeconds() + REQUEST_TIMEOUT_SECONDS, id_str }); + mPendingDeadlines.push({ LLTimer::getTotalSeconds() + timeout, id_str }); } // Send the request @@ -715,6 +717,25 @@ void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr << getConnectionCount() << LL_ENDL; } +bool LLJSONRPCServer::update() +{ + // Main thread, once per frame; throttled to TIMEOUT_SWEEP_INTERVAL. + if (mSweepTimer.getElapsedTimeF32() < LLJSONRPCConnection::TIMEOUT_SWEEP_INTERVAL) + { + return true; + } + mSweepTimer.reset(); + + for (const auto& conn : getConnections()) + { + if (auto rpc = std::dynamic_pointer_cast<LLJSONRPCConnection>(conn)) + { + rpc->sweepTimeouts(); + } + } + return true; +} + void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) { LLMutexLock lock(&mGlobalMethodsMutex); diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index aa4a96be4d..98c858b022 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -29,14 +29,13 @@ #include "llwebsocketmgr.h" #include "llsd.h" #include "lluuid.h" +#include "lltimer.h" #include <functional> #include <unordered_map> #include <memory> #include <queue> -class LLEventTimer; - /** * @class LLJSONRPCConnection * @brief JSON-RPC 2.0 WebSocket connection implementation @@ -285,10 +284,23 @@ public: * @param method The method name to call * @param params The parameters to pass * @param callback Callback for the response (optional) - * @return The request ID for correlation + * @param timeout Seconds before the callback is told the call timed out + * @return The request ID for correlation, undefined where it could not + * be sent (the callback is then never called) */ LLSD call(const std::string& method, const LLSD& params = LLSD(), - ResponseCallback callback = nullptr); + ResponseCallback callback = nullptr, F64 timeout = REQUEST_TIMEOUT_SECONDS); + + /** + * @brief Whether the peer's requests are dispatched + * + * A connection starts out unauthenticated and refuses every request as + * unauthorized, whether or not the method exists, until it is told the + * peer has proven itself -- e.g. by answering a challenge. Answers to + * its own calls go through either way: that is how the peer proves + * itself. + */ + bool isAuthenticated() const { return mAuthenticated.load(); } /** * @brief Send a JSON-RPC notification (no response expected) @@ -361,6 +373,8 @@ protected: */ LLSD generateId(); + void setAuthenticated(bool authenticated) { mAuthenticated = authenticated; } + public: /** * @brief Build a JSON-RPC 2.0 envelope. @@ -390,7 +404,8 @@ private: // Per-request timeout tracking. mPendingDeadlines is a min-heap of // (deadline, request_id) ordered by deadline; entries whose request has // already been answered become tombstones (skipped when they reach the - // top). A single recurring timer per connection sweeps the heap. + // top). The owning server calls sweepTimeouts() once per frame (see + // LLJSONRPCServer::update()) to fire any expired entries. struct PendingDeadline { F64 mDeadline; // absolute time in seconds (LLTimer::getTotalSeconds) @@ -399,16 +414,20 @@ private: bool operator<(const PendingDeadline& rhs) const { return mDeadline > rhs.mDeadline; } }; std::priority_queue<PendingDeadline> mPendingDeadlines; - std::weak_ptr<LLEventTimer> mTimeoutTimer; + + std::atomic<bool> mAuthenticated{ false }; static constexpr F64 REQUEST_TIMEOUT_SECONDS = 120.0; - static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f; - /// Invoked by the sweep timer; fires the timeout callback for any - /// request whose deadline has passed. Safe to call from the main thread. +public: + /// Sweep interval used by LLJSONRPCServer::update() to throttle sweepTimeouts(). + static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f; + + /// Invoked once per frame by the owning server (see LLJSONRPCServer::update()); + /// fires the timeout callback for any request whose deadline has passed. + /// Main-thread only. void sweepTimeouts(); -public: void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback); void testSweepTimeouts(); size_t testPendingRequestCount() const; @@ -497,12 +516,15 @@ public: */ LLSD getServerStats() const; + bool update() override; + protected: LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override; virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection, const LLSD& params) const; + virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection, const LLSD& params) const = 0; virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection, @@ -521,4 +543,5 @@ private: std::string mServerName; // Store server name for stats std::atomic<U64> mTotalRequestsHandled{0}; std::atomic<U64> mTotalNotificationsSent{0}; + LLTimer mSweepTimer; }; diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp index be1ab54efd..ef07295eed 100644 --- a/indra/llcorehttp/llwebsocketmgr.cpp +++ b/indra/llcorehttp/llwebsocketmgr.cpp @@ -31,6 +31,7 @@ #include "llsdserialize.h" #include "llhost.h" #include "llsdjson.h" +#include "lltimer.h" #include <websocketpp/config/boost_config.hpp> #include <websocketpp/config/asio_no_tls.hpp> @@ -47,6 +48,10 @@ namespace using Server_t = websocketpp::server<websocketpp::config::asio>; using Client_t = websocketpp::client<websocketpp::config::asio>; using Connection_t = websocketpp::connection<websocketpp::config::asio>; + + // How long stop() waits for the I/O thread to exit before logging a + // warning. It still joins after this -- this is diagnostic, not a timeout. + constexpr F32 SERVER_STOP_WARN_SECONDS = 5.0f; } //------------------------------------------------------------------------ @@ -190,6 +195,7 @@ struct Server_impl mLocalOnly(local_only) { + mServer.set_validate_handler([this](websocketpp::connection_hdl hdl) { return this->onValidate(hdl); }); mServer.set_open_handler([this](websocketpp::connection_hdl hdl) { this->onOpen(hdl); }); mServer.set_close_handler([this](websocketpp::connection_hdl hdl) { this->onClose(hdl); }); mServer.set_message_handler([this](websocketpp::connection_hdl hdl, Server_t::message_ptr msg) { this->onMessage(hdl, msg); }); @@ -307,6 +313,34 @@ struct Server_impl } /** + * @brief Decide whether an upgrade request is let in + * @param hdl WebSocket connection handle from websocketpp + * @return false to answer the request 403 and never open it + * + * Called by websocketpp once the request has been read, before it is + * answered: the owner says which origins it takes. + */ + bool onValidate(websocketpp::connection_hdl hdl) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; + websocketpp::lib::error_code ec; + Server_t::connection_ptr con = mServer.get_con_from_hdl(hdl, ec); + if (ec || !con) + { + return false; + } + const std::string& origin = con->get_origin(); + if (mOwner->acceptOrigin(origin)) + { + return true; + } + // Once for each origin: a page can ask as often as it likes. + LL_WARNS_ONCE("WebSocket") << mOwner->mServerName << " refused a connection from origin " << origin << LL_ENDL; + con->set_status(websocketpp::http::status_code::forbidden); + return false; + } + + /** * @brief Handle new connection establishment event * @param hdl WebSocket connection handle from websocketpp * @@ -407,9 +441,11 @@ bool LLWebsocketMgr::WSServer::start() // Reset the stop flag mShouldStop = false; + mRunning = true; // Start the server thread mServerThread = std::thread([this]() { + LL_PROFILER_SET_THREAD_NAME(mServerName.c_str()); LL_INFOS("WebSocket") << "WebSocket server thread starting for: " << mServerName << LL_ENDL; // Run the controlled server loop that checks the stop flag @@ -422,6 +458,7 @@ bool LLWebsocketMgr::WSServer::start() } LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL; + mRunning = false; }); onStarted(); @@ -455,6 +492,22 @@ void LLWebsocketMgr::WSServer::stop() if (mServerThread.joinable()) { + // The I/O thread should exit promptly once mImpl->stop() returns. If + // it doesn't, something on that thread is blocked -- most likely a + // regression reintroducing a cross-thread wait. Warn instead of + // hanging silently; still join (detaching would use-after-free mImpl). + LLTimer wait_timer; + while (mRunning && wait_timer.getElapsedTimeF32() < SERVER_STOP_WARN_SECONDS) + { + ms_sleep(10); + } + if (mRunning) + { + LL_WARNS("WebSocket") << mServerName << ": I/O thread did not exit within " + << SERVER_STOP_WARN_SECONDS << "s; joining anyway. This indicates a " + << "thread-boundary regression." << LL_ENDL; + } + mServerThread.join(); LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL; } @@ -463,10 +516,11 @@ void LLWebsocketMgr::WSServer::stop() bool LLWebsocketMgr::WSServer::isRunning() const { - LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; - - // Check both the thread state, websocket server state, and the stop flag - return mServerThread.joinable() && !mImpl->mServer.stopped() && !mShouldStop; + // mRunning is set true just before the I/O thread launches and false as + // the last thing that thread does before exiting -- lock-free, and safe + // to call while start()/stop() hold mThreadMutex on this thread (unlike + // a mutex-based check, which would self-deadlock there). + return mRunning && !mShouldStop; } void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) @@ -540,6 +594,18 @@ LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(cons return nullptr; } +std::vector<LLWebsocketMgr::WSConnection::ptr_t> LLWebsocketMgr::WSServer::getConnections() const +{ + LLMutexLock lock(&mConnectionMutex); + std::vector<WSConnection::ptr_t> result; + result.reserve(mConnections.size()); + for (const auto& [handle, conn] : mConnections) + { + result.push_back(conn); + } + return result; +} + LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const { websocketpp::lib::error_code ec; diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h index 2c335307e3..19ceb5feed 100644 --- a/indra/llcorehttp/llwebsocketmgr.h +++ b/indra/llcorehttp/llwebsocketmgr.h @@ -122,7 +122,7 @@ public: * Sends a text message to the remote endpoint. The message is queued * asynchronously and may not be sent immediately. */ - bool sendMessage(const std::string& message) const; + virtual bool sendMessage(const std::string& message) const; bool sendMessage(const boost::json::value& json) const; bool sendMessage(const LLSD& data) const; @@ -243,9 +243,26 @@ public: void broadcastMessage(const std::string& message); virtual bool update() { return true; } + /** + * @brief Whether an upgrade request from this origin is let in + * @param origin The request's Origin header, empty where it had none + * + * A browser sends the page's origin with every WebSocket it opens, + * and nothing stops a page -- or media on a prim, in the viewer's + * own browser -- from opening one to 127.0.0.1. A program, such as + * an editor's extension, sends none. By default only those are let + * in; a request refused is answered 403 and never opens. Called on + * the server's thread. + */ + virtual bool acceptOrigin(const std::string& origin) const { return origin.empty(); } + connection_state_t getConnectionState(const connection_h& handle) const; protected: + /// Snapshot of live connections. Safe to iterate after the lock is + /// released -- these are real shared_ptrs, not weak references. + std::vector<WSConnection::ptr_t> getConnections() const; + virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle); bool start(); @@ -283,6 +300,7 @@ public: // Threading support std::thread mServerThread; ///< Thread running the ASIO event loop std::atomic<bool> mShouldStop{ false }; ///< Thread-safe stop flag + std::atomic<bool> mRunning{ false }; ///< True from just before the I/O thread launches until it exits mutable LLMutex mThreadMutex; ///< Mutex for thread synchronization }; diff --git a/indra/llcorehttp/tests/jsonrpcws_test.cpp b/indra/llcorehttp/tests/jsonrpcws_test.cpp new file mode 100644 index 0000000000..c07cb6f4fd --- /dev/null +++ b/indra/llcorehttp/tests/jsonrpcws_test.cpp @@ -0,0 +1,553 @@ +/** + * @file jsonrpcws_test.cpp + * @brief Security-focused unit tests for LLJSONRPCConnection / LLJSONRPCServer + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "lljsonrpcws.h" +#include "llsdjson.h" + +#include "../test/lltut.h" + +#include <boost/json.hpp> +#include <vector> + +namespace +{ + // Exposes the protected test-only members LLJSONRPCConnection provides + // for exactly this purpose, on a connection with no real transport. + class TestJSONRPCConnection : public LLJSONRPCConnection + { + public: + TestJSONRPCConnection() + : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h()) + { + } + + using LLJSONRPCConnection::processMessage; + using LLJSONRPCConnection::validateMessage; + using LLJSONRPCConnection::testInjectPendingRequest; + using LLJSONRPCConnection::testPendingRequestCount; + using LLJSONRPCConnection::testSweepTimeouts; + using LLJSONRPCConnection::setAuthenticated; + }; + + // Minimal concrete server, used only to register the built-in + // system.* methods on a connection via setupConnectionMethods() -- + // LLJSONRPCServer itself is abstract (handleGetVersion is pure virtual). + // Never started, so port 0 is fine. + class TestJSONRPCServer : public LLJSONRPCServer + { + public: + TestJSONRPCServer() : LLJSONRPCServer("test_jsonrpcws_server", 0, true) {} + + using LLJSONRPCServer::setupConnectionMethods; + + LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t&, const LLSD&) const override + { + return LLSD(); + } + }; + + // Captures what would be sent to the peer, since there is no real + // transport to observe it on. + class CapturingJSONRPCConnection : public TestJSONRPCConnection + { + public: + mutable std::vector<std::string> mSent; + + bool sendMessage(const std::string& message) const override + { + mSent.push_back(message); + return true; + } + }; +} + +namespace tut +{ + struct jsonrpcws_data + { + }; + + typedef test_group<jsonrpcws_data> jsonrpcws_test; + typedef jsonrpcws_test::object jsonrpcws_object; + jsonrpcws_test jsonrpcws_testcase("LLJSONRPCConnection"); + + template<> template<> + void jsonrpcws_object::test<1>() + { + set_test_name("an unauthenticated connection dispatches neither requests nor notifications"); + + TestJSONRPCConnection conn; + conn.setAuthenticated(false); + + int calls = 0; + conn.registerMethod("object.list", + [&](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + ++calls; + return LLSD(); + }); + + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(request); + + const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(notification); + + ensure_equals("neither a request nor a notification reaches the handler", calls, 0); + } + + template<> template<> + void jsonrpcws_object::test<2>() + { + set_test_name("once authenticated, requests and notifications dispatch normally"); + + TestJSONRPCConnection conn; + conn.setAuthenticated(true); + ensure("authenticated after the peer proves itself", conn.isAuthenticated()); + + int calls = 0; + conn.registerMethod("object.list", + [&](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + ++calls; + return LLSD(); + }); + + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(request); + ensure_equals("the request reaches the handler", calls, 1); + + const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(notification); + ensure_equals("the notification also reaches the handler", calls, 2); + } + + template<> template<> + void jsonrpcws_object::test<3>() + { + set_test_name("an unauthenticated connection still receives the answers to its own calls"); + + TestJSONRPCConnection conn; + ensure("unauthenticated by default", !conn.isAuthenticated()); + + LLSD answer; + conn.testInjectPendingRequest( + "rpc_1", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD& result, const LLSD& error) + { + ensure("no error", error.isUndefined()); + answer = result; + }); + + LLSD result; + result["challenge_response"] = "secret"; + conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD())); + ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret")); + ensure("and it is answered", conn.testPendingRequestCount() == 0); + } + + template<> template<> + void jsonrpcws_object::test<4>() + { + set_test_name("an unauthenticated request is answered -32002; a notification gets nothing back"); + + CapturingJSONRPCConnection conn; + ensure("unauthenticated by default", !conn.isAuthenticated()); + + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(request); + ensure_equals("exactly one message sent for the request", conn.mSent.size(), (size_t)1); + + boost::system::error_code ec; + const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("unauthorized error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED); + + const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(notification); + ensure_equals("nothing sent for a notification", conn.mSent.size(), (size_t)1); + } + + template<> template<> + void jsonrpcws_object::test<5>() + { + set_test_name("malformed JSON is answered with a parse error, and the connection survives"); + + CapturingJSONRPCConnection conn; + conn.setAuthenticated(true); + + conn.onMessage("{not valid json"); + ensure_equals("a parse error is sent", conn.mSent.size(), (size_t)1); + + boost::system::error_code ec; + const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("parse error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::PARSE_ERROR); + + // The connection itself is unaffected: valid follow-up traffic still works. + int calls = 0; + conn.registerMethod("object.list", + [&](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + ++calls; + return LLSD(); + }); + conn.onMessage(boost::json::serialize(LlsdToJson(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD())))); + ensure_equals("a later valid request still dispatches", calls, 1); + } + + template<> template<> + void jsonrpcws_object::test<6>() + { + set_test_name("a batch request is rejected outright; batch support is intentionally disabled"); + + CapturingJSONRPCConnection conn; + conn.setAuthenticated(true); + + const LLSD one = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + LLSD batch = LLSD::emptyArray(); + batch.append(one); + batch.append(one); + + conn.onMessage(boost::json::serialize(LlsdToJson(batch))); + ensure_equals("a single error is sent for the whole batch", conn.mSent.size(), (size_t)1); + + boost::system::error_code ec; + const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("invalid request error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INVALID_REQUEST); + } + + template<> template<> + void jsonrpcws_object::test<7>() + { + set_test_name("a missing or wrong jsonrpc version is rejected by validateMessage"); + + TestJSONRPCConnection conn; + + LLSD missing_version; + missing_version["method"] = "object.list"; + ensure("missing version fails validation", !conn.validateMessage(missing_version, true)); + + LLSD wrong_version = missing_version; + wrong_version["jsonrpc"] = "1.0"; + ensure("wrong version fails validation", !conn.validateMessage(wrong_version, true)); + + LLSD correct_version = missing_version; + correct_version["jsonrpc"] = "2.0"; + ensure("correct version passes validation", conn.validateMessage(correct_version, true)); + } + + template<> template<> + void jsonrpcws_object::test<8>() + { + set_test_name("params of the wrong type are rejected; the handler is never invoked"); + + TestJSONRPCConnection conn; + conn.setAuthenticated(true); + + LLSD bad_params; + bad_params["jsonrpc"] = "2.0"; + bad_params["method"] = "object.list"; + bad_params["params"] = "not-an-array-or-object"; + ensure("string params fail validation", !conn.validateMessage(bad_params, true)); + + int calls = 0; + conn.registerMethod("object.list", + [&](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + ++calls; + return LLSD(); + }); + + conn.processMessage(bad_params); + ensure_equals("the handler is never invoked", calls, 0); + + LLSD good_params = bad_params; + good_params["params"] = LLSD::emptyMap(); + conn.processMessage(good_params); + ensure_equals("valid params do dispatch", calls, 1); + } + + template<> template<> + void jsonrpcws_object::test<9>() + { + set_test_name("a response for an unknown id is silently ignored"); + + TestJSONRPCConnection conn; + + bool callback_called = false; + conn.testInjectPendingRequest( + "req_1", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD&, const LLSD&) + { + callback_called = true; + }); + + LLSD result; + result["value"] = "ok"; + conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("unknown_id"), std::string(), LLSD(), result, LLSD())); + + ensure("no callback fired for an unrelated id", !callback_called); + ensure_equals("the real pending request is still tracked", conn.testPendingRequestCount(), (size_t)1); + + conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), result, LLSD())); + ensure("the callback fires for its own id", callback_called); + ensure_equals("and it is removed once answered", conn.testPendingRequestCount(), (size_t)0); + } + + template<> template<> + void jsonrpcws_object::test<10>() + { + set_test_name("a response with both result and error, or with neither, is rejected as invalid"); + + TestJSONRPCConnection conn; + + LLSD both; + both["jsonrpc"] = "2.0"; + both["id"] = "req_1"; + both["result"] = "ok"; + both["error"]["code"] = -1; + both["error"]["message"] = "bad"; + ensure("result and error together fail validation", !conn.validateMessage(both, false)); + + LLSD neither; + neither["jsonrpc"] = "2.0"; + neither["id"] = "req_1"; + ensure("neither result nor error fails validation", !conn.validateMessage(neither, false)); + + LLSD result_only; + result_only["jsonrpc"] = "2.0"; + result_only["id"] = "req_1"; + result_only["result"] = "ok"; + ensure("result alone passes validation", conn.validateMessage(result_only, false)); + + bool callback_called = false; + conn.testInjectPendingRequest( + "req_1", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD&, const LLSD&) + { + callback_called = true; + }); + + conn.processMessage(both); + ensure("an invalid response never reaches the callback", !callback_called); + ensure_equals("the pending request is still tracked", conn.testPendingRequestCount(), (size_t)1); + } + + template<> template<> + void jsonrpcws_object::test<11>() + { + set_test_name("an unauthenticated peer is told unauthorized, never that the method doesn't exist"); + + CapturingJSONRPCConnection conn; + + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "no.such.method", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(request); + ensure_equals("one message sent", conn.mSent.size(), (size_t)1); + + boost::system::error_code ec; + LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("unauthorized, not method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED); + + conn.setAuthenticated(true); + conn.processMessage(request); + ensure_equals("a second message sent once authenticated", conn.mSent.size(), (size_t)2); + + sent = LlsdFromJson(boost::json::parse(conn.mSent[1], ec)); + ensure("parsed without error", !ec); + ensure_equals("now it is method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::METHOD_NOT_FOUND); + } + + template<> template<> + void jsonrpcws_object::test<12>() + { + set_test_name("unregisterMethod removes both sync and async handlers"); + + TestJSONRPCConnection conn; + + conn.registerMethod("sync.method", + [](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + return LLSD(); + }); + conn.registerAsyncMethod("async.method", + [](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + return LLSD(); + }); + + ensure_equals("sync method registered", conn.getMethods().count("sync.method"), (size_t)1); + ensure_equals("async method registered", conn.getMethods().count("async.method"), (size_t)1); + + conn.unregisterMethod("sync.method"); + conn.unregisterMethod("async.method"); + + ensure_equals("sync method gone", conn.getMethods().count("sync.method"), (size_t)0); + ensure_equals("async method gone", conn.getMethods().count("async.method"), (size_t)0); + } + + template<> template<> + void jsonrpcws_object::test<13>() + { + set_test_name("a handler throwing a plain std::exception is converted to a generic internal error"); + + CapturingJSONRPCConnection conn; + conn.setAuthenticated(true); + + conn.registerMethod("boom", + [](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + throw std::runtime_error("boom detail"); + }); + + conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "boom", LLSD::emptyMap(), LLSD(), LLSD())); + ensure_equals("one message sent", conn.mSent.size(), (size_t)1); + + boost::system::error_code ec; + const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("internal error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INTERNAL_ERROR); + + // The exception's own message must never reach the client -- it can + // carry paths, connection strings, or other internal detail. + ensure("the exception's own message is not forwarded to the client", + sent["error"]["message"].asString().find("boom detail") == std::string::npos); + } + + template<> template<> + void jsonrpcws_object::test<14>() + { + set_test_name("call() with no way to send returns undefined and leaves nothing pending"); + + TestJSONRPCConnection conn; + + bool callback_called = false; + const LLSD id = conn.call("object.list", LLSD::emptyMap(), + [&](const LLSD&, const LLSD&) + { + callback_called = true; + }); + + ensure("call() reports it could not send", id.isUndefined()); + ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0); + ensure("the callback is never invoked for an unsent call", !callback_called); + } + + template<> template<> + void jsonrpcws_object::test<15>() + { + set_test_name("the built-in system.* methods are gated by authentication like any other method"); + + TestJSONRPCServer server; + auto conn = std::make_shared<CapturingJSONRPCConnection>(); + server.setupConnectionMethods(conn); + + ensure_equals("system.listMethods is registered", conn->getMethods().count("system.listMethods"), (size_t)1); + ensure_equals("system.getStats is registered", conn->getMethods().count("system.getStats"), (size_t)1); + + conn->processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "system.listMethods", LLSD::emptyMap(), LLSD(), LLSD())); + ensure_equals("one message sent", conn->mSent.size(), (size_t)1); + + boost::system::error_code ec; + const LLSD sent = LlsdFromJson(boost::json::parse(conn->mSent[0], ec)); + ensure("parsed without error", !ec); + ensure_equals("unauthenticated, even a built-in method is refused", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED); + } + + template<> template<> + void jsonrpcws_object::test<16>() + { + set_test_name("makeEnvelope includes id and method by the documented rules"); + + // Notification: id undefined, method non-empty -> id omitted. + const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + ensure("notification omits id", !notification.has("id")); + ensure_equals("notification keeps method", notification["method"].asString(), std::string("object.list")); + + // Request: id defined, method non-empty -> both kept. + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + ensure("request keeps id", request.has("id")); + ensure_equals("request keeps method", request["method"].asString(), std::string("object.list")); + + // Success response: id defined, method empty -> id kept, method omitted. + const LLSD response = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), LLSD("ok"), LLSD()); + ensure("response keeps id", response.has("id")); + ensure("response omits method", !response.has("method")); + ensure_equals("response keeps result", response["result"].asString(), std::string("ok")); + + // Error response with an undefined id (e.g. a parse error): method + // is empty, so id is kept even though undefined -- serializes as null. + LLSD error_obj; + error_obj["code"] = -32700; + error_obj["message"] = "Parse error"; + const LLSD error_response = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD(), error_obj); + ensure("error response keeps id even when undefined", error_response.has("id")); + ensure("that id is null", error_response["id"].isUndefined()); + ensure("error response omits method", !error_response.has("method")); + } + + template<> template<> + void jsonrpcws_object::test<17>() + { + set_test_name("onClose fails every pending request with CONNECTION_CLOSED, exactly once"); + + TestJSONRPCConnection conn; + + int callback_count_a = 0; + int callback_count_b = 0; + conn.testInjectPendingRequest( + "req_a", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD& result, const LLSD& error) + { + ++callback_count_a; + ensure("no result on connection close", result.isUndefined()); + ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED); + }); + conn.testInjectPendingRequest( + "req_b", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD&, const LLSD& error) + { + ++callback_count_b; + ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED); + }); + ensure_equals("two requests pending", conn.testPendingRequestCount(), (size_t)2); + + conn.onClose(); + + ensure_equals("first callback fires exactly once", callback_count_a, 1); + ensure_equals("second callback fires exactly once", callback_count_b, 1); + ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0); + } +} diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp index 65d7fe93a9..cc3c241a96 100644 --- a/indra/llcorehttp/tests/llcorehttp_test.cpp +++ b/indra/llcorehttp/tests/llcorehttp_test.cpp @@ -45,6 +45,7 @@ #include "test_httpheaders.hpp" #include "test_httprequestqueue.hpp" #include "test_jsonrpcws.hpp" +#include "test_websocketmgr.hpp" #include "_httpservice.h" #include "llproxy.h" diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp index 6cf7932aa0..a40d0117b8 100644 --- a/indra/llcorehttp/tests/test_jsonrpcws.hpp +++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp @@ -24,6 +24,7 @@ public: using LLJSONRPCConnection::testInjectPendingRequest; using LLJSONRPCConnection::testPendingRequestCount; using LLJSONRPCConnection::testSweepTimeouts; + using LLJSONRPCConnection::setAuthenticated; }; } @@ -91,6 +92,7 @@ namespace tut set_test_name("processMessage dispatches notification handler"); TestJSONRPCConnection conn; + conn.setAuthenticated(true); bool called = false; LLSD seen_id; @@ -121,6 +123,7 @@ namespace tut set_test_name("sweepTimeouts expires overdue callbacks"); TestJSONRPCConnection conn; + conn.setAuthenticated(true); bool callback_called = false; conn.testInjectPendingRequest( @@ -141,6 +144,60 @@ namespace tut ensure("timeout callback should be called", callback_called); ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0); } + + template<> template<> + void JSONRPCWSTestObjectType::test<6>() + { + set_test_name("a connection not yet authenticated dispatches nothing, and everything once it is"); + + TestJSONRPCConnection conn; + ensure("unauthenticated unless told otherwise", !conn.isAuthenticated()); + + int calls = 0; + conn.registerMethod("object.list", + [&](const std::string&, const LLSD&, const LLSD&) -> LLSD + { + ++calls; + return LLSD(); + }); + + conn.setAuthenticated(false); + ensure("not authenticated", !conn.isAuthenticated()); + const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(request); + const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD()); + conn.processMessage(notification); + ensure_equals("neither a request nor a notification reaches the handler", calls, 0); + + conn.setAuthenticated(true); + conn.processMessage(request); + ensure_equals("dispatched once authenticated", calls, 1); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<7>() + { + set_test_name("a connection not yet authenticated still takes the answers to its own calls"); + + TestJSONRPCConnection conn; + conn.setAuthenticated(false); + + LLSD answer; + conn.testInjectPendingRequest( + "rpc_1", + LLTimer::getTotalSeconds() + 60.0, + [&](const LLSD& result, const LLSD& error) + { + ensure("no error", error.isUndefined()); + answer = result; + }); + + LLSD result; + result["challenge_response"] = "secret"; + conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD())); + ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret")); + ensure("and it is answered", conn.testPendingRequestCount() == 0); + } } #endif
\ No newline at end of file diff --git a/indra/llcorehttp/tests/test_websocketmgr.hpp b/indra/llcorehttp/tests/test_websocketmgr.hpp new file mode 100644 index 0000000000..38b3d79cbf --- /dev/null +++ b/indra/llcorehttp/tests/test_websocketmgr.hpp @@ -0,0 +1,157 @@ +/** + * @file test_websocketmgr.hpp + * @brief A WebSocket server lets programs in and turns pages away. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef TEST_LLCORE_WEBSOCKETMGR_H_ +#define TEST_LLCORE_WEBSOCKETMGR_H_ + +#include "llwebsocketmgr.h" + +#include <boost/asio/io_context.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/asio/read_until.hpp> +#include <boost/asio/streambuf.hpp> +#include <boost/asio/write.hpp> + +#include <chrono> +#include <istream> +#include <random> + +namespace +{ + // A port nothing on this machine is listening on, as far as can be + // told; zero where none turned up. + U16 unusedPort() + { + std::random_device random; + for (int tries = 0; tries < 20; ++tries) + { + const U16 port = static_cast<U16>(40000 + random() % 20000); + boost::asio::io_context io; + boost::asio::ip::tcp::acceptor acceptor(io); + boost::system::error_code ec; + acceptor.open(boost::asio::ip::tcp::v4(), ec); + if (!ec) + { + acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec); + } + if (!ec) + { + return port; + } + } + return 0; + } + + // The HTTP status a WebSocket upgrade request from this origin -- none, + // where empty -- is answered with: 101 where it would open, -1 where + // nothing answered at all. The request is what a browser or a program + // sends, written out, so that what is tested is what the server says. + int upgradeFrom(U16 port, const std::string& origin) + { + boost::asio::io_context io; + boost::asio::ip::tcp::socket socket(io); + boost::system::error_code ec; + socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec); + if (ec) + { + return -1; + } + + std::string request = "GET / HTTP/1.1\r\n" + "Host: 127.0.0.1:" + std::to_string(port) + "\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Sec-WebSocket-Version: 13\r\n"; + if (!origin.empty()) + { + request += "Origin: " + origin + "\r\n"; + } + request += "\r\n"; + boost::asio::write(socket, boost::asio::buffer(request), ec); + if (ec) + { + return -1; + } + + // The status line, or nothing within the time. + boost::asio::streambuf response; + bool answered = false; + boost::asio::async_read_until(socket, response, "\r\n", + [&](const boost::system::error_code& error, std::size_t) { answered = !error; }); + io.run_for(std::chrono::seconds(10)); + socket.close(ec); + if (!answered) + { + return -1; + } + std::istream line(&response); + std::string version; + int status = -1; + line >> version >> status; + return status; + } +} + +namespace tut +{ + struct WebsocketMgrTestData + { + }; + + typedef test_group<WebsocketMgrTestData> WebsocketMgrTestGroupType; + typedef WebsocketMgrTestGroupType::object WebsocketMgrTestObjectType; + WebsocketMgrTestGroupType WebsocketMgrTestGroup("LLWebsocketMgr Tests"); + + template<> template<> + void WebsocketMgrTestObjectType::test<1>() + { + set_test_name("a program, which sends no origin, is let in; a page, which always does, is refused"); + + const U16 port = unusedPort(); + if (!port) + { + skip("no port free to listen on"); + } + const std::string name = "origin_test"; + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + ensure("added", manager.addServer(std::make_shared<LLWebsocketMgr::WSServer>(name, port, true))); + ensure("started", manager.startServer(name)); + + const int program = upgradeFrom(port, std::string()); + const int page = upgradeFrom(port, "https://example.com"); + const int opaque = upgradeFrom(port, "null"); + const int local = upgradeFrom(port, "http://127.0.0.1:8080"); + manager.removeServer(name); + + ensure_equals("no origin: in", program, 101); + ensure_equals("a web page: refused", page, 403); + ensure_equals("a page with an opaque origin, a sandboxed frame or a file: refused", opaque, 403); + ensure_equals("a page served from this machine: refused", local, 403); + } +} + +#endif diff --git a/indra/llcorehttp/tests/testwsclient.h b/indra/llcorehttp/tests/testwsclient.h new file mode 100644 index 0000000000..587934f2b6 --- /dev/null +++ b/indra/llcorehttp/tests/testwsclient.h @@ -0,0 +1,170 @@ +/** + * @file testwsclient.h + * @brief Minimal WebSocket client for driving a server under test + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_TESTWSCLIENT_H +#define LL_TESTWSCLIENT_H + +// MSVC does not report __cplusplus >= 201103L without /Zc:__cplusplus, so +// websocketpp's own C++11 detection would otherwise fall back to +// boost::random_device, which nothing here links. +#define _WEBSOCKETPP_CPP11_RANDOM_DEVICE_ + +#include <websocketpp/config/asio_no_tls_client.hpp> +#include <websocketpp/client.hpp> + +#include <chrono> +#include <condition_variable> +#include <mutex> +#include <string> +#include <thread> +#include <vector> + +// A minimal WebSocket client for driving a server under test: connects, +// optionally with an Origin header, and can send and collect messages. +class TestWSClient +{ +public: + using client_t = websocketpp::client<websocketpp::config::asio_client>; + + TestWSClient() + { + mClient.clear_access_channels(websocketpp::log::alevel::all); + mClient.clear_error_channels(websocketpp::log::elevel::all); + mClient.init_asio(); + + mClient.set_open_handler([this](websocketpp::connection_hdl hdl) + { + std::lock_guard<std::mutex> lock(mMutex); + mHandle = hdl; + mOpened = true; + mCondVar.notify_all(); + }); + mClient.set_fail_handler([this](websocketpp::connection_hdl) + { + std::lock_guard<std::mutex> lock(mMutex); + mFailed = true; + mCondVar.notify_all(); + }); + mClient.set_close_handler([this](websocketpp::connection_hdl) + { + std::lock_guard<std::mutex> lock(mMutex); + mClosed = true; + mCondVar.notify_all(); + }); + mClient.set_message_handler([this](websocketpp::connection_hdl, client_t::message_ptr msg) + { + std::lock_guard<std::mutex> lock(mMutex); + mReceived.push_back(msg->get_payload()); + mCondVar.notify_all(); + }); + } + + ~TestWSClient() + { + close(); + mClient.stop(); + if (mThread.joinable()) + { + mThread.join(); + } + } + + // Initiates a close and waits for the ack, so teardown looks like a + // normal closure instead of a dropped socket. Safe to call more than + // once, and safe if the connection never opened. + void close(std::chrono::seconds timeout = std::chrono::seconds(2)) + { + std::unique_lock<std::mutex> lock(mMutex); + if (!mOpened || mClosed) + { + return; + } + websocketpp::lib::error_code ec; + mClient.close(mHandle, websocketpp::close::status::normal, "test done", ec); + mCondVar.wait_for(lock, timeout, [this]() { return mClosed; }); + } + + // Connects to 127.0.0.1:port, optionally with an Origin header, and + // waits up to timeout for the handshake to open or fail. + // force_origin_header sends "Origin:" with an empty value instead of + // omitting the header entirely, for origin.empty(). + bool connect(U16 port, const std::string& origin, bool force_origin_header = false, + std::chrono::seconds timeout = std::chrono::seconds(5)) + { + websocketpp::lib::error_code ec; + client_t::connection_ptr con = mClient.get_connection( + "ws://127.0.0.1:" + std::to_string(port) + "/", ec); + if (ec) + { + return false; + } + if (!origin.empty() || force_origin_header) + { + con->append_header("Origin", origin); + } + mClient.connect(con); + mThread = std::thread([this]() { mClient.run(); }); + + std::unique_lock<std::mutex> lock(mMutex); + return mCondVar.wait_for(lock, timeout, [this]() { return mOpened || mFailed; }) && mOpened; + } + + bool send(const std::string& message) + { + websocketpp::lib::error_code ec; + mClient.send(mHandle, message, websocketpp::frame::opcode::text, ec); + return !ec; + } + + // Waits up to timeout for at least one message to arrive, then + // removes and returns it; empty where none arrived in time. + std::string waitForMessage(std::chrono::seconds timeout = std::chrono::seconds(5)) + { + std::unique_lock<std::mutex> lock(mMutex); + if (!mCondVar.wait_for(lock, timeout, [this]() { return !mReceived.empty(); })) + { + return std::string(); + } + std::string message = mReceived.front(); + mReceived.erase(mReceived.begin()); + return message; + } + + bool isOpen() const { return mOpened && !mClosed; } + +private: + client_t mClient; + websocketpp::connection_hdl mHandle; + std::thread mThread; + std::mutex mMutex; + std::condition_variable mCondVar; + bool mOpened{ false }; + bool mFailed{ false }; + bool mClosed{ false }; + std::vector<std::string> mReceived; +}; + +#endif // LL_TESTWSCLIENT_H diff --git a/indra/llcorehttp/tests/wsserver_test.cpp b/indra/llcorehttp/tests/wsserver_test.cpp new file mode 100644 index 0000000000..62fa66a9cd --- /dev/null +++ b/indra/llcorehttp/tests/wsserver_test.cpp @@ -0,0 +1,438 @@ +/** + * @file wsserver_test.cpp + * @brief Security-focused unit tests for LLWebsocketMgr::WSServer + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llwebsocketmgr.h" + +#include "testwsclient.h" +#include "../test/lltut.h" + +#include <boost/asio/io_context.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/asio/ip/udp.hpp> +#include <boost/asio/read_until.hpp> +#include <boost/asio/streambuf.hpp> +#include <boost/asio/write.hpp> + +#include <atomic> +#include <chrono> +#include <istream> +#include <random> +#include <thread> + +namespace +{ + // A port nothing on this machine is listening on, as far as can be + // told; zero where none turned up. + U16 unusedPort() + { + std::random_device random; + for (int tries = 0; tries < 20; ++tries) + { + const U16 port = static_cast<U16>(40000 + random() % 20000); + boost::asio::io_context io; + boost::asio::ip::tcp::acceptor acceptor(io); + boost::system::error_code ec; + acceptor.open(boost::asio::ip::tcp::v4(), ec); + if (!ec) + { + acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec); + } + if (!ec) + { + return port; + } + } + return 0; + } + + // The HTTP status a WebSocket upgrade request from this origin -- none, + // where empty -- is answered with: 101 where it would open, -1 where + // nothing answered at all. + // force_origin_header sends "Origin:" with an empty value instead of + // omitting the header entirely, for origin.empty(). + int upgradeFrom(U16 port, const std::string& origin, bool force_origin_header = false) + { + boost::asio::io_context io; + boost::asio::ip::tcp::socket socket(io); + boost::system::error_code ec; + socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec); + if (ec) + { + return -1; + } + + std::string request = "GET / HTTP/1.1\r\n" + "Host: 127.0.0.1:" + std::to_string(port) + "\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Sec-WebSocket-Version: 13\r\n"; + if (!origin.empty() || force_origin_header) + { + request += "Origin: " + origin + "\r\n"; + } + request += "\r\n"; + boost::asio::write(socket, boost::asio::buffer(request), ec); + if (ec) + { + return -1; + } + + boost::asio::streambuf response; + bool answered = false; + boost::asio::async_read_until(socket, response, "\r\n", + [&](const boost::system::error_code& error, std::size_t) { answered = !error; }); + io.run_for(std::chrono::seconds(10)); + socket.close(ec); + if (!answered) + { + return -1; + } + std::istream line(&response); + std::string version; + int status = -1; + line >> version >> status; + return status; + } + + // Polls a predicate until it is true or the deadline passes. A client + // seeing its own handshake complete says nothing about whether the + // server's own thread has finished its side of onOpen bookkeeping yet. + template <typename Predicate> + bool waitUntil(Predicate predicate, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!predicate()) + { + if (std::chrono::steady_clock::now() >= deadline) + { + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return true; + } + + // The machine's own non-loopback IPv4 address, as seen when routing + // toward the internet (no packet is actually sent); empty where none + // could be found, such as a loopback-only sandbox. + std::string localNonLoopbackAddress() + { + boost::asio::io_context io; + boost::asio::ip::udp::socket socket(io); + boost::system::error_code ec; + socket.connect(boost::asio::ip::udp::endpoint(boost::asio::ip::make_address("8.8.8.8"), 53), ec); + if (ec) + { + return std::string(); + } + return socket.local_endpoint(ec).address().to_string(); + } + + // Whether a TCP connection to host:port succeeds within a short time; + // refused or silently dropped are both treated as "no". + bool canConnect(const std::string& host, U16 port) + { + boost::asio::io_context io; + boost::asio::ip::tcp::socket socket(io); + boost::system::error_code result = boost::asio::error::would_block; + socket.async_connect( + boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(host), port), + [&](const boost::system::error_code& ec) { result = ec; }); + io.run_for(std::chrono::seconds(3)); + boost::system::error_code close_ec; + socket.close(close_ec); + return !result; + } + + // Records what a server actually did, so tests can assert on lifecycle + // callbacks the base class leaves as no-ops. + class TestWSServer : public LLWebsocketMgr::WSServer + { + public: + using LLWebsocketMgr::WSServer::WSServer; + + std::atomic<int> mOpenedCount{ 0 }; + std::atomic<int> mClosedCount{ 0 }; + + // Empty where the base policy (reject any Origin) should apply; + // otherwise the one origin this test server lets in. + std::string mAllowedOrigin; + + void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t&) override + { + ++mOpenedCount; + } + + void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t&) override + { + ++mClosedCount; + } + + bool acceptOrigin(const std::string& origin) const override + { + if (!mAllowedOrigin.empty() && origin == mAllowedOrigin) + { + return true; + } + return WSServer::acceptOrigin(origin); + } + }; +} + +namespace tut +{ + struct wsserver_data + { + U16 mPort; + std::string mName; + + wsserver_data() : + mPort(unusedPort()), + mName("wsserver_test") + { + } + + ~wsserver_data() + { + LLWebsocketMgr::instance().removeServer(mName); + } + }; + + typedef test_group<wsserver_data> wsserver_test; + typedef wsserver_test::object wsserver_object; + wsserver_test wsserver_testcase("LLWebsocketMgr::WSServer"); + + template<> template<> + void wsserver_object::test<1>() + { + set_test_name("a program, which sends no Origin header, is let in"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("no origin: connection opens", upgradeFrom(mPort, std::string()), 101); + ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; })); + + TestWSClient client; + ensure("no origin: client connects over the network", client.connect(mPort, std::string())); + ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; })); + + client.close(); + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<2>() + { + set_test_name("a page, which sends an Origin header, is refused"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403); + + TestWSClient client; + ensure("a web page: client never opens over the network", !client.connect(mPort, "https://example.com")); + ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0); + + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<3>() + { + set_test_name("a page with an opaque origin (a sandboxed frame or a file) is refused"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("an opaque origin: refused", upgradeFrom(mPort, "null"), 403); + + TestWSClient client; + ensure("an opaque origin: client never opens over the network", !client.connect(mPort, "null")); + ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0); + + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<4>() + { + set_test_name("a page served from this machine is refused"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("a page on this machine: refused", upgradeFrom(mPort, "http://127.0.0.1:8080"), 403); + + TestWSClient client; + ensure("a page on this machine: client never opens over the network", !client.connect(mPort, "http://127.0.0.1:8080")); + ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0); + + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<5>() + { + set_test_name("an Origin header present but empty is treated the same as none"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("empty origin: connection opens", upgradeFrom(mPort, std::string(), true), 101); + ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; })); + + TestWSClient client; + ensure("empty origin: client connects over the network", client.connect(mPort, std::string(), true)); + ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; })); + + client.close(); + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<7>() + { + set_test_name("local_only binds to loopback only; the machine's own LAN address is unreachable"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + const std::string lan_address = localNonLoopbackAddress(); + if (lan_address.empty()) + { + skip("no non-loopback network interface to test against"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure("loopback: reachable", canConnect("127.0.0.1", mPort)); + ensure("LAN address: unreachable", !canConnect(lan_address, mPort)); + + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<8>() + { + set_test_name("a refused connection never reaches onConnectionOpened, and an accepted one does"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<TestWSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403); + ensure_equals("refused: onConnectionOpened never fires", server->mOpenedCount.load(), 0); + + TestWSClient client; + ensure("no origin: client connects over the network", client.connect(mPort, std::string())); + ensure("accepted: onConnectionOpened fires once", waitUntil([&]() { return server->mOpenedCount.load() == 1; })); + + client.close(); + manager.removeServer(mName); + } + + template<> template<> + void wsserver_object::test<9>() + { + set_test_name("multiple simultaneous connections are each tracked independently"); + + if (!mPort) + { + skip("no port free to listen on"); + } + + LLWebsocketMgr& manager = LLWebsocketMgr::instance(); + auto server = std::make_shared<TestWSServer>(mName, mPort, true); + ensure("added", manager.addServer(server)); + ensure("started", manager.startServer(mName)); + + TestWSClient client_a; + TestWSClient client_b; + TestWSClient client_c; + ensure("first client connects", client_a.connect(mPort, std::string())); + ensure("second client connects", client_b.connect(mPort, std::string())); + ensure("third client connects", client_c.connect(mPort, std::string())); + + ensure("server counts all three connections", waitUntil([&]() { return server->getConnectionCount() == 3; })); + ensure("onConnectionOpened fires once per connection", waitUntil([&]() { return server->mOpenedCount.load() == 3; })); + + client_a.close(); + client_b.close(); + client_c.close(); + manager.removeServer(mName); + } +} diff --git a/indra/llfilesystem/lldiriterator.cpp b/indra/llfilesystem/lldiriterator.cpp index ca00d25fb7..ec6d1726f0 100644 --- a/indra/llfilesystem/lldiriterator.cpp +++ b/indra/llfilesystem/lldiriterator.cpp @@ -69,7 +69,7 @@ LLDirIterator::Impl::Impl(const std::string &dirname, const std::string &mask) if (!is_dir) { - LL_WARNS() << "Invalid path: \"" << dir_path.string() << "\"" << LL_ENDL; + LL_WARNS() << "Invalid path: \"" << dirname << "\"" << LL_ENDL; return; } diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index e7b43e15e2..5af801a1df 100644 --- a/indra/llfilesystem/lldiskcache.cpp +++ b/indra/llfilesystem/lldiskcache.cpp @@ -34,6 +34,7 @@ #include "llapp.h" #include "llassettype.h" #include "lldir.h" +#include "llstring.h" #include <chrono> #include <filesystem> @@ -116,14 +117,14 @@ void LLDiskCache::purge() } if (std::filesystem::is_regular_file(*iter, ec) && !ec) { - if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos) + if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos) { uintmax_t file_size = std::filesystem::file_size(*iter, ec); if (ec) { continue; } - const std::string file_path = (*iter).path().string(); + const std::string file_path = fsyspath((*iter).path()).string(); const std::filesystem::file_time_type file_time = std::filesystem::last_write_time(*iter, ec); if (ec) { @@ -165,7 +166,7 @@ void LLDiskCache::purge() } if (should_remove) { - std::filesystem::remove(entry.second.second, ec); + std::filesystem::remove(fsyspath(entry.second.second), ec); if (ec) { LL_WARNS() << "Failed to delete cache file " << entry.second.second << ": " << ec.message() << LL_ENDL; @@ -245,7 +246,7 @@ void LLDiskCache::clearCache() { if (std::filesystem::is_regular_file(*iter, ec) && !ec) { - if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos) + if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos) { std::filesystem::remove(*iter, ec); if (ec) @@ -274,8 +275,9 @@ void LLDiskCache::removeOldVFSFiles() { if (std::filesystem::is_regular_file(*iter, ec) && !ec) { - if (((*iter).path().string().find(CACHE_FORMAT) != std::string::npos) || - ((*iter).path().string().find(DB_FORMAT) != std::string::npos)) + const std::string file_path = fsyspath((*iter).path()).string(); + if ((file_path.find(CACHE_FORMAT) != std::string::npos) || + (file_path.find(DB_FORMAT) != std::string::npos)) { std::filesystem::remove(*iter, ec); if (ec) @@ -311,7 +313,7 @@ uintmax_t LLDiskCache::dirFileSize(const std::string& dir) { if (std::filesystem::is_regular_file(*iter, ec) && !ec) { - if ((*iter).path().string().find(CACHE_FILENAME_PREFIX) != std::string::npos) + if (fsyspath((*iter).path()).string().find(CACHE_FILENAME_PREFIX) != std::string::npos) { uintmax_t file_size = std::filesystem::file_size(*iter, ec); if (!ec) diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 09a0ce0b7a..33ec074029 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -1976,8 +1976,6 @@ LLVolume::LLVolume(const LLVolumeParams ¶ms, const F32 detail, const bool ge mNumHullIndices = 0; // set defaults - mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); - if (mParams.getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE) { mPathp = new LLDynamicPath(); @@ -2005,13 +2003,6 @@ void LLVolume::resizePath(S32 length) setDirty(); } -void LLVolume::setDirty() -{ - mPathp->setDirty(); - mProfilep->setDirty(); - mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); -} - void LLVolume::regen() { generate(); @@ -3293,100 +3284,10 @@ S32 sculpt_sides(F32 detail) } } -static bool validate_sculpt_geometry(const LLVolume* volume) -{ - if (!volume || volume->getNumVolumeFaces() == 0) - { - return true; - } - - // Validate each face in the sculpt - for (S32 face_idx = 0; face_idx < volume->getNumVolumeFaces(); ++face_idx) - { - const LLVolumeFace& face = volume->getVolumeFace(face_idx); - - // Skip validation for degenerate faces - if (face.mNumVertices < 3 || face.mNumIndices < 3) - { - continue; - } - - // Calculate 'bounding' surface - LLVector4a extent_size; - extent_size.setSub(face.mExtents[1], face.mExtents[0]); - - F32 width = extent_size[0]; - F32 height = extent_size[1]; - F32 depth = extent_size[2]; - - F32 bounding_surface = width * height + width * depth + height * depth; - bounding_surface *= 2; - - if (!llfinite(bounding_surface) || bounding_surface <= FLT_EPSILON) - { - return false; - } - - // Calculate total surface area of all triangles - F32 total_triangle_area = 0.0f; - S32 num_triangles = face.mNumIndices / 3; - - for (S32 i = 0; i < num_triangles; ++i) - { - S32 idx_base = i * 3; - U16 idx0 = face.mIndices[idx_base]; - U16 idx1 = face.mIndices[idx_base + 1]; - U16 idx2 = face.mIndices[idx_base + 2]; - - // Validate indices - if (idx0 >= face.mNumVertices || idx1 >= face.mNumVertices || idx2 >= face.mNumVertices) - { - continue; - } - - const LLVector4a& v0 = face.mPositions[idx0]; - const LLVector4a& v1 = face.mPositions[idx1]; - const LLVector4a& v2 = face.mPositions[idx2]; - - // Calculate triangle area - LLVector4a edge1, edge2, cross; - edge1.setSub(v1, v0); - edge2.setSub(v2, v0); - cross.setCross3(edge1, edge2); - - F32 triangle_area = cross.getLength3().getF32() * 0.5f; - total_triangle_area += triangle_area; - } - - if (!llfinite(total_triangle_area)) - { - return false; - } - - // The idea here is that the total area of a sculpt is normally comparable - // to the area of a bounding box. But a random overlapping collection of - // triangles has a significantly larger area than a sculpt normally has. - F32 area_ratio = total_triangle_area / bounding_surface; - constexpr F32 MAX_AREA_TO_AREA_RATIO = 16.0f; - if (area_ratio > MAX_AREA_TO_AREA_RATIO) - { - LL_DEBUGS("LLVOLUME") << "Sculpt rejected: excessive triangle area." - << " Face index: " << face_idx - << " Total triangle area: " << total_triangle_area << " sq m" - << " Bounding surface area: " << bounding_surface << " sq m" - << " Ratio: " << area_ratio - << " Triangle count: " << num_triangles - << " Bounds: [" << width << " x " << height << " x " << depth << "]" << LL_ENDL; - return false; - } - } - - return true; -} // determine the number of vertices in both s and t direction for this sculpt -static void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) +void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) { // this code has the following properties: // 1) the aspect ratio of the mesh is as close as possible to the ratio of the map @@ -3496,62 +3397,10 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, mSculptLevel = sculpt_level; - LLSculptValidationState cached_state = LLSculptValidationState::Valid; - if (!data_is_empty && (sculpt_level >= 0) && sculpt_level < SCULPT_CACHE_SIZE) - { - // validation might be expensive, so we do it only once per lod. - cached_state = mSculptValidationCache[sculpt_level]; - } - - switch (cached_state) - { - case LLSculptValidationState::Valid: - { - // Delete any existing faces, then regenerate - mVolumeFaces.clear(); - createVolumeFaces(); - break; - } - case LLSculptValidationState::Invalid: - { - // Invalid geometry - // Regenerate mesh with an empty placeholder - mVolumeFaces.clear(); - sculptGenerateEmptyPlaceholder(); - createVolumeFaces(); - break; - } - case LLSculptValidationState::Unvalidated: - { - // First time at this LOD - mVolumeFaces.clear(); - createVolumeFaces(); - - bool valid_geometry = true; - - // Todo: can this be backed into createVolumeFaces? - // Or calculated without having to call createVolumeFaces first? - // Todo 2: should the same be done for meshes? - valid_geometry = validate_sculpt_geometry(this); - mSculptValidationCache[sculpt_level] = valid_geometry ? LLSculptValidationState::Valid : LLSculptValidationState::Invalid; - - if (!valid_geometry) - { - LL_WARNS("LLVOLUME") << "Sculpt failed geometry validation - either invalid image or malicious. " - << "Replacing with placeholder. Image: " << mParams.getSculptID() << LL_ENDL; - - // Clear the malicious faces - mVolumeFaces.clear(); - - // Regenerate mesh with empty placeholder - sculptGenerateEmptyPlaceholder(); + // Delete any existing faces so that they get regenerated + mVolumeFaces.clear(); - // Recreate faces with placeholder geometry - createVolumeFaces(); - } - break; - } - } //switch (cached_state) + createVolumeFaces(); } diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 86a7bf0e6a..3496928f7b 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -60,8 +60,6 @@ class LLVolumeOctree; #include "llalignedarray.h" #include "llrigginginfo.h" -#include <array> - //============================================================================ constexpr S32 MIN_DETAIL_FACES = 6; @@ -1042,7 +1040,7 @@ public: const LLVector4a& getMeshPt(const U32 i) const { return mMesh[i]; } - void setDirty(); + void setDirty() { mPathp->setDirty(); mProfilep->setDirty(); } void regen(); void genTangents(S32 face); @@ -1133,11 +1131,6 @@ public: bool mIsMeshAssetLoaded; bool mIsMeshAssetUnavaliable; - // Cache for sculpt geometry validation results - static constexpr S32 SCULPT_CACHE_SIZE = 6; // 0 to 5 inclusive - enum class LLSculptValidationState : S8 { Unvalidated, Valid, Invalid }; - std::array<LLSculptValidationState, SCULPT_CACHE_SIZE> mSculptValidationCache; - const LLVolumeParams mParams; LLPath *mPathp; LLProfile *mProfilep; diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index dea95de955..f63477c325 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -234,7 +234,11 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) // Destroys the object LLFolderViewItem::~LLFolderViewItem() { - mViewModelItem = NULL; + if (mViewModelItem) + { + mViewModelItem->setFolderViewItem(nullptr); + mViewModelItem = nullptr; + } gFocusMgr.removeKeyboardFocusWithoutCallback(this); } @@ -356,7 +360,12 @@ bool LLFolderViewItem::isPotentiallyVisible(S32 filter_generation) void LLFolderViewItem::refresh() { - LLFolderViewModelItem& vmi = *getViewModelItem(); + LLFolderViewModelItem* vmi_ptr = getViewModelItem(); + if (!vmi_ptr) + { + return; + } + LLFolderViewModelItem& vmi = *vmi_ptr; mLabel = utf8str_to_wstring(vmi.getDisplayName()); mLabelFontBuffer.reset(); @@ -387,6 +396,10 @@ void LLFolderViewItem::refresh() void LLFolderViewItem::refreshSuffix() { LLFolderViewModelItem const* vmi = getViewModelItem(); + if (!vmi) + { + return; + } // icons are slightly expensive to get, can be optimized // see LLInventoryIcon::getIcon() diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 3b3e7ea3ba..87ed21291f 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -478,7 +478,7 @@ bool LLWebRTCImpl::terminate() { for (auto& connection : connections) { - connection->closeOnSignalingThread(); + connection->closeOnSignalingThread(true); } // Destroy the connections here, on the signaling thread, while // it's still running. @@ -1146,13 +1146,13 @@ void LLWebRTCPeerConnectionImpl::terminate() mWebRTCImpl->PostSignalingTask( [self]() { - self->closeOnSignalingThread(); + self->closeOnSignalingThread(false); self->mPendingJobs--; }); } // Signaling thread only. -void LLWebRTCPeerConnectionImpl::closeOnSignalingThread() +void LLWebRTCPeerConnectionImpl::closeOnSignalingThread(bool webrtc_terminate) { // Stop issuing stats requests; one may already be in flight, and // Close() below will flush it. @@ -1203,13 +1203,16 @@ void LLWebRTCPeerConnectionImpl::closeOnSignalingThread() observer->OnPeerConnectionClosed(); } - // Nothing may call back into the viewer past this point. Connections - // closed while the viewer is still running unset themselves as observers - // when they're destroyed, but any that are left for llwebrtc::terminate() - // to close deliberately don't -- they're torn down as soon as it returns, - // so a late callback would be reaching into freed memory. - mSignalingObserverList.clear(); - mDataObserverList.clear(); + if (webrtc_terminate) + { + // Nothing may call back into the viewer past this point. Connections + // closed while the viewer is still running unset themselves as observers + // when they're destroyed, but any that are left for llwebrtc::terminate() + // to close deliberately don't -- they're torn down as soon as it returns, + // so a late callback would be reaching into freed memory. + mSignalingObserverList.clear(); + mDataObserverList.clear(); + } } void LLWebRTCPeerConnectionImpl::setSignalingObserver(LLWebRTCSignalingObserver *observer) { mSignalingObserverList.emplace_back(observer); } diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index ae3a6a4903..c980b28d6e 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -600,10 +600,13 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void init(LLWebRTCImpl * webrtc_impl); // Posts closeOnSignalingThread() and returns immediately. void terminate(); + // The actual close. Signaling thread only. Callable directly (via a // BlockingCall) when the caller needs the connection to be fully closed // before it continues -- see LLWebRTCImpl::terminate(). - void closeOnSignalingThread(); + // webrtc_terminate indicates we're shutting down the webrtc library, + // if not, we'll be reusing the peer connection for a reconnection + void closeOnSignalingThread(bool webrtc_terminate); virtual void AddRef() const override = 0; virtual webrtc::RefCountReleaseStatus Release() const override = 0; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index c7a80a7a4b..b91cc546a6 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -10,6 +10,9 @@ include(Linking) include(Boost) include(bugsplat) include(BuildPackagesInfo) +# Luau isn't an autobuild package, so stage its license separately. +file(MAKE_DIRECTORY "${AUTOBUILD_INSTALL_DIR}/LICENSES") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/luau.txt" "${AUTOBUILD_INSTALL_DIR}/LICENSES/luau.txt" COPYONLY) include(BuildVersion) include(CMakeCopyIfDifferent) include(CubemapToEquirectangularJS) @@ -192,7 +195,6 @@ set(viewer_SOURCE_FILES llfloaterabout.cpp llfloateravatarwelcomepack.cpp llfloaterbvhpreview.cpp - llfloateraddpaymentmethod.cpp llfloaterauction.cpp llfloaterautoreplacesettings.cpp llfloateravatarpicker.cpp @@ -881,7 +883,6 @@ set(viewer_HEADER_FILES llfloaterabout.h llfloateravatarwelcomepack.h llfloaterbvhpreview.h - llfloateraddpaymentmethod.h llfloaterauction.h llfloaterautoreplacesettings.h llfloateravatarpicker.h diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index aa8d051439..3434ab1516 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -1194,6 +1194,39 @@ <key>Value</key> <string>https://accounts.secondlife.com/packs/floater?lang=[LANGUAGE]&shortfall=[SHORTFALL]</string> </map> + <key>BuyCurrencyAddPaymentURL</key> + <map> + <key>Comment</key> + <string>URL the viewer probes at login; a 200 means the L$ packs floater handles residents without a payment method</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>https://accounts.secondlife.com/packs/floater/add-payment-enabled</string> + </map> + <key>PaymentMethodURL</key> + <map> + <key>Comment</key> + <string>URL for adding a payment method</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1</string> + </map> + <key>PaymentMethodStagingURL</key> + <map> + <key>Comment</key> + <string>URL for adding a payment method on the Damballah</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>https://secondlife-staging.com/my/lindex/buy.php?associate_for_viewer=1</string> + </map> <key>EnableButtonFlashing</key> <map> <key>Comment</key> @@ -13414,7 +13447,7 @@ <key>Type</key> <string>Boolean</string> <key>Value</key> - <integer>1</integer> + <integer>0</integer> </map> <key>PreferredBrowserBehavior</key> <map> @@ -14600,17 +14633,6 @@ <key>Value</key> <integer>180</integer> </map> - <key>OSHibernationMode</key> - <map> - <key>Comment</key> - <string>Whether to prevent OS from hibernating. 0 - can hibernate; 1 - can't hibernate, can turn screen off; 2 - can't hibernate, can't turn screen off </string> - <key>Persist</key> - <integer>1</integer> - <key>Type</key> - <string>S32</string> - <key>Value</key> - <integer>0</integer> - </map> <key>HeightUnits</key> <map> <key>Comment</key> diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 2f0ad15d18..178a14f470 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -579,6 +579,9 @@ void LLAgent::setFirstLogin(bool b) { setFeatureVersion(UI_FEATURE_VERSION, UI_FEATURE_FLAGS); } + + // Shared environment on teleport should only be enabled by default for new users; + gSavedSettings.setBOOL("SwitchToSharedEnvAfterTeleport", true); } } @@ -1552,8 +1555,6 @@ void LLAgent::setAFK() setControlFlags(AGENT_CONTROL_AWAY | AGENT_CONTROL_STOP); gAwayTimer.start(); } - - LLAppViewer::instance()->setPermitOSHibernation(true); } //----------------------------------------------------------------------------- @@ -1572,13 +1573,6 @@ void LLAgent::clearAFK() sendAnimationRequest(ANIM_AGENT_AWAY, ANIM_REQUEST_STOP); clearControlFlags(AGENT_CONTROL_AWAY); } - - if (isAgentAvatarValid()) - { - // Only set this if agent is inworld, login screen - // shouldn't prevent hibernation. - LLAppViewer::instance()->setPermitOSHibernation(false); - } } //----------------------------------------------------------------------------- diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 2a7e2b61f9..441f43163d 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -643,12 +643,6 @@ bool LLAppViewer::sendURLToOtherInstance(const std::string& url) return false; } -//virtual -void LLAppViewer::setOSHibernationMode(eHibernationMode mode) -{ - // See OS specific files -} - //---------------------------------------------------------------------------- // LLAppViewer definition @@ -5996,29 +5990,6 @@ void LLAppViewer::outOfMemorySoftQuit() } } -void LLAppViewer::setPermitOSHibernation(bool permit) -{ - if (permit) - { - if (mCurrentHibernationMode != LL_HIBERNATE_MODE_DEFAULT) - { - // Will call OS specific code to let OS hibernate when idle - setOSHibernationMode(LL_HIBERNATE_MODE_DEFAULT); - mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; - } - } - else - { - static LLCachedControl<S32> os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); - eHibernationMode mode = static_cast<eHibernationMode>(os_hibernation_mode()); - if (mode != LL_HIBERNATE_MODE_DEFAULT && mCurrentHibernationMode != mode) - { - setOSHibernationMode(mode); - mCurrentHibernationMode = mode; - } - } -} - void LLAppViewer::idleNameCache() { // Neither old nor new name cache can function before agent has a region @@ -6229,9 +6200,6 @@ void LLAppViewer::disconnectViewer() // Pass the connection state to LLUrlEntryParcel not to attempt // parcel info requests while disconnected. LLUrlEntryParcel::setDisconnected(gDisconnected); - - // Restore default OS hibernation mode - setPermitOSHibernation(true); } void LLAppViewer::forceErrorLLError() @@ -6525,15 +6493,6 @@ void LLAppViewer::handleLoginComplete() // we logged in successfully, so save settings on logout LL_INFOS() << "Login successful, per account settings will be saved on log out." << LL_ENDL; mSavePerAccountSettings=true; - - // Don't allow hibernation while we're running - setPermitOSHibernation(false); - // Track 'hibernation' mode changes - mOSHibernationModeChangeConnection = gSavedSettings.getControl("OSHibernationMode")->getSignal()->connect([](LLControlVariable* control, const LLSD& new_val, const LLSD& old_val) - { - // setPermitOSHibernation will sort itself out based on new mode. - LLAppViewer::instance()->setPermitOSHibernation(false); - }); } //virtual diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index 837b598ae5..54f94fe98c 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -270,8 +270,6 @@ public: // Note: mQuitRequested can be aborted by user. void outOfMemorySoftQuit(); - virtual void setPermitOSHibernation(bool permit); - #ifdef LL_DISCORD static void initDiscordSocial(); static void updateDiscordActivity(); @@ -288,14 +286,6 @@ protected: virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); - typedef enum - { - LL_HIBERNATE_MODE_DEFAULT = 0, // Use the platform's default behavior. - LL_HIBERNATE_MODE_PREVENT = 1, - LL_HIBERNATE_MODE_PREVENT_SCREEN = 2, - } eHibernationMode; - virtual void setOSHibernationMode(eHibernationMode mode); - virtual bool initParseCommandLine(LLCommandLineParser& clp) { return true; } // Allow platforms to specify the command line args. @@ -403,9 +393,6 @@ private: LLAppCoreHttp mAppCoreHttp; bool mIsFirstRun; - - eHibernationMode mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; - boost::signals2::scoped_connection mOSHibernationModeChangeConnection; }; // Globals with external linkage. From viewer.h diff --git a/indra/newview/llappviewerlinux.cpp b/indra/newview/llappviewerlinux.cpp index 2fd0eebd44..bcc9127d0e 100644 --- a/indra/newview/llappviewerlinux.cpp +++ b/indra/newview/llappviewerlinux.cpp @@ -79,10 +79,6 @@ namespace void (*gOldTerminateHandler)() = NULL; } -// Initialize static members -guint32 LLAppViewerLinux::sPowerInhibitCookie = 0; -bool LLAppViewerLinux::sPowerInhibitActive = false; - void check_vm_bloat() { #if LL_LINUX @@ -174,7 +170,6 @@ finally: } #endif // LL_LINUX } - static void exceptionTerminateHandler() { // reinstall default terminate() handler in case we re-terminate. @@ -286,11 +281,6 @@ LLAppViewerLinux::LLAppViewerLinux() LLAppViewerLinux::~LLAppViewerLinux() { - // Clean up any power management inhibition on exit - if (sPowerInhibitActive) - { - uninhibitPowerManagement(); - } } bool LLAppViewerLinux::init() @@ -466,178 +456,6 @@ bool LLAppViewerLinux::sendURLToOtherInstance(const std::string& url) } #endif // LL_GLIB - -#if LL_GLIB - -namespace -{ - // Session-bus sleep inhibitors, tried in order; both hand back a uint cookie. - struct PowerInhibitor - { - const char* service; - const char* path; - const char* iface; - const char* uninhibit; - bool gnome_args; - }; - - const PowerInhibitor POWER_INHIBITORS[] = - { - { "org.freedesktop.PowerManagement", "/org/freedesktop/PowerManagement/Inhibit", - "org.freedesktop.PowerManagement.Inhibit", "UnInhibit", false }, - { "org.gnome.SessionManager", "/org/gnome/SessionManager", - "org.gnome.SessionManager", "Uninhibit", true } - }; - - const PowerInhibitor* sActiveInhibitor = nullptr; - - // Returns the reply, or null on failure. Takes ownership of args. - GVariant* call_power_manager(const PowerInhibitor& inhibitor, const char* method, GVariant* args, - const GVariantType* reply_type) - { - g_variant_ref_sink(args); - - GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); - if (!bus) - { - LL_WARNS("OS") << "Getting dbus failed." << LL_ENDL; - g_variant_unref(args); - return nullptr; - } - - GError* error = nullptr; - GVariant* result = g_dbus_connection_call_sync(bus, inhibitor.service, inhibitor.path, - inhibitor.iface, method, args, reply_type, - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - if (!result) - { - LL_DEBUGS("OS") << inhibitor.service << "." << method << " failed: " - << (error ? error->message : "unknown error") << LL_ENDL; - } - if (error) - { - g_error_free(error); - } - - g_variant_unref(args); - g_object_unref(bus); - return result; - } -} - -#endif // LL_GLIB - -void LLAppViewerLinux::setOSHibernationMode(eHibernationMode mode) -{ - if (mode == LL_HIBERNATE_MODE_DEFAULT) - { - // Allow OS to sleep/hibernate - remove any inhibition - if (sPowerInhibitActive) - { - uninhibitPowerManagement(); - LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; - } - } - else if (mode == LL_HIBERNATE_MODE_PREVENT) - { - // Prevent system sleep, but allow display to turn off - // Release any existing inhibition first to allow mode switching - if (sPowerInhibitActive) - { - uninhibitPowerManagement(); - } - - if (inhibitPowerManagement(false)) - { - LL_INFOS("OS") << "Prevented OS hibernation/sleep, display sleep allowed" << LL_ENDL; - } - else - { - LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep" << LL_ENDL; - } - } - else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) - { - // Prevent both system and display sleep - // Release any existing inhibition first to allow mode switching - if (sPowerInhibitActive) - { - uninhibitPowerManagement(); - } - - if (inhibitPowerManagement(true)) - { - LL_INFOS("OS") << "Prevented OS hibernation/sleep and display sleep" << LL_ENDL; - } - else - { - LL_WARNS("OS") << "Failed to prevent OS hibernation/sleep and display sleep" << LL_ENDL; - } - } -} - -// TODO: This is AI Generated!!!, needs review and testing. -bool LLAppViewerLinux::inhibitPowerManagement(bool inhibit_display) -{ -#if LL_GLIB - const char* reason = inhibit_display ? - "Viewer active - preventing system and display sleep" : - "Viewer active - preventing system sleep"; - - for (const PowerInhibitor& inhibitor : POWER_INHIBITORS) - { - // Inhibit(app: s, reason: s) -> u, or GNOME's Inhibit(app: s, xid: u, reason: s, flags: u) -> u - // GNOME flags: 4 = suspend, 8 = idle (display), 12 = both - GVariant* args = inhibitor.gnome_args ? - g_variant_new("(susu)", "SecondLifeViewer", (guint32)0, reason, - (guint32)(inhibit_display ? 12 : 4)) : - g_variant_new("(ss)", "Second Life Viewer", reason); - - GVariant* result = call_power_manager(inhibitor, "Inhibit", args, G_VARIANT_TYPE("(u)")); - if (!result) - { - continue; - } - - guint32 cookie = 0; - g_variant_get(result, "(u)", &cookie); - g_variant_unref(result); - - sPowerInhibitCookie = cookie; - sPowerInhibitActive = true; - sActiveInhibitor = &inhibitor; - LL_INFOS("OS") << "Successfully inhibited power management using " - << inhibitor.service << LL_ENDL; - return true; - } -#endif // LL_GLIB - - return false; -} - -void LLAppViewerLinux::uninhibitPowerManagement() -{ -#if LL_GLIB - if (sPowerInhibitActive && sActiveInhibitor) - { - GVariant* result = call_power_manager(*sActiveInhibitor, sActiveInhibitor->uninhibit, - g_variant_new("(u)", (guint32)sPowerInhibitCookie), - nullptr); - if (result) - { - g_variant_unref(result); - LL_INFOS("OS") << "Successfully uninhibited power management using " - << sActiveInhibitor->service << LL_ENDL; - } - - sActiveInhibitor = nullptr; - } -#endif // LL_GLIB - - sPowerInhibitCookie = 0; - sPowerInhibitActive = false; -} - void LLAppViewerLinux::initCrashReporting(bool reportFreeze) { } diff --git a/indra/newview/llappviewerlinux.h b/indra/newview/llappviewerlinux.h index faac3c0a05..6f4e087726 100644 --- a/indra/newview/llappviewerlinux.h +++ b/indra/newview/llappviewerlinux.h @@ -60,16 +60,6 @@ protected: bool initSLURLHandler() override; bool sendURLToOtherInstance(const std::string& url) override; - void setOSHibernationMode(eHibernationMode mode) override; - -private: - // Power management state tracking - static guint32 sPowerInhibitCookie; - static bool sPowerInhibitActive; - - // Helper methods for power management - bool inhibitPowerManagement(bool inhibit_display); - void uninhibitPowerManagement(); }; #endif // LL_LLAPPVIEWERLINUX_H diff --git a/indra/newview/llappviewermacosx-objc.h b/indra/newview/llappviewermacosx-objc.h index 13b8056193..bfbb48dadb 100644 --- a/indra/newview/llappviewermacosx-objc.h +++ b/indra/newview/llappviewermacosx-objc.h @@ -32,6 +32,5 @@ void force_ns_sxeption(); void register_url_schemes(); -void set_os_hibernation_mode(int mode); #endif // LL_LLAPPVIEWERMACOSX_OBJC_H diff --git a/indra/newview/llappviewermacosx-objc.mm b/indra/newview/llappviewermacosx-objc.mm index ab4ae428b9..75d6b56e3e 100644 --- a/indra/newview/llappviewermacosx-objc.mm +++ b/indra/newview/llappviewermacosx-objc.mm @@ -29,7 +29,6 @@ #endif #import <Cocoa/Cocoa.h> -#import <IOKit/pwr_mgt/IOPMLib.h> #include <iostream> #include "llappviewermacosx-objc.h" @@ -64,68 +63,3 @@ void register_url_schemes() } } } - -// Add these as static variables at file scope -static IOPMAssertionID gPowerAssertionID = kIOPMNullAssertionID; - -void set_os_hibernation_mode(int mode) -{ - // Release existing assertion - if (gPowerAssertionID != kIOPMNullAssertionID) - { - IOReturn result = IOPMAssertionRelease(gPowerAssertionID); - if (result == kIOReturnSuccess) - { - gPowerAssertionID = kIOPMNullAssertionID; - NSLog(@"Permitted OS hibernation/sleep"); - } - else - { - NSLog(@"Failed to release power assertion: %d", result); - } - } - - if (mode == 1) - { - // Prevent OS from sleeping/hibernating - CFStringRef assertionName = CFSTR("Second Life Viewer"); - // kIOPMAssertionTypeNoIdleSleep prevents idle sleep - IOReturn result = IOPMAssertionCreateWithName( - kIOPMAssertionTypeNoIdleSleep, - kIOPMAssertionLevelOn, - assertionName, - &gPowerAssertionID - ); - - if (result == kIOReturnSuccess) - { - NSLog(@"Prevented OS hibernation/sleep, allow display sleep"); - } - else - { - NSLog(@"Failed to create power assertion: %d", result); - } - } - else if (mode == 2) - { - // Prevent OS from sleeping/hibernating, prevent screen from going off - CFStringRef assertionName = CFSTR("Second Life Viewer"); - // kIOPMAssertionTypeNoIdleSleep prevents idle sleep - // kIOPMAssertionTypeNoDisplaySleep prevents display sleep - IOReturn result = IOPMAssertionCreateWithName( - kIOPMAssertionTypeNoDisplaySleep, - kIOPMAssertionLevelOn, - assertionName, - &gPowerAssertionID - ); - - if (result == kIOReturnSuccess) - { - NSLog(@"Prevented OS hibernation/sleep or screen from turning off"); - } - else - { - NSLog(@"Failed to create power assertion: %d", result); - } - } -} diff --git a/indra/newview/llappviewermacosx.cpp b/indra/newview/llappviewermacosx.cpp index c188bb459f..f914a76fc6 100644 --- a/indra/newview/llappviewermacosx.cpp +++ b/indra/newview/llappviewermacosx.cpp @@ -126,7 +126,6 @@ bool pumpMainLoop() void cleanupViewer() { - set_os_hibernation_mode(0); // restore default OS hibernation behavior if(!LLApp::isError()) { if (gViewerAppPtr) @@ -429,12 +428,6 @@ bool LLAppViewerMacOSX::initSLURLHandler() return true; } -void LLAppViewerMacOSX::setOSHibernationMode(eHibernationMode mode) -{ - // pass to objective-c++ - set_os_hibernation_mode((int)mode); -} - std::string LLAppViewerMacOSX::generateSerialNumber() { char serial_md5[MD5HEX_STR_SIZE]; // Flawfinder: ignore diff --git a/indra/newview/llappviewermacosx.h b/indra/newview/llappviewermacosx.h index 1153bd4191..35fc99dbd9 100644 --- a/indra/newview/llappviewermacosx.h +++ b/indra/newview/llappviewermacosx.h @@ -47,7 +47,6 @@ public: protected: virtual bool restoreErrorTrap(); virtual bool initSLURLHandler(); - virtual void setOSHibernationMode(eHibernationMode mode); std::string generateSerialNumber(); virtual bool initParseCommandLine(LLCommandLineParser& clp); diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 2080d0b7bf..f8a728a163 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -1054,56 +1054,6 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) return false; } -void LLAppViewerWin32::setOSHibernationMode(eHibernationMode mode) -{ - // ES_CONTINUOUS tells Windows to reset the idle timer - // and restore normal operation - // ES_SYSTEM_REQUIRED prevents system sleep/hibernation - // ES_DISPLAY_REQUIRED prevents display sleep - - if (mode == LL_HIBERNATE_MODE_DEFAULT) - { - // Allow OS to hibernate - clear the previous execution state flags - // ES_CONTINUOUS without other flags allows the system to idle normally - SetThreadExecutionState(ES_CONTINUOUS); - LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; - } - else if (mode == LL_HIBERNATE_MODE_PREVENT) - { - // Prevent OS from hibernating while viewer is running - // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake - EXECUTION_STATE result = SetThreadExecutionState( - ES_CONTINUOUS | ES_SYSTEM_REQUIRED - ); - if (result == NULL) - { - LL_WARNS("OS") << "Failed to prevent OS hibernation, error: " << GetLastError() << LL_ENDL; - } - else - { - LL_INFOS("OS") << "Prevented OS hibernation, but allowed display sleep" << LL_ENDL; - } - } - else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) - { - // Prevent OS from hibernating or turning screen off while viewer is running - // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake - // ES_DISPLAY_REQUIRED keeps the display on - EXECUTION_STATE result = SetThreadExecutionState( - ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED - ); - - if (result == NULL) - { - LL_WARNS("OS") << "Failed to prevent OS hibernation and display sleep, error: " << GetLastError() << LL_ENDL; - } - else - { - LL_INFOS("OS") << "Prevented OS hibernation/sleep" << LL_ENDL; - } - } -} - bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir) { // Velopack installs viewer like this: diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 7c3c7a475a..971907694a 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -62,7 +62,6 @@ protected: bool restoreErrorTrap() override; bool sendURLToOtherInstance(const std::string& url) override; - void setOSHibernationMode(eHibernationMode mode) override; std::string generateSerialNumber(); diff --git a/indra/newview/llbuycurrencyhtml.cpp b/indra/newview/llbuycurrencyhtml.cpp index 9ad068cf7f..f5bc3b5357 100644 --- a/indra/newview/llbuycurrencyhtml.cpp +++ b/indra/newview/llbuycurrencyhtml.cpp @@ -89,6 +89,8 @@ public: LLBuyCurrencyHTMLHandler gBuyCurrencyHTMLHandler; bool LLBuyCurrencyHTML::sWebFloaterEnabled = false; +bool LLBuyCurrencyHTML::sAddPaymentEnabled = false; +LLFetchAvatarPaymentInfo* LLBuyCurrencyHTML::sPaymentInfoRequest = NULL; //////////////////////////////////////////////////////////////////////////////// // static @@ -110,11 +112,37 @@ static void checkFeatureFlag_coro(std::string check_url) } // static +static void checkAddPaymentFlag_coro(std::string check_url) +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t + httpAdapter = std::make_shared<LLCoreHttpUtil::HttpCoroutineAdapter>("CheckBuyCurrencyAddPaymentURL", httpPolicy); + LLCore::HttpRequest::ptr_t httpRequest = std::make_shared<LLCore::HttpRequest>(); + + // a redirect to some other page that answers 200 must not read as enabled + LLCore::HttpOptions::ptr_t httpOptions = std::make_shared<LLCore::HttpOptions>(); + httpOptions->setRetries(0); + httpOptions->setFollowRedirects(false); + + LLSD result = httpAdapter->getRawAndSuspend(httpRequest, check_url, httpOptions); + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + + LLBuyCurrencyHTML::sAddPaymentEnabled = status.isHttpStatus() && status.getType() == 200; + LL_INFOS("LLBuyCurrency") << "Add payment probe returned " << status.toString() + << ", in-floater add payment " << (LLBuyCurrencyHTML::sAddPaymentEnabled ? "enabled" : "disabled") << LL_ENDL; +} + +// static void LLBuyCurrencyHTML::checkFeatureFlag() { std::string check_url = LLFloaterBuyCurrencyHTML::buildURL(); LLCoros::instance().launch("checkFeatureFlag_coro", [check_url]() { checkFeatureFlag_coro(check_url); }); + + std::string add_payment_url = gSavedSettings.getString("BuyCurrencyAddPaymentURL"); + LLCoros::instance().launch("checkAddPaymentFlag_coro", + [add_payment_url]() { checkAddPaymentFlag_coro(add_payment_url); }); } //////////////////////////////////////////////////////////////////////////////// @@ -124,15 +152,7 @@ void LLBuyCurrencyHTML::checkFeatureFlag() // the case where the amount is not requested. void LLBuyCurrencyHTML::openCurrencyFloater() { - if (gSavedSettings.getBOOL("BuyCurrencyHTML") && sWebFloaterEnabled) - { - LLBuyCurrencyHTML::showDialog(); - } - else - { - // legacy version - LLFloaterBuyCurrency::buyCurrency(); - } + routeCurrencyRequest(false, LLStringUtil::null, 0); } //////////////////////////////////////////////////////////////////////////////// @@ -142,19 +162,68 @@ void LLBuyCurrencyHTML::openCurrencyFloater() // the case where the amount and a string to display are requested. void LLBuyCurrencyHTML::openCurrencyFloater( const std::string& message, S32 sum ) { - if (gSavedSettings.getBOOL("BuyCurrencyHTML") && sWebFloaterEnabled) + routeCurrencyRequest(true, message, sum); +} + +//////////////////////////////////////////////////////////////////////////////// +// static +// Without the in-floater add payment flow, the web floater is opened only for +// residents with payment info on file; the rest go to the LindeX payment page. +void LLBuyCurrencyHTML::routeCurrencyRequest( bool has_target, const std::string& message, S32 sum ) +{ + if (!gSavedSettings.getBOOL("BuyCurrencyHTML") || !sWebFloaterEnabled) { - LLBuyCurrencyHTML::showDialog(sum - gStatusBar->getBalance()); - LLFloaterBuyCurrencyHTML* floater = dynamic_cast<LLFloaterBuyCurrencyHTML*>(LLFloaterReg::getInstance("buy_currency_html")); - if (floater) + // legacy version, which runs its own payment info check + if (has_target) { - floater->setFallbackContext(message, sum); + LLFloaterBuyCurrency::buyCurrency(message, sum); } + else + { + LLFloaterBuyCurrency::buyCurrency(); + } + } + else if (sAddPaymentEnabled) + { + openWebFloater(has_target, message, sum); } else { - // legacy version - LLFloaterBuyCurrency::buyCurrency( message, sum ); + delete sPaymentInfoRequest; + sPaymentInfoRequest = new LLFetchAvatarPaymentInfo( + [has_target, message, sum](bool has_piof) + { + delete sPaymentInfoRequest; + sPaymentInfoRequest = NULL; + + if (has_piof) + { + openWebFloater(has_target, message, sum); + } + else + { + LL_INFOS("LLBuyCurrency") << "No payment info on file, opening payment method page" << LL_ENDL; + LLFloaterBuyCurrency::openPaymentMethodPage(); + } + }); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// static +void LLBuyCurrencyHTML::openWebFloater( bool has_target, const std::string& message, S32 sum ) +{ + if (!has_target) + { + LLBuyCurrencyHTML::showDialog(); + return; + } + + LLBuyCurrencyHTML::showDialog(sum - gStatusBar->getBalance()); + LLFloaterBuyCurrencyHTML* floater = dynamic_cast<LLFloaterBuyCurrencyHTML*>(LLFloaterReg::getInstance("buy_currency_html")); + if (floater) + { + floater->setFallbackContext(message, sum); } } diff --git a/indra/newview/llbuycurrencyhtml.h b/indra/newview/llbuycurrencyhtml.h index 2f78199dcb..c8a98c42a9 100644 --- a/indra/newview/llbuycurrencyhtml.h +++ b/indra/newview/llbuycurrencyhtml.h @@ -29,6 +29,8 @@ #include "llsingleton.h" +class LLFetchAvatarPaymentInfo; + class LLBuyCurrencyHTML { public: @@ -45,10 +47,25 @@ class LLBuyCurrencyHTML // close (and destroy) the currency floater static void closeDialog(); - // probe BuyCurrencyPacksURL once at login; clears sWebFloaterEnabled if 501 + // probe BuyCurrencyPacksURL once at login; clears sWebFloaterEnabled if 501. + // also launches the BuyCurrencyAddPaymentURL probe that sets sAddPaymentEnabled static void checkFeatureFlag(); static bool sWebFloaterEnabled; + + // true only when BuyCurrencyAddPaymentURL answered 200, meaning the web floater + // can add a payment method; otherwise residents without one are sent to the LindeX + static bool sAddPaymentEnabled; + + private: + // route to the legacy floater, the web floater, or the LindeX payment page + static void routeCurrencyRequest( bool has_target, const std::string& message, S32 sum ); + + // open the web floater, with the shortfall and fallback context when there is a target + static void openWebFloater( bool has_target, const std::string& message, S32 sum ); + + // pending payment info check made before opening the web floater + static LLFetchAvatarPaymentInfo* sPaymentInfoRequest; }; #endif // LL_LLBUYCURRENCYHTML_H diff --git a/indra/newview/llfloateraddpaymentmethod.cpp b/indra/newview/llfloateraddpaymentmethod.cpp deleted file mode 100644 index 64bd7b6aa1..0000000000 --- a/indra/newview/llfloateraddpaymentmethod.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @file llfloateraddpaymentmethod.cpp - * @brief LLFloaterAddPaymentMethod class implementation - * - * $LicenseInfo:firstyear=2020&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2020, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "llviewerprecompiledheaders.h" - -#include "llfloateraddpaymentmethod.h" -#include "llnotificationsutil.h" -#include "lluictrlfactory.h" -#include "llviewernetwork.h" -#include "llweb.h" - - -LLFloaterAddPaymentMethod::LLFloaterAddPaymentMethod(const LLSD& key) - : LLFloater(key) -{ -} - -LLFloaterAddPaymentMethod::~LLFloaterAddPaymentMethod() -{ -} - -bool LLFloaterAddPaymentMethod::postBuild() -{ - setCanDrag(false); - getChild<LLButton>("continue_btn")->setCommitCallback(boost::bind(&LLFloaterAddPaymentMethod::onContinueBtn, this)); - getChild<LLButton>("close_btn")->setCommitCallback(boost::bind(&LLFloaterAddPaymentMethod::onCloseBtn, this)); - return true; -} - -void LLFloaterAddPaymentMethod::onOpen(const LLSD& key) -{ - centerOnScreen(); -} - -void LLFloaterAddPaymentMethod::onContinueBtn() -{ - closeFloater(); - LLNotificationsUtil::add("AddPaymentMethod", LLSD(), LLSD(), - [this](const LLSD¬if, const LLSD&resp) - { - S32 opt = LLNotificationsUtil::getSelectedOption(notif, resp); - if (opt == 0) - { - const std::string& grid_id = LLGridManager::getInstance()->getGridId(); - const std::string& grid_id_lower = utf8str_tolower(grid_id); - LLWeb::loadURL(getString(grid_id_lower == "damballah" ? "continue_staging_url" : "continue_url")); - } - }); -} - -void LLFloaterAddPaymentMethod::onCloseBtn() -{ - closeFloater(); -} - -void LLFloaterAddPaymentMethod::centerOnScreen() -{ - LLVector2 window_size = LLUI::getInstance()->getWindowSize(); - centerWithin(LLRect(0, 0, ll_round(window_size.mV[VX]), ll_round(window_size.mV[VY]))); -} - diff --git a/indra/newview/llfloateraddpaymentmethod.h b/indra/newview/llfloateraddpaymentmethod.h deleted file mode 100644 index 252caf8f5b..0000000000 --- a/indra/newview/llfloateraddpaymentmethod.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @file llfloateraddpaymentmethod.h - * @brief LLFloaterAddPaymentMethod class definition - * - * $LicenseInfo:firstyear=2020&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2020, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#ifndef LL_FLOATER_ADDPAYMENTMETHOD_H -#define LL_FLOATER_ADDPAYMENTMETHOD_H - -#include "llfloater.h" - -class LLFloaterAddPaymentMethod: - public LLFloater -{ - friend class LLFloaterReg; -public: - bool postBuild() override; - void onOpen(const LLSD& key) override; - -private: - LLFloaterAddPaymentMethod(const LLSD& key); - - void centerOnScreen(); - - void onCloseBtn(); - void onContinueBtn(); - - /*virtual*/ ~LLFloaterAddPaymentMethod(); - -}; - -#endif diff --git a/indra/newview/llfloaterbuycurrency.cpp b/indra/newview/llfloaterbuycurrency.cpp index 506cff31f4..7533145625 100644 --- a/indra/newview/llfloaterbuycurrency.cpp +++ b/indra/newview/llfloaterbuycurrency.cpp @@ -40,6 +40,8 @@ #include "llviewchildren.h" #include "llviewerwindow.h" #include "lluictrlfactory.h" +#include "llviewercontrol.h" +#include "llviewernetwork.h" #include "llweb.h" #include "llwindow.h" #include "llappviewer.h" @@ -336,16 +338,39 @@ void LLFloaterBuyCurrency::handleBuyCurrency(bool has_piof, bool has_target, con } else { - LLFloaterReg::showInstance("add_payment_method"); + openPaymentMethodPage(); } } +// static +void LLFloaterBuyCurrency::openPaymentMethodPage() +{ + const std::string& grid_id = LLGridManager::getInstance()->getGridId(); + const std::string& grid_id_lower = utf8str_tolower(grid_id); + const std::string url = gSavedSettings.getString(grid_id_lower == "damballah" ? "PaymentMethodStagingURL" : "PaymentMethodURL"); + LLWeb::loadURL(url); +} + LLFetchAvatarPaymentInfo::LLFetchAvatarPaymentInfo(bool has_target, const std::string& name, S32 price) : mAvatarID(gAgent.getID()), mHasTarget(has_target), mPrice(price), mName(name) { + sendRequest(); +} + +LLFetchAvatarPaymentInfo::LLFetchAvatarPaymentInfo(const callback_t& cb) +: mAvatarID(gAgent.getID()), + mHasTarget(false), + mPrice(0), + mCallback(cb) +{ + sendRequest(); +} + +void LLFetchAvatarPaymentInfo::sendRequest() +{ LLAvatarPropertiesProcessor* processor = LLAvatarPropertiesProcessor::getInstance(); // register ourselves as an observer processor->addObserver(mAvatarID, this); @@ -364,6 +389,16 @@ void LLFetchAvatarPaymentInfo::processProperties(void* data, EAvatarProcessorTyp if (data && type == APT_PROPERTIES) { LLAvatarData* avatar_data = static_cast<LLAvatarData*>(data); - LLFloaterBuyCurrency::handleBuyCurrency(LLAvatarPropertiesProcessor::hasPaymentInfoOnFile(avatar_data), mHasTarget, mName, mPrice); + bool has_piof = LLAvatarPropertiesProcessor::hasPaymentInfoOnFile(avatar_data); + if (mCallback) + { + // call a local copy, since the callback may delete this observer + callback_t cb = mCallback; + cb(has_piof); + } + else + { + LLFloaterBuyCurrency::handleBuyCurrency(has_piof, mHasTarget, mName, mPrice); + } } } diff --git a/indra/newview/llfloaterbuycurrency.h b/indra/newview/llfloaterbuycurrency.h index 1b04c9c6d2..c6a80de926 100644 --- a/indra/newview/llfloaterbuycurrency.h +++ b/indra/newview/llfloaterbuycurrency.h @@ -30,21 +30,34 @@ #include "llavatarpropertiesprocessor.h" #include "stdtypes.h" #include "llagent.h" + +#include <functional> + class LLFloater; class LLFetchAvatarPaymentInfo : public LLAvatarPropertiesObserver { public: + typedef std::function<void(bool has_piof)> callback_t; + + // hands the result to LLFloaterBuyCurrency::handleBuyCurrency LLFetchAvatarPaymentInfo(bool has_target, const std::string& name = std::string(), S32 price = 0); + + // hands the result to cb, which may delete this observer + LLFetchAvatarPaymentInfo(const callback_t& cb); + ~LLFetchAvatarPaymentInfo(); void processProperties(void* data, EAvatarProcessorType type); private: + void sendRequest(); + LLUUID mAvatarID; bool mHasTarget; std::string mName; S32 mPrice; + callback_t mCallback; }; @@ -62,6 +75,9 @@ public: a space and the price will be appended */ + // open the LindeX page where a resident adds a payment method + static void openPaymentMethodPage(); + static LLFloater* buildFloater(const LLSD& key); static LLFetchAvatarPaymentInfo* sPropertiesRequest; diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp index 301a0a0cc9..92cf302a8d 100644 --- a/indra/newview/llinventorypanel.cpp +++ b/indra/newview/llinventorypanel.cpp @@ -602,8 +602,8 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve if (model_item && view_item && viewmodel_item) { const LLUUID idp = viewmodel_item->getUUID(); - view_item->destroyView(); removeItemID(idp); + view_item->destroyView(); } LLInventoryObject const* objectp = mInventory->getObject(item_id); @@ -709,6 +709,7 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve LLFolderViewModelItem* old_parent_vmi = old_parent->getViewModelItem(); LLFolderViewModelItemInventory* viewmodel_folder = static_cast<LLFolderViewModelItemInventory*>(old_parent_vmi); LLFolderViewFolder* new_parent = getFolderByID(model_item->getParentUUID()); + const bool was_favorite = view_item->isFavorite(); if (old_parent != new_parent // Item has been moved. && (new_parent != NULL || !isInRootContent(item_id, view_item)) // item is not or shouldn't be in root content @@ -748,7 +749,7 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve old_parent_vmi->dirtyDescendantsFilter(); } - if (view_item->isFavorite()) + if (was_favorite) { if (old_parent) { diff --git a/indra/newview/llpanelcontents.cpp b/indra/newview/llpanelcontents.cpp index 43dd1bfe40..d9301d60f8 100644 --- a/indra/newview/llpanelcontents.cpp +++ b/indra/newview/llpanelcontents.cpp @@ -111,6 +111,7 @@ LLPanelContents::LLPanelContents() LLPanelContents::~LLPanelContents() { + mSimulatorFeaturesConnection.disconnect(); // Children all cleaned up by default view destructor. } @@ -119,6 +120,7 @@ void LLPanelContents::getState(LLViewerObject *objectp ) { if( !objectp ) { + mSimulatorFeaturesConnection.disconnect(); mLastScriptObjectID.setNull(); getChildView("button new script")->setEnabled(false); getChildView("button new notecard")->setEnabled(false); @@ -154,11 +156,18 @@ void LLPanelContents::getState(LLViewerObject *objectp ) region->getSimulatorFeatures(simulatorFeatures); lua_region = simulatorFeatures["LuaScriptsEnabled"].asBoolean(); } + mSimulatorFeaturesConnection.disconnect(); + if (region && !region->simulatorFeaturesReceived()) + { + mSimulatorFeaturesConnection = region->setSimulatorFeaturesReceivedCallback( + boost::bind(&LLPanelContents::onSimulatorFeaturesReceived, this, _1)); + } LLComboBox* new_script = getChild<LLComboBox>("button new script"); new_script->setEnabledByValue("lua", lua_region); if (mLastScriptObjectID != objectp->getID() || mLastLuaRegion != lua_region) { - new_script->setValue(lua_region ? "lua" : "lsl"); + mNewScriptIsLua = lua_region; + new_script->setValue(mNewScriptIsLua ? "lua" : "lsl"); } mLastScriptObjectID = objectp->getID(); mLastLuaRegion = lua_region; @@ -187,6 +196,11 @@ void LLPanelContents::getState(LLViewerObject *objectp ) } } +void LLPanelContents::onSimulatorFeaturesReceived(const LLUUID&) +{ + refresh(); +} + void LLPanelContents::onFilterEdit() { const std::string& filter_substring = mFilterEditor->getText(); @@ -277,31 +291,18 @@ void LLPanelContents::onNewScriptFlyoutCommit(LLUICtrl* ctrl) LLViewerObject* object = LLSelectMgr::getInstance()->getSelection()->getFirstRootObject(children_ok); if (!object) return; - U8 script_language; const std::string value = ctrl->getValue().asString(); if (value == "lsl") { - script_language = SST_LSL; + mNewScriptIsLua = false; } else if (value == "lua") { - script_language = SST_LUA; - } - else - { - script_language = SST_LSL; - LLViewerRegion* region = object->getRegion(); - if (region && region->simulatorFeaturesReceived()) - { - LLSD simulatorFeatures; - region->getSimulatorFeatures(simulatorFeatures); - if (simulatorFeatures["LuaScriptsEnabled"].asBoolean()) - { - script_language = SST_LUA; - } - } + mNewScriptIsLua = true; } + ctrl->setValue(mNewScriptIsLua ? "lua" : "lsl"); + const U8 script_language = mNewScriptIsLua ? SST_LUA : SST_LSL; std::string vm = (script_language == SST_LUA) ? "luau" : "mono"; LLSD params; diff --git a/indra/newview/llpanelcontents.h b/indra/newview/llpanelcontents.h index d576b72353..e2909d21fc 100644 --- a/indra/newview/llpanelcontents.h +++ b/indra/newview/llpanelcontents.h @@ -35,6 +35,7 @@ #include "llvoinventorylistener.h" #include "llscripteditorws.h" #include "v3math.h" +#include <boost/signals2/connection.hpp> class LLButton; class LLPanelObjectInventory; @@ -79,10 +80,13 @@ public: protected: void getState(LLViewerObject *object); void onFilterEdit(); + void onSimulatorFeaturesReceived(const LLUUID& region_id); bool mDirtyFilter { false }; LLUUID mLastScriptObjectID; bool mLastLuaRegion { false }; + bool mNewScriptIsLua { false }; + boost::signals2::connection mSimulatorFeaturesConnection; public: class LLFilterEditor* mFilterEditor; diff --git a/indra/newview/llpanelprofilepicks.cpp b/indra/newview/llpanelprofilepicks.cpp index 6870d719af..ca1dd1b786 100644 --- a/indra/newview/llpanelprofilepicks.cpp +++ b/indra/newview/llpanelprofilepicks.cpp @@ -41,6 +41,7 @@ #include "llpanelavatar.h" #include "llpanelprofile.h" #include "llparcel.h" +#include "llregionhandle.h" #include "llstartup.h" #include "lltabcontainer.h" #include "lltextbox.h" @@ -915,7 +916,19 @@ void LLPanelProfilePick::processParcelInfo(const LLParcelData& parcel_data) { // Region might have moved since the pick was saved; refresh the stored global position // using the parcel info so map/teleport use the current location. - setPosGlobal(LLVector3d(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z)); + LLVector3d parcel_pos(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z); + U64 old_region_handle = to_region_handle(getPosGlobal()); + U64 new_region_handle = to_region_handle(parcel_pos); + if (old_region_handle != new_region_handle) + { + F32 old_origin_x, old_origin_y; + from_region_handle(old_region_handle, &old_origin_x, &old_origin_y); + LLVector3d local_offset = getPosGlobal() - LLVector3d(old_origin_x, old_origin_y, 0.0); + + F32 new_origin_x, new_origin_y; + from_region_handle(new_region_handle, &new_origin_x, &new_origin_y); + setPosGlobal(LLVector3d(new_origin_x, new_origin_y, 0.0) + local_offset); + } setPickLocation(createLocationText(LLStringUtil::null, parcel_data.name, parcel_data.sim_name, getPosGlobal())); diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index cee1180fa7..ffb748baae 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -2046,6 +2046,7 @@ void LLPreviewLSL::saveIfNeeded(bool sync /*= true*/) mPendingUploads = 0; mScriptEd->mErrorList->deleteAllItems(); + mScriptEd->enableSave(false); mScriptEd->mEditor->makePristine(); if (sync) @@ -2082,6 +2083,7 @@ void LLPreviewLSL::saveIfNeeded(bool sync /*= true*/) void LLPreviewLSL::onCompileTargetChanged() { + mScriptEd->enableSave(true); bool is_lua = (mScriptEd->mCompileTarget->getValue().asString() == "luau"); mScriptEd->mEditor->setLuauLanguage(is_lua); mScriptEd->processKeywords(is_lua); diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp index 85a9542dce..5cbb7ca76d 100644 --- a/indra/newview/llpublishedobjectmgr.cpp +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -32,6 +32,7 @@ #include "llchat.h" #include "llinventorydefines.h" #include "llregex.h" +#include "llsdutil.h" #include "llselectmgr.h" #include "llviewerinventory.h" #include "llviewerobject.h" @@ -66,7 +67,7 @@ namespace return std::string(s); } - std::string get_prim_name(LLViewerObject* obj) + std::string get_prim_name(LLViewerObject* obj, S32 link_number = 1) { std::string name = nv_string(obj, "Name"); if (!name.empty()) @@ -85,25 +86,17 @@ namespace return node->mName; } - // Never emit an empty prim/object name to downstream tooling. - return obj->getID().asString(); + // Real prim names need a full ObjectProperties reply, which requires selection. + return (link_number > 1) ? llformat("Link #%d", link_number) : std::string("Object"); } void add_object_permissions(LLSD& object_data, LLViewerObject* object) { - LLPermissions* permissions = - LLSelectMgr::getInstance()->findObjectPermissions(object); - if (!permissions) + LLSD permissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(object); + if (permissions.isDefined()) { - return; + object_data["permissions"] = permissions; } - - LLSD permission_entry; - permission_entry["owner"] = - static_cast<S32>(permissions->getMaskOwner()); - permission_entry["next_owner"] = - static_cast<S32>(permissions->getMaskNextOwner()); - object_data["permissions"] = permission_entry; } } @@ -742,6 +735,59 @@ LLSD LLPublishedObjectMgr::buildPrimInventoryLLSD(LLViewerObject* object) const return items; } +LLSD LLPublishedObjectMgr::makePermissionsLLSD(U32 owner_mask, U32 next_owner_mask) +{ + LLSD permissions; + permissions["owner"] = static_cast<S32>(owner_mask); + permissions["next_owner"] = static_cast<S32>(next_owner_mask); + return permissions; +} + +LLSD LLPublishedObjectMgr::getObjectPermissionsLLSD(LLViewerObject* object) +{ + if (!object) + { + return LLSD(); + } + + LLPermissions* permissions = + LLSelectMgr::getInstance()->findObjectPermissions(object); + if (permissions) + { + return makePermissionsLLSD(permissions->getMaskOwner(), permissions->getMaskNextOwner()); + } + + // Unselected prims carry no LLPermissions, but object flags still hold this agent's rights. + // next_owner is unavailable from flags and is omitted until ObjectProperties arrives. + U32 owner_mask = 0; + if (object->permModify()) { owner_mask |= PERM_MODIFY; } + if (object->permCopy()) { owner_mask |= PERM_COPY; } + if (object->permTransfer()) { owner_mask |= PERM_TRANSFER; } + if (object->permMove()) { owner_mask |= PERM_MOVE; } + + LLSD permission_entry; + permission_entry["owner"] = static_cast<S32>(owner_mask); + return permission_entry; +} + +bool LLPublishedObjectMgr::computeCanSaveBack(LLViewerObject* root, LLUUID& source_task_id) +{ + source_task_id.setNull(); + if (!root || root->isAttachment()) + { + return false; + } + + LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(root); + if (!node || !node->mValid || node->mFromTaskID.isNull()) + { + return false; + } + + source_task_id = node->mFromTaskID; + return true; +} + LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; @@ -768,12 +814,13 @@ LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const { LLSD link; link["link_id"] = child->getID(); - link["link_number"] = link_number++; - link["link_name"] = get_prim_name(child); + link["link_number"] = link_number; + link["link_name"] = get_prim_name(child, link_number); link["link_description"] = nv_string(child, "Desc"); add_object_permissions(link, child); link["inventory"] = buildPrimInventoryLLSD(child); linked_objects.append(link); + ++link_number; } if (linked_objects.size() > 0) { @@ -1083,25 +1130,37 @@ bool LLPublishedObjectMgr::applyPropertyChange( if (prim_id == root_id) { - bool has_name = !name.empty(); - bool name_changed = has_name && (pub_info->mObjectName != name); - bool desc_changed = (pub_info->mObjectDescription != desc); - if (!name_changed && !desc_changed) - { - return false; - } + bool changed = false; - if (name_changed) + if (!name.empty() && pub_info->mObjectName != name) { pub_info->mObjectName = name; update["object_name"] = name; + changed = true; } - if (desc_changed) + if (pub_info->mObjectDescription != desc) { pub_info->mObjectDescription = desc; update["object_description"] = desc; + changed = true; } - return true; + + // Properties arrive after publish, so this field is backfilled here. + LLViewerObject* root = gObjectList.findObject(root_id); + if (root) + { + LLUUID source_task_id; + const bool can_save_back = computeCanSaveBack(root, source_task_id); + if (can_save_back != pub_info->mCanSaveBackToContents) + { + pub_info->mCanSaveBackToContents = can_save_back; + pub_info->mSourceTaskID = source_task_id; + update["can_save_back"] = can_save_back; + changed = true; + } + } + + return changed; } auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(), @@ -1135,6 +1194,61 @@ bool LLPublishedObjectMgr::applyPropertyChange( return true; } +bool LLPublishedObjectMgr::applyPermissionsChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const LLUUID& owner_id, + U32 owner_mask, + U32 next_owner_mask, + LLSD& update) +{ + PublishedObjectInfo* pub_info = getPublished(root_id); + if (!pub_info) + { + return false; + } + + const LLSD permissions = makePermissionsLLSD(owner_mask, next_owner_mask); + + update = LLSD(); + update["object_id"] = root_id; + + if (prim_id == root_id) + { + bool changed = false; + if (!llsd_equals(permissions, pub_info->mPermissions)) + { + pub_info->mPermissions = permissions; + update["permissions"] = permissions; + changed = true; + } + if (owner_id.notNull() && pub_info->mOwnerID != owner_id) + { + pub_info->mOwnerID = owner_id; + update["owner_id"] = owner_id; + changed = true; + } + return changed; + } + + auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == prim_id; }); + if (prim_it == pub_info->mPrims.end() || llsd_equals(permissions, prim_it->mPermissions)) + { + return false; + } + + prim_it->mPermissions = permissions; + + LLSD modified_entry; + modified_entry["link_id"] = prim_id; + modified_entry["permissions"] = permissions; + LLSD modified_arr = LLSD::emptyArray(); + modified_arr.append(modified_entry); + update["changes"]["linked_objects"]["modified"] = modified_arr; + return true; +} + bool LLPublishedObjectMgr::hasActiveLinksetFlushTimer(const LLUUID& root_id) const { auto it = mLinksetFlushTimers.find(root_id); diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h index 51af129dcb..8a6c8b705c 100644 --- a/indra/newview/llpublishedobjectmgr.h +++ b/indra/newview/llpublishedobjectmgr.h @@ -55,6 +55,7 @@ public: S32 mLinkNumber; std::string mPrimDescription; S16 mInventorySerial; + LLSD mPermissions; }; struct PublishedObjectInfo @@ -73,6 +74,7 @@ public: std::string mRegionName; bool mCanSaveBackToContents{ false }; LLUUID mSourceTaskID; + LLSD mPermissions; std::vector<PublishedPrimInfo> mPrims; std::vector<std::unique_ptr<LLPublishedPrimListener>> mListeners; }; @@ -256,6 +258,12 @@ public: LLSD buildPrimInventoryLLSD(LLViewerObject* object) const; LLSD buildPublishedObjectLLSD(LLViewerObject* root) const; LLSD buildObjectListLLSD() const; + + // Both depend on a valid LLSelectNode, so they yield nothing until ObjectProperties arrives. + static LLSD getObjectPermissionsLLSD(LLViewerObject* object); + static bool computeCanSaveBack(LLViewerObject* root, LLUUID& source_task_id); + static LLSD makePermissionsLLSD(U32 owner_mask, U32 next_owner_mask); + bool buildLinksetUpdateLLSD(const LLUUID& root_id, LLSD& update) const; bool reconcileLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child, F64 request_start_sec); bool reconcileLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); @@ -275,6 +283,13 @@ public: const std::string& name, const std::string& desc, LLSD& update); + bool applyPermissionsChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const LLUUID& owner_id, + U32 owner_mask, + U32 next_owner_mask, + LLSD& update); void beginPendingPublish(const LLUUID& object_id, const std::vector<LLViewerObject*>& prims); bool hasPendingPublish(const LLUUID& object_id) const; diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 93b1bd5eb0..da7c0cb0fa 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -69,9 +69,16 @@ #include "llviewertexteditor.h" #include "llvoinventorylistener.h" #include "roles_constants.h" +#include "workqueue.h" #include <array> +#include <openssl/rand.h> + +#if LL_WINDOWS +#include <aclapi.h> +#endif + namespace { // Per-operation timeouts (seconds) for coroutine-based async RPC handlers. @@ -80,6 +87,11 @@ namespace constexpr F32 NOTECARD_UPLOAD_TIMEOUT = 30.0f; constexpr F32 ITEM_CREATE_TIMEOUT = 30.0f; + // Seconds a client has to answer the handshake: plenty to read a + // file, and no longer than that is a connection kept that has not + // proven itself. + constexpr F64 HANDSHAKE_TIMEOUT = 30.0; + // Linkset flush coalescing delays (seconds). constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f; constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f; @@ -453,7 +465,10 @@ void LLScriptEditorWSServer::onStopped() LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL; - LLNotificationsUtil::add("ExternalEditorServerStopped"); + if (!LLApp::isExiting()) + { + LLNotificationsUtil::add("ExternalEditorServerStopped"); + } } void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) @@ -627,27 +642,27 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c LL_DEBUGS("ScriptEditorWS") << "Setting up script editor connection methods" << LL_ENDL; U32 connection_id = script_connection->getConnectionID(); - // Sync methods (run on the WebSocket I/O thread; must not touch - // main-thread-only viewer state). - script_connection->registerMethod("language.syntax.id", + // All per-connection methods dispatch to the main thread inside a + // coroutine (see LLJSONRPCConnection::processRequest). + script_connection->registerAsyncMethod("language.syntax.id", bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { return s.handleLanguageIdRequest(); })); - script_connection->registerMethod("language.syntax", + script_connection->registerAsyncMethod("language.syntax", bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleSyntaxRequest(params); })); - script_connection->registerMethod("language.syntax.cache", + script_connection->registerAsyncMethod("language.syntax.cache", bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { return s.handleSyntaxCacheRequest(); })); - script_connection->registerMethod("language.syntax.get", + script_connection->registerAsyncMethod("language.syntax.get", bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleSyntaxCacheFileRequest(params); @@ -659,7 +674,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleScriptSubscribe(connection_id, params); })); - script_connection->registerMethod("script.list", + script_connection->registerAsyncMethod("script.list", bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) { return s.handleFileWatcherFileListRequest(); @@ -671,7 +686,6 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleObjectUnpublish(connection_id, params); })); - // Async methods (dispatched to the main thread inside a coroutine). script_connection->registerAsyncMethod("script.unsubscribe", bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { @@ -1060,6 +1074,9 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p msg->sendReliable(host); } + // Force a properties reply so the editor sees the change without an in-world selection. + LLSelectMgr::instance().requestObjectPropertiesFamily(prim); + // Step 4: Return Success Response LLSD response; response["success"] = true; @@ -1253,7 +1270,7 @@ void LLScriptEditorWSServer::broadcastLanguageChange() if (isRunning()) { - broadcastNotification("language.syntax.change", params); + notifyAll("language.syntax.change", params); } } } @@ -2296,7 +2313,6 @@ void LLScriptEditorWSServer::forwardChatToIDE( void LLScriptEditorWSServer::sendRuntimeEvent( const LLPublishedObjectMgr::RuntimeChatEvent& event) const { - std::string script_id; if (event.mItemID.notNull()) { @@ -2363,7 +2379,7 @@ void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::stri if (it != mActiveConnections.end()) { auto connection = it->second.lock(); - if (connection) + if (connection && connection->isAuthenticated()) { connection->notify(method, params); } @@ -2383,7 +2399,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa for (const auto& pair : mActiveConnections) { auto connection = pair.second.lock(); - if (connection) + if (connection && connection->isAuthenticated()) { connection->sendMessage(payload); } @@ -2392,7 +2408,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa // static -std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj) +std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj, S32 link_number) { std::string name = nv_string(obj, "Name"); if (!name.empty()) @@ -2411,8 +2427,8 @@ std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj) return node->mName; } - // Never emit an empty prim/object name to downstream tooling. - return obj->getID().asString(); + // Real prim names need a full ObjectProperties reply, which requires selection. + return (link_number > 1) ? llformat("Link #%d", link_number) : std::string("Object"); } bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) @@ -2440,12 +2456,8 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) // Collect root + all children std::vector<LLViewerObject*> prims = collect_linkset(root); - // Request object properties for each prim in the linkset (root + children), - // matching the hover path so name/description metadata is refreshed. - for (LLViewerObject* prim : prims) - { - LLSelectMgr::instance().requestObjectPropertiesFamily(prim); - } + // The sim coalesces properties-family requests to the root, so one request covers the linkset. + LLSelectMgr::instance().requestObjectPropertiesFamily(root); // Set up a PendingPublish to coordinate inventory loading across all prims. // We register a listener and call requestInventory() on every prim. @@ -2510,24 +2522,14 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) info.mOwnerID = root->mOwnerID; info.mObjectName = pub["object_name"].asString(); info.mObjectDescription = pub["object_description"].asString(); + info.mPermissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(root); if (root->getRegion()) { info.mRegionName = root->getRegion()->getName(); } - LLSelectNode* root_select_node = LLSelectMgr::instance().getSelection()->findNode(root); - if (root_select_node - && root_select_node->mValid - && !root_select_node->mFromTaskID.isNull() - && !root->isAttachment()) - { - info.mCanSaveBackToContents = true; - info.mSourceTaskID = root_select_node->mFromTaskID; - } - else - { - info.mCanSaveBackToContents = false; - info.mSourceTaskID.setNull(); - } + LLUUID source_task_id; + info.mCanSaveBackToContents = LLPublishedObjectMgr::computeCanSaveBack(root, source_task_id); + info.mSourceTaskID = source_task_id; S32 link_num = 1; std::vector<LLViewerObject*> prims = collect_linkset(root); @@ -2535,9 +2537,10 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) { LLPublishedObjectMgr::PublishedPrimInfo prim_info; prim_info.mPrimID = prim->getID(); - prim_info.mPrimName = getPrimName(prim); // Use helper with selection fallback prim_info.mLinkNumber = link_num++; + prim_info.mPrimName = getPrimName(prim, prim_info.mLinkNumber); prim_info.mInventorySerial = static_cast<S16>(prim->getInventorySerial()); + prim_info.mPermissions = LLPublishedObjectMgr::getObjectPermissionsLLSD(prim); info.mPrims.push_back(prim_info); } @@ -2578,12 +2581,9 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) << " (" << pub["object_name"].asString() << ") with " << (prims.size() - 1) << " linked prim(s)" << LL_ENDL; - // Re-request object properties now that the object is published so - // onObjectPropertyChanged can emit object.update for root and linked prims. - for (LLViewerObject* prim : prims) - { - LLSelectMgr::instance().requestObjectPropertiesFamily(prim); - } + // Re-request now that the object is published so onObjectPropertyChanged can emit + // object.update. The sim answers per linkset, so only the root needs asking. + LLSelectMgr::instance().requestObjectPropertiesFamily(root); } void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child) @@ -2745,6 +2745,24 @@ void LLScriptEditorWSServer::onObjectPropertyChanged( } } +void LLScriptEditorWSServer::onObjectPermissionsReceived( + const LLUUID& prim_id, const LLUUID& owner_id, U32 owner_mask, U32 next_owner_mask) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LLViewerObject* prim = gObjectList.findObject(prim_id); + if (!prim) + { + return; + } + + LLSD update; + if (mPublishedObjectManager.applyPermissionsChange( + prim->getRootEdit()->getID(), prim_id, owner_id, owner_mask, next_owner_mask, update)) + { + notifyAll("object.update", update); + } +} + void LLScriptEditorWSServer::unpublishObject(const LLUUID& object_id, const std::string& reason) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; @@ -2782,6 +2800,26 @@ void LLScriptEditorWSConnection::onOpen() LL_INFOS("ScriptEditorWS") << "Script editor JSON-RPC connection opened" << LL_ENDL; + // gAgent, LLVersionInfo, LLSyntaxDefCache, and challenge file I/O below + // are main-thread only; this callback runs on the WS I/O thread. + wptr_t that = weak_from_this(); + if (!LL::WorkQueue::postToMainLoop([that]() + { + auto self = that.lock(); + if (self) + { + self->sendHandshake(); + } + })) + { + LL_WARNS("ScriptEditorWS") << "Main loop unavailable; dropping handshake for new connection" << LL_ENDL; + } +} + +void LLScriptEditorWSConnection::sendHandshake() +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + // Build hello data LLSD handshake; handshake["server_version"] = "1.0.0"; @@ -2792,11 +2830,14 @@ void LLScriptEditorWSConnection::onOpen() handshake["agent_id"] = gAgent.getID(); handshake["agent_name"] = gAgentUsername; - std::string challenge_file = generateChallenge(); - if (!challenge_file.empty()) + const Challenge challenge = writeChallenge(); + if (challenge.mFile.empty()) { - handshake["challenge"] = challenge_file; + // Nothing to prove itself by, so nothing it may do. + sendDisconnect(DisconnectReason::INTERNAL_ERROR, "Unable to issue a challenge"); + return; } + handshake["challenge"] = challenge.mFile; LLSD languages = LLSD::emptyArray(); languages.append("lsl"); @@ -2815,22 +2856,33 @@ void LLScriptEditorWSConnection::onOpen() wptr_t that = weak_from_this(); - // Send session.handshake method call and the response - call("session.handshake", handshake, [that](const LLSD& result, const LLSD& error) { - if (error.isUndefined()) + // The answer comes once, whichever way: from the client, as the + // time runs out, or as the connection closes. + const LLSD sent = call( + "session.handshake", handshake, + [that, challenge](const LLSD& result, const LLSD& error) { + // Whatever the answer, the file has done its work. + LLFile::remove(challenge.mFile); auto self = that.lock(); - if (self) + if (!self) { - self->handleHandshakeResponse(result); + return; } - } - else - { - LL_WARNS("ScriptEditorWS") << "Handshake failed: " - << error["message"].asString() << LL_ENDL; - } - }); + if (error.isDefined()) + { + self->handleHandshakeError(error); + return; + } + self->handleHandshakeResponse(result, challenge.mSecret); + }, + HANDSHAKE_TIMEOUT); + if (sent.isUndefined()) + { + LLFile::remove(challenge.mFile); + handleHandshakeError(LLSD().with("code", LLJSONRPCConnection::RPCError::INTERNAL_ERROR).with("message", "Handshake not sent")); + return; + } LL_INFOS("ScriptEditorWS") << "Sent handshake call to new editor client" << LL_ENDL; } @@ -2862,40 +2914,36 @@ void LLScriptEditorWSConnection::sendDisconnect(DisconnectReason reason, const s closeConnection(1000, message); } -void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) +void LLScriptEditorWSConnection::handleHandshakeError(const LLSD& error) { - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL; - - mClientName = result["client_name"].asString(); - mClientVersion = result["client_version"].asString(); - mProtocolVersion = result["protocol_version"].asString(); - - // Validate challenge response (if a challenge was issued). - const bool challenge_issued = mChallenge.notNull(); - bool valid_response = true; - if (challenge_issued) + const S32 code = error["code"].asInteger(); + if (code == LLJSONRPCConnection::RPCError::CONNECTION_CLOSED) { - valid_response = result.has("challenge_response") && - (result["challenge_response"].asUUID() == mChallenge); - mChallenge.setNull(); + // Gone already. + return; } + LL_WARNS("ScriptEditorWS") << "Handshake failed: " << error["message"].asString() << LL_ENDL; + sendDisconnect(code == LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT ? DisconnectReason::TIMEOUT : DisconnectReason::PROTOCOL_ERROR, + "Handshake failed"); +} - // Always clean up the temporary challenge file if one was created, - // regardless of validation outcome. - if (!mChallengeFile.empty()) - { - LLFile::remove(mChallengeFile); - mChallengeFile.clear(); - } +void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result, const LLUUID& secret) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LL_INFOS("ScriptEditorWS") << "Processing handshake response from client" << LL_ENDL; - if (challenge_issued && !valid_response) + // Only something that could read the file knows what it says. + if (!result.isMap() || result["challenge_response"].asUUID() != secret) { LL_WARNS("ScriptEditorWS") << "Invalid or missing challenge response from client" << LL_ENDL; sendDisconnect(DisconnectReason::PROTOCOL_ERROR, "Invalid challenge response"); return; } + mClientName = result["client_name"].asString(); + mClientVersion = result["client_version"].asString(); + mProtocolVersion = result["protocol_version"].asString(); + if (mProtocolVersion != "1.0") { LL_WARNS("ScriptEditorWS") << "Protocol version mismatch. Expected: 1.0, Got: " @@ -2921,29 +2969,134 @@ void LLScriptEditorWSConnection::handleHandshakeResponse(const LLSD& result) } } + // Let in before it is told so: what it asks on hearing it must be + // answered. + setAuthenticated(true); notify("session.ok"); LL_INFOS("ScriptEditorWS") << "Handshake completed successfully." << LL_ENDL; } -std::string LLScriptEditorWSConnection::generateChallenge() +// static +LLScriptEditorWSConnection::Challenge LLScriptEditorWSConnection::writeChallenge() { - mChallenge.generate(); + // A secret nobody could work out: a new id is made of the time and + // the network card's address, which are anybody's to know. + Challenge challenge; + if (RAND_bytes(challenge.mSecret.mData, UUID_BYTES) != 1) + { + LL_WARNS("ScriptEditorWS") << "Unable to make a challenge secret" << LL_ENDL; + return {}; + } - mChallengeFile = std::string(LLFile::tmpdir()) + "sl_script_challenge_" + mChallenge.asString() + ".tmp"; + // Named for nothing it holds, made new rather than written over + // anything, or through a link, already by that name, and readable by + // the user alone: the name goes to whoever connects, and the temp + // folder can be everyone's. + const std::string file = LLFile::tmpdir() + "sl_script_challenge_" + LLUUID::generateNewID().asString() + ".tmp"; + const std::string text = challenge.mSecret.asString(); + if (!writeUserOnlyFile(file, text)) + { + LL_WARNS("ScriptEditorWS") << "Unable to write challenge file " << file << LL_ENDL; + return {}; + } + + challenge.mFile = file; + return challenge; +} - llofstream file(mChallengeFile.c_str()); - if (!file.is_open()) +// static +bool LLScriptEditorWSConnection::writeUserOnlyFile(const std::string& file, const std::string& text) +{ +#if LL_WINDOWS + // LLFile::open() ignores the POSIX permission bits on Windows -- its + // decode_attributes() only maps them to a normal/read-only file + // attribute, never to an ACL -- so a mode like 0600 there does not + // keep other local accounts from reading the file. Build a DACL + // explicitly, granting only the current user's SID access. + HANDLE token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { - LL_WARNS("ScriptEditorWS") << "Unable to open challenge file: " << mChallengeFile << LL_ENDL; - mChallenge.setNull(); - mChallengeFile.clear(); - return std::string(); + LL_WARNS("ScriptEditorWS") << "Unable to open the process token for the challenge file's ACL" << LL_ENDL; + return false; + } + + DWORD needed = 0; + GetTokenInformation(token, TokenUser, nullptr, 0, &needed); + std::vector<BYTE> user_info(needed); + const bool got_user = needed != 0 && GetTokenInformation(token, TokenUser, user_info.data(), needed, &needed); + CloseHandle(token); + if (!got_user) + { + LL_WARNS("ScriptEditorWS") << "Unable to read the current user's SID for the challenge file's ACL" << LL_ENDL; + return false; } + PSID user_sid = reinterpret_cast<PTOKEN_USER>(user_info.data())->User.Sid; - file << mChallenge; - file.close(); + EXPLICIT_ACCESSW ea = {}; + ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE; + ea.grfAccessMode = SET_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.TrusteeType = TRUSTEE_IS_USER; + ea.Trustee.ptstrName = reinterpret_cast<LPWSTR>(user_sid); - return mChallengeFile; + PACL dacl = nullptr; + if (SetEntriesInAclW(1, &ea, nullptr, &dacl) != ERROR_SUCCESS) + { + LL_WARNS("ScriptEditorWS") << "Unable to build an ACL for the challenge file" << LL_ENDL; + return false; + } + + SECURITY_DESCRIPTOR sd; + const bool sd_ok = InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) && + SetSecurityDescriptorDacl(&sd, TRUE, dacl, FALSE); + if (!sd_ok) + { + LL_WARNS("ScriptEditorWS") << "Unable to build a security descriptor for the challenge file" << LL_ENDL; + LocalFree(dacl); + return false; + } + + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = &sd; + sa.bInheritHandle = FALSE; + + const std::wstring wfile = ll_convert<std::wstring>(file); + // CREATE_NEW: fails if the file already exists, matching LLFile::noreplace. + HANDLE handle = CreateFileW(wfile.c_str(), GENERIC_WRITE, 0, &sa, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + LocalFree(dacl); + if (handle == INVALID_HANDLE_VALUE) + { + return false; + } + + DWORD written = 0; + const bool ok = WriteFile(handle, text.data(), static_cast<DWORD>(text.size()), &written, nullptr) && + written == text.size(); + CloseHandle(handle); + if (!ok) + { + LLFile::remove(file); + } + return ok; +#else + // POSIX open() honors the mode bits directly. + std::error_code ec; + LLFile out(file, LLFile::out | LLFile::noreplace, ec, 0600); + if (ec) + { + return false; + } + const bool written = out.write(text.data(), static_cast<S64>(text.size()), ec) == static_cast<S64>(text.size()) && !ec; + const bool closed = out.close(ec) == 0; + if (!written || !closed) + { + LLFile::remove(file); + return false; + } + return true; +#endif } diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 6eb8385210..9919800ebe 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -69,6 +69,10 @@ public: LLScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t server, const LLWebsocketMgr::connection_h& handle) : LLJSONRPCConnection(server, handle) { + // Anything that can reach the port can connect: nothing it asks + // is answered until it has proven it can read the user's files. + setAuthenticated(false); + // Reserve id 0 as the "unassigned" sentinel used by EditorSubscription; // on wrap, skip past it. U32 id; @@ -93,12 +97,33 @@ public: private: using string_set_t = std::set<std::string>; + + // What the client proves itself with: a secret written to a file only + // the user can read, under a name that says nothing of it. The client + // is told where the file is and answers with what it says. + struct Challenge + { + LLUUID mSecret; + std::string mFile; + }; + // Empty where no secret could be made or written. + static Challenge writeChallenge(); + + // Writes text to a new file at path, readable only by the current + // user. On Windows this means an explicit ACL, since LLFile::open() + // there ignores POSIX permission bits entirely. Fails if the file + // already exists. + static bool writeUserOnlyFile(const std::string& path, const std::string& text); + /** * @brief Handle the handshake response from the client * @param result The response data from the client containing client information + * @param secret What the challenge file said, which the client must answer with */ - void handleHandshakeResponse(const LLSD& result); - std::string generateChallenge(); + void handleHandshakeResponse(const LLSD& result, const LLUUID& secret); + // The handshake refused, unanswered in time, or never sent. + void handleHandshakeError(const LLSD& error); + void sendHandshake(); LLScriptEdContainer* getEditor() const; std::shared_ptr<LLScriptEditorWSServer> getServer() const; @@ -113,8 +138,6 @@ private: std::string mScriptLanguage; ///< Programming language of the script (lsl, luau, etc.) string_set_t mLanguages; ///< Set of supported scripting languages string_set_t mFeatures; ///< Active client features (live_sync, compilation, etc.) - LLUUID mChallenge; - std::string mChallengeFile; ///< Temporary file used for challenge-response verification static std::atomic<U32> sNextConnectionID; }; @@ -151,6 +174,11 @@ private: * ## Security Considerations * * - Server binds to localhost only by default for security + * - A connection from a browser -- any upgrade request with an Origin -- is + * refused, so no web page, the viewer's own included, can reach it + * - Nothing a client asks is answered, and nothing is sent it, until it + * has answered the handshake's challenge with what a file only the user + * can read says; a wrong answer, or none within 30 seconds, closes it * - JSON-RPC 2.0 structured protocol with validation * - Rate limiting handled by base JSON-RPC server * - Error handling with standardized JSON-RPC error codes @@ -225,6 +253,7 @@ public: void onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id); void onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id); void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial = -1); + void onObjectPermissionsReceived(const LLUUID& prim_id, const LLUUID& owner_id, U32 owner_mask, U32 next_owner_mask); void onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child); void onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); @@ -280,7 +309,7 @@ protected: ValidatedItem validatePublishedItem(const LLSD& params, U32 permMask) const; // --- Object Content Publishing (helpers) --- - static std::string getPrimName(LLViewerObject* obj); + static std::string getPrimName(LLViewerObject* obj, S32 link_number = 1); void notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const; void notifyAll(const std::string& method, const LLSD& params) const; void buildAndSendPublish(const LLUUID& object_id); diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index a09b54ffac..f9108fcc64 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -6108,11 +6108,14 @@ void LLSelectMgr::processObjectProperties(LLMessageSystem* msg, void** user_data node->mInventorySerial = inv_serial; node->mSitName.assign(sit_name); node->mTouchName.assign(touch_name); + } - if (auto ws_server = LLScriptEditorWSServer::getServer()) - { - ws_server->onObjectPropertyChanged(id, name, desc, inv_serial); - } + // Published objects need property updates even when not selected. + LLScriptEditorWSServer::ptr_t ws_server = LLScriptEditorWSServer::getServer(); + if (ws_server) + { + ws_server->onObjectPropertyChanged(id, name, desc, inv_serial); + ws_server->onObjectPermissionsReceived(id, owner_id, owner_mask, next_owner_mask); } } @@ -6213,6 +6216,7 @@ void LLSelectMgr::processObjectPropertiesFamily(LLMessageSystem* msg, void** use if (auto ws_server = LLScriptEditorWSServer::getServer()) { ws_server->onObjectPropertyChanged(id, name, desc); + ws_server->onObjectPermissionsReceived(id, owner_id, owner_mask, next_owner_mask); } } diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 7d3b554709..fa10288132 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -36,7 +36,6 @@ #include "llfasttimerview.h" #include "llfloater360capture.h" #include "llfloaterabout.h" -#include "llfloateraddpaymentmethod.h" #include "llfloaterauction.h" #include "llfloaterautoreplacesettings.h" #include "llfloateravatarpicker.h" @@ -242,7 +241,6 @@ public: const std::list<std::string> blacklist_untrusted = { "360capture", "block_timers", - "add_payment_method", "appearance", "associate_listing", "avatar_picker", @@ -332,7 +330,6 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("360capture", "floater_360capture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloater360Capture>); LLFloaterReg::add("block_timers", "floater_fast_timers.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFastTimerView>); LLFloaterReg::add("about_land", "floater_about_land.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLand>); - LLFloaterReg::add("add_payment_method", "floater_add_payment_method.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAddPaymentMethod>); LLFloaterReg::add("appearance", "floater_my_appearance.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterSidePanelContainer>); LLFloaterReg::add("associate_listing", "floater_associate_listing.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAssociateListing>); LLFloaterReg::add("auction", "floater_auction.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAuction>); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index b549428062..dfd03ece35 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -1539,30 +1539,12 @@ void LLViewerWindow::handleCloseRequestCanceled() void LLViewerWindow::handleSuspendRequest() { - static LLCachedControl<S32> os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); - if (os_hibernation_mode == 0) - { - LL_INFOS() << "Got a 'suspend' event from OS" << LL_ENDL; - // Viewer doesn't handle hibernation. - // Just send statistics. - LLAppViewer::instance()->sendViewerStatistics(false); - } - else - { - LL_INFOS() << "Got a 'suspend' event from OS, disconnecting" << LL_ENDL; - // Viewer is set to prevent hibernation if agent isn't away. - // If we got here, likely Agent 'went' away then viewer got - // a hibernation message. - // We have a limited timeframe. Sends stats then disconnect. - LLViewerRegion* region = gAgent.getRegion(); - if (region) - { - LLAppViewer::instance()->sendViewerStatistics(true); - LLAppViewer::instance()->metricsSend(!gDisconnected); - // Make sure to show a message. - LLAppViewer::instance()->forceDisconnect(LLTrans::getString("YouHaveBeenDisconnected")); - } - } + LLAppViewer::instance()->sendViewerStatistics(false); + // Todo: this should send a disconnect request as viewer + // can't keep heartbeat up while suspended and will get + // disconnected within a minute. + // Add a disconnect here once 'prevent OS from sleeping' + // feature is ready. } bool LLViewerWindow::handleCloseRequest(LLWindow *window, bool from_user) diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 1cbce33cbd..fb463fc94e 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -289,7 +289,26 @@ void LLWebRTCVoiceClient::initWebRTC() mWebRTCDeviceInterface = llwebrtc::getDeviceInterface(); mWebRTCDeviceInterface->setDevicesObserver(this); - mMainQueue = LL::WorkQueue::getInstance("mainloop"); + + if (!mVoiceWorkQueue) + { + mVoiceWorkQueue = std::make_shared<LL::WorkQueue>("VoiceWebRTC"); + // Service this queue every idle tick, on the main thread. + doOnIdleRepeating( + [queue = LL::WorkQueue::weak_t(mVoiceWorkQueue)]() -> bool + { + auto locked = queue.lock(); + if (!locked || locked->isClosed()) + { + // Done + return true; + } + locked->runPending(); + // Returning false means "call me again next idle". + return false; + }); + } + refreshDeviceLists(); } @@ -352,11 +371,11 @@ void LLWebRTCVoiceClient::drainConnections() LLTimer timer; while (!sessionState::allSessionsClosed() && timer.getElapsedTimeF32() < DRAIN_TIMEOUT_SECONDS) { - // OnPeerConnectionClosed comes back through the main queue, so it has + // OnPeerConnectionClosed comes back through the queue, so it has // to be pumped or the state machines never see connections finish. - if (auto main_queue = mMainQueue.lock()) + if (mVoiceWorkQueue) { - main_queue->runFor(std::chrono::milliseconds(DRAIN_POLL_MS)); + mVoiceWorkQueue->runFor(std::chrono::milliseconds(DRAIN_POLL_MS)); } sessionState::processSessionStates(); @@ -714,7 +733,7 @@ void LLWebRTCVoiceClient::voiceConnectionCoro() updatePosition(); } } - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceWorkQueue, [=, this] { if (sShuttingDown) { @@ -836,7 +855,7 @@ void LLWebRTCVoiceClient::OnDevicesChanged(const llwebrtc::LLWebRTCVoiceDeviceLi const llwebrtc::LLWebRTCVoiceDeviceList& capture_devices) { - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceWorkQueue, [=, this] { OnDevicesChangedImpl(render_devices, capture_devices); @@ -2524,7 +2543,7 @@ LLVoiceWebRTCConnection::LLVoiceWebRTCConnection(const LLUUID ®ionID, const s mWebRTCPeerConnectionInterface = llwebrtc::newPeerConnection(); mWebRTCPeerConnectionInterface->setSignalingObserver(this); - mMainQueue = LL::WorkQueue::getInstance("mainloop"); + mVoiceMainQueue = LLWebRTCVoiceClient::getInstance()->getVoiceWorkQueue(); } LLVoiceWebRTCConnection::~LLVoiceWebRTCConnection() @@ -2558,7 +2577,7 @@ LLVoiceWebRTCConnection::~LLVoiceWebRTCConnection() // callback from llwebrtc void LLVoiceWebRTCConnection::OnIceGatheringState(llwebrtc::LLWebRTCSignalingObserver::EIceGatheringState state) { - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { LL_DEBUGS("Voice") << "Ice Gathering voice account. " << state << LL_ENDL; @@ -2582,7 +2601,7 @@ void LLVoiceWebRTCConnection::OnIceGatheringState(llwebrtc::LLWebRTCSignalingObs // callback from llwebrtc void LLVoiceWebRTCConnection::OnIceCandidate(const llwebrtc::LLWebRTCIceCandidate& candidate) { - LL::WorkQueue::postMaybe(mMainQueue, [=, this] { mIceCandidates.push_back(candidate); }); + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { mIceCandidates.push_back(candidate); }); } void LLVoiceWebRTCConnection::processIceUpdates() @@ -2698,7 +2717,7 @@ void LLVoiceWebRTCConnection::processIceUpdatesCoro(connectionPtr_t connection) void LLVoiceWebRTCConnection::OnOfferAvailable(const std::string &sdp) { connectionPtr_t connection = shared_from_this(); - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=] { if (connection->mShutDown) { @@ -2726,7 +2745,7 @@ void LLVoiceWebRTCConnection::OnOfferAvailable(const std::string &sdp) void LLVoiceWebRTCConnection::OnAudioEstablished(llwebrtc::LLWebRTCAudioInterface* audio_interface) { connectionPtr_t connection = shared_from_this(); - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=] { if (connection->mShutDown) { @@ -2749,7 +2768,7 @@ void LLVoiceWebRTCConnection::OnAudioEstablished(llwebrtc::LLWebRTCAudioInterfac // callback from llwebrtc void LLVoiceWebRTCConnection::OnRenegotiationNeeded() { - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { LL_DEBUGS("Voice") << "Voice channel requires renegotiation." << LL_ENDL; setVoiceConnectionState(VOICE_STATE_SESSION_RETRY); @@ -2760,7 +2779,7 @@ void LLVoiceWebRTCConnection::OnRenegotiationNeeded() // callback from llwebrtc void LLVoiceWebRTCConnection::OnPeerConnectionClosed() { - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { LL_DEBUGS("Voice") << "Peer connection has closed." << LL_ENDL; if (mVoiceConnectionState == VOICE_STATE_WAIT_FOR_CLOSE) @@ -3271,7 +3290,7 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() // llwebrtc callback void LLVoiceWebRTCConnection::OnDataReceived(const std::string& data, bool binary) { - LL::WorkQueue::postMaybe(mMainQueue, [=, this] { LLVoiceWebRTCConnection::OnDataReceivedImpl(data, binary); }); + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { LLVoiceWebRTCConnection::OnDataReceivedImpl(data, binary); }); } // @@ -3500,7 +3519,7 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b void LLVoiceWebRTCConnection::OnDataChannelReady(llwebrtc::LLWebRTCDataInterface *data_interface) { connectionPtr_t connection = shared_from_this(); - LL::WorkQueue::postMaybe(mMainQueue, + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=] { if (connection->mShutDown) { @@ -3542,7 +3561,7 @@ void LLVoiceWebRTCConnection::sendJoin() void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data) { - LL::WorkQueue::postMaybe(mMainQueue, [=, this] + LL::WorkQueue::postMaybe(mVoiceMainQueue, [=, this] { if (mShutDown) { diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h index 420d431fae..76e172ff3d 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -40,6 +40,7 @@ class LLWebRTCProtocolParser; #include "llcoros.h" #include "llparcel.h" #include "llmutelist.h" +#include "workqueue.h" #include <queue> #include "boost/json.hpp" @@ -85,6 +86,8 @@ public: // drainConnections() and ~LLVoiceWebRTCConnection(). static bool isWebRTCTerminated() { return sWebRTCTerminated; } + LL::WorkQueue::weak_t getVoiceWorkQueue() const { return mVoiceWorkQueue; } + const LLVoiceVersionInfo& getVersion() override; void updateVersion(); @@ -468,7 +471,7 @@ private: /// llwebrtc::terminate(). Bounded and best effort. void drainConnections(); - LL::WorkQueue::weak_t mMainQueue; + LL::WorkQueue::ptr_t mVoiceWorkQueue; F32 mTuningMicGain; int mTuningSpeakerVolume; @@ -693,7 +696,7 @@ class LLVoiceWebRTCConnection : } EVoiceConnectionState; EVoiceConnectionState mVoiceConnectionState; - LL::WorkQueue::weak_t mMainQueue; + LL::WorkQueue::weak_t mVoiceMainQueue; void setVoiceConnectionState(EVoiceConnectionState new_voice_connection_state) { diff --git a/indra/newview/luau.txt b/indra/newview/luau.txt new file mode 100644 index 0000000000..45d93adef3 --- /dev/null +++ b/indra/newview/luau.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019-2025 Roblox Corporation +Copyright (c) 1994–2019 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml index 981c35800f..7be9a9d555 100644 --- a/indra/newview/skins/default/xui/da/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/da/panel_preferences_setup.xml @@ -45,12 +45,4 @@ <combo_box.item label="Installér automatisk" name="Install_automatically"/> <combo_box.item label="Hent og installér opdateringer manuelt" name="Install_manual"/> </combo_box> - <text name="os_hibernation_mode"> - Forhindr OS i at gå i dvale, hvis du ikke er væk: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Forhindrer kun OS i at gå i dvale, hvis brugeren ikke er væk"> - <combo_box.item label="Kan gå i dvale" name="can_hibernate"/> - <combo_box.item label="Kan ikke gå i dvale, kan slukke skærmen" name="cant_hibernate"/> - <combo_box.item label="Kan ikke gå i dvale, kan ikke slukke skærmen" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml index ef7ac09dba..a8509cabac 100644 --- a/indra/newview/skins/default/xui/de/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/de/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Proxy-Einstellungen: </text> <button label="Proxy-Einstellungen ändern" label_selected="Durchsuchen" name="set_proxy" width="160"/> - <text name="os_hibernation_mode"> - OS-Ruhezustand verhindern, wenn nicht abwesend: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Verhindert den OS-Ruhezustand nur, wenn der Benutzer nicht abwesend ist"> - <combo_box.item label="Ruhezustand möglich" name="can_hibernate"/> - <combo_box.item label="Kein Ruhezustand, Bildschirm kann ausgeschaltet werden" name="cant_hibernate"/> - <combo_box.item label="Kein Ruhezustand, Bildschirm kann nicht ausgeschaltet werden" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/en/floater_add_payment_method.xml b/indra/newview/skins/default/xui/en/floater_add_payment_method.xml deleted file mode 100644 index 18f5f2af32..0000000000 --- a/indra/newview/skins/default/xui/en/floater_add_payment_method.xml +++ /dev/null @@ -1,142 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<floater - height="405" - width="900" - layout="topleft" - name="floater_add_payment_method" - single_instance="true" - show_title="false" - legacy_header_height="0" - header_height="0" - background_visible="false" - can_resize="false" - can_drag_on_left="false" - can_minimize="false" - can_close="false"> - <floater.string - name="continue_url"> - https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1 - </floater.string> - <floater.string - name="continue_staging_url"> - https://secondlife-staging.com/my/lindex/buy.php?associate_for_viewer=1 - </floater.string> - <panel - background_opaque="false" - bg_alpha_color="PanelGray" - border_visible="false" - background_visible="true" - label="wrapper_panel" - layout="topleft" - left="0" - name="wrapper_panel" - top="0" - height="405" - width="900" - follows="all" - translate="false"> - <text - type="string" - length="1" - follows="top|left|right" - font="SansSerifLargeBold" - text_color="White" - layout="topleft" - left="0" - height="14" - top_pad="30" - width="900" - halign="center" - name="title_txt"> - Add a payment method to buy Linden dollars and enjoy more of Second Life. - </text> - <button - follows="top|left|right" - height="24" - label="Get started" - layout="topleft" - left="320" - name="continue_btn" - image_unselected="PushButton_Login" - image_pressed="PushButton_Login_Pressed" - image_hover_unselected="PushButton_Login_Over" - label_color="White" - top_pad ="15" - pad_bottom="1" - width="140"/> - <button - follows="top|left|right" - height="24" - label="Later" - layout="topleft" - left_pad="9" - pad_bottom="1" - name="close_btn" - width="90"/> - <icon - height="195" - width="260" - image_name="add_payment_image_left" - layout="topleft" - left="30" - name="image_left" - top_pad="30" - use_draw_context_alpha="false"/> - <icon - height="195" - width="260" - image_name="add_payment_image_center" - layout="topleft" - left_pad="30" - name="image_center" - use_draw_context_alpha="false"/> - <icon - height="195" - width="260" - image_name="add_payment_image_right" - layout="topleft" - left_pad="30" - name="image_right" - use_draw_context_alpha="false"/> - <text - type="string" - length="1" - follows="top|left" - text_color="White" - height="75" - width="260" - top_pad="15" - layout="topleft" - word_wrap="true" - left="30" - name="image_left_desc"> - Create an avatar you love. Choose from millions of items in stores and the Second Life Marketplace. Your style is here. - </text> - <text - type="string" - length="1" - follows="top|left" - text_color="White" - height="75" - width="260" - layout="topleft" - word_wrap="true" - left_pad="30" - name="image_center_desc"> - Donate to your favorite performers and hosts. Your support helps them create great experiences. - </text> - <text - type="string" - length="1" - follows="top|left" - text_color="White" - height="75" - width="260" - layout="topleft" - word_wrap="true" - left_pad="30" - name="image_right_desc"> - Make the home of your dreams. With millions of items available to purchase, you can make your retreat unique. - </text> - </panel> -</floater> diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 97be1f0bf6..8a8ca26acb 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -1374,7 +1374,7 @@ function="World.EnvPreset" parameter="script_debug" /> </menu_item_check> <menu_item_call - label="Recompile Scripts (Mono)" + label="Recompile LSL Scripts (Mono)" name="Mono"> <menu_item_call.on_click function="Tools.SelectedScriptAction" @@ -1383,7 +1383,7 @@ function="World.EnvPreset" function="EditableSelectedMono" /> </menu_item_call> <menu_item_call - label="Recompile Scripts (LSL)" + label="Recompile LSL Scripts (Legacy LSL)" name="LSL"> <menu_item_call.on_click function="Tools.SelectedScriptAction" @@ -1392,20 +1392,20 @@ function="World.EnvPreset" function="EditableSelected" /> </menu_item_call> <menu_item_call - label="Recompile Scripts (Lua)" - name="Lua"> + label="Recompile LSL Scripts (LSL on Lua)" + name="LSL on Luau"> <menu_item_call.on_click function="Tools.SelectedScriptAction" - parameter="compile lua" /> + parameter="compile lsl-luau" /> <menu_item_call.on_enable function="EditableSelectedLua" /> </menu_item_call> <menu_item_call - label="Recompile Scripts (LSL on Luau)" - name="LSL on Luau"> + label="Recompile Lua Scripts" + name="Lua"> <menu_item_call.on_click function="Tools.SelectedScriptAction" - parameter="compile lsl-luau" /> + parameter="compile lua" /> <menu_item_call.on_enable function="EditableSelectedLua" /> </menu_item_call> diff --git a/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml b/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml index eeb37eddcf..80d5bed4a5 100644 --- a/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml +++ b/indra/newview/skins/default/xui/en/panel_inbox_inventory.xml @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="utf-8" standalone="yes"?> +<!-- preinitialize_views="false" will require explicitly building views elsewhere anyway, see viewer #6348 --> <inbox_inventory_panel accepts_drag_and_drop="false" name="inventory_inbox" @@ -13,7 +14,7 @@ border="false" bevel_style="none" show_item_link_overlays="true" - preinitialize_views="false" + preinitialize_views="true" > <scroll reserve_scroll_corner="false" /> </inbox_inventory_panel> diff --git a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml index d5f2f69df7..836c6094c5 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_setup.xml @@ -246,41 +246,4 @@ <button.commit_callback function="Pref.Proxy" /> </button> - <text - type="string" - length="1" - follows="left|top" - height="10" - layout="topleft" - left="30" - name="os_hibernation_mode" - mouse_opaque="false" - top_pad="8" - width="300"> - Prevent OS from hibernating if not Away: - </text> - <combo_box - control_name="OSHibernationMode" - follows="left|top" - height="23" - layout="topleft" - left_delta="50" - top_pad="10" - name="os_hibernation_combobox" - width="300" - tool_tip="Only prevents OS from sleeping if user is not away"> - <combo_box.item - label="Can hibernate" - name="can_hibernate" - value="0" /> - <combo_box.item - label="Can't hibernate, can turn screen off" - name="cant_hibernate" - value="1" /> - <combo_box.item - label="Can't hibernate, can't turn screen off" - name="cant_hibernate_screen" - value="2" /> - </combo_box> </panel> - diff --git a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml index 27b3c407cf..36888850c4 100644 --- a/indra/newview/skins/default/xui/es/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/es/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Configuración de proxy: </text> <button label="Ajustar la configuración de proxy" label_selected="Elegir" name="set_proxy"/> - <text name="os_hibernation_mode"> - Evitar que el sistema entre en hibernación si no está Ausente: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Solo evita que el sistema entre en suspensión si el usuario no está ausente"> - <combo_box.item label="Puede hibernar" name="can_hibernate"/> - <combo_box.item label="No puede hibernar, puede apagar la pantalla" name="cant_hibernate"/> - <combo_box.item label="No puede hibernar, no puede apagar la pantalla" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml b/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml index 60cbe93e6e..c010fa9e90 100644 --- a/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/fr/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Paramètres de proxy : </text> <button label="Définir" label_selected="Parcourir" name="set_proxy"/> - <text name="os_hibernation_mode"> - Empêcher l’OS d’hiberner si vous n’êtes pas absent : - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Empêche seulement la mise en veille de l’OS si l’utilisateur n’est pas absent"> - <combo_box.item label="Peut hiberner" name="can_hibernate"/> - <combo_box.item label="Ne peut pas hiberner, peut éteindre l’écran" name="cant_hibernate"/> - <combo_box.item label="Ne peut pas hiberner, ne peut pas éteindre l’écran" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/it/panel_preferences_setup.xml b/indra/newview/skins/default/xui/it/panel_preferences_setup.xml index 95ecf05617..c9d90539e1 100644 --- a/indra/newview/skins/default/xui/it/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/it/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Impostazioni proxy: </text> <button label="Regola impostazioni proxy" label_selected="Sfoglia" name="set_proxy" width="160"/> - <text name="os_hibernation_mode"> - Impedisci al sistema operativo di ibernarsi se non sei Assente: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Impedisce al sistema operativo di andare in sospensione solo se l'utente non è assente"> - <combo_box.item label="Può ibernarsi" name="can_hibernate"/> - <combo_box.item label="Non può ibernarsi, può spegnere lo schermo" name="cant_hibernate"/> - <combo_box.item label="Non può ibernarsi, non può spegnere lo schermo" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml b/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml deleted file mode 100644 index b420bf7d20..0000000000 --- a/indra/newview/skins/default/xui/ja/floater_add_payment_method.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<floater name="floater_add_payment_method"> - <floater.string name="continue_url"> - https://secondlife.com/my/lindex/buy.php?associate_for_viewer=1&lang=ja-JP - </floater.string> - <panel label="wrapper_panel" name="wrapper_panel"> - <text name="title_txt"> - 支払い方法を追加してリンデンドルを購入し、Second Lifeをもっと楽しみましょう。 - </text> - <button label="始めましょう" name="continue_btn"/> - <button label="また後で"/> - <icon name="image_left"/> - <icon name="image_center"/> - <icon name="image_right"/> - <text name="image_left_desc"> - お気に入りのアバターを作成しましょう。ストアやSecond Lifeマーケットプレイスにある何百万ものアイテムからお選びください。あなただけのスタイルがここにあります。 - </text> - <text name="image_center_desc"> - お気に入りのパフォーマーやホストに寄付をしましょう。あなたのサポートは、彼らが素晴らしい体験を生み出すのに役立ちます。 - </text> - <text name="image_right_desc"> - あなたの夢の家を作りましょう。何百万ものアイテムを購入できるので、あなただけの住まいを作ることができます。 - </text> - </panel> -</floater> diff --git a/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml b/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml index 8940ca5bc9..959f827a61 100644 --- a/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/ja/panel_preferences_setup.xml @@ -36,12 +36,4 @@ プロキシ設定: </text> <button label="プロキシ設定を調整" label_selected="参照" name="set_proxy"/> - <text name="os_hibernation_mode"> - 離席中でない場合、OS の休止状態を防止: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="ユーザーが離席中でない場合にのみ、OS のスリープを防止します"> - <combo_box.item label="休止状態を許可" name="can_hibernate"/> - <combo_box.item label="休止状態を禁止(画面オフは可)" name="cant_hibernate"/> - <combo_box.item label="休止状態を禁止(画面オフも不可)" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml b/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml deleted file mode 100644 index 2ef78d3d35..0000000000 --- a/indra/newview/skins/default/xui/pl/floater_add_payment_method.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes" ?> -<floater name="floater_add_payment_method"> - <panel name="wrapper_panel"> - <text name="title_txt"> - Dodaj metodę płatności, aby kupić Lindeny i lepiej doświadczać Second Life. - </text> - <button label="Rozpocznij" name="continue_btn" /> - <button label="Później" name="close_btn" /> - <text name="image_left_desc"> - Stwórz awatara, którego pokochasz. Wybieraj spośród milionów sklepów i Second Life Marketplace. Twój styl jest tutaj. - </text> - <text name="image_center_desc"> - Wspieraj swoich ulubionych wykonawców i gospodarzy. Pomożesz im tworzyć wspaniałe doświadczenia. - </text> - <text name="image_right_desc"> - Stwórz dom swoich marzeń. Miliony przedmiotów dostępnych do kupienia mogą uczynić Twoje gniazdko wyjątkowym. - </text> - </panel> -</floater> diff --git a/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml b/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml index 7147f024cb..bd36f10509 100644 --- a/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/pl/panel_preferences_setup.xml @@ -33,12 +33,4 @@ Ustawienia proxy: </text> <button label="Dostosuj ustawienia" label_selected="Przeglądaj" name="set_proxy" /> - <text name="os_hibernation_mode"> - Zapobiegaj hibernacji systemu, gdy użytkownik nie jest AFK: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Zapobiega usypianiu systemu tylko wtedy, gdy użytkownik nie jest AFK"> - <combo_box.item label="Może hibernować" name="can_hibernate"/> - <combo_box.item label="Nie może hibernować, może wyłączyć ekran" name="cant_hibernate"/> - <combo_box.item label="Nie może hibernować, nie może wyłączyć ekranu" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml b/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml index 101f58488d..56fc225bc0 100644 --- a/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/pt/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Configurações de proxy: </text> <button label="Ajustar configurações de proxy" label_selected="Procurar" name="set_proxy" width="180"/> - <text name="os_hibernation_mode"> - Impedir que o sistema entre em hibernação se não estiver Ausente: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Só impede que o sistema entre em suspensão se o utilizador não estiver ausente"> - <combo_box.item label="Pode hibernar" name="can_hibernate"/> - <combo_box.item label="Não pode hibernar, pode desligar o ecrã" name="cant_hibernate"/> - <combo_box.item label="Não pode hibernar, não pode desligar o ecrã" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/pt/settings_comments.xml b/indra/newview/skins/default/xui/pt/settings_comments.xml index 22f6487847..63d68a3f5b 100644 --- a/indra/newview/skins/default/xui/pt/settings_comments.xml +++ b/indra/newview/skins/default/xui/pt/settings_comments.xml @@ -1435,8 +1435,6 @@ <string>Número de logins bem-sucedidos no Second Life.</string> <key>NvAPICreateApplicationProfile</key> <string>Criar perfil de aplicativo NVIDIA para configurações otimizadas.</string> - <key>OSHibernationMode</key> - <string>Se deve impedir que o SO hiberne. 0 - pode hibernar; 1 - não pode hibernar, pode desligar a tela; 2 - não pode hibernar, não pode desligar a tela.</string> <key>ObjectCacheEnabled</key> <string>Ativar o cache de objetos.</string> <key>ObjectCostHighColor</key> diff --git a/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml b/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml index cec72ebe21..137e047d97 100644 --- a/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/ru/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Настройки прокси-сервера: </text> <button label="Задать настройки" label_selected="Обзор" name="set_proxy"/> - <text name="os_hibernation_mode"> - Предотвращать гибернацию ОС, если пользователь не отошел: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Предотвращает переход ОС в сон, только если пользователь не отошел"> - <combo_box.item label="Гибернация разрешена" name="can_hibernate"/> - <combo_box.item label="Гибернация запрещена, экран можно выключать" name="cant_hibernate"/> - <combo_box.item label="Гибернация запрещена, экран нельзя выключать" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml b/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml index a3d99ac01f..a20d0e5ff1 100644 --- a/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/tr/panel_preferences_setup.xml @@ -36,12 +36,4 @@ Proxy Ayarları: </text> <button label="Proxy ayarlarını yap" label_selected="Gözat" name="set_proxy"/> - <text name="os_hibernation_mode"> - Uzakta değilse işletim sisteminin hazırda beklemesini engelle: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="Yalnızca kullanıcı uzakta değilse işletim sisteminin uykuya geçmesini engeller"> - <combo_box.item label="Hazırda bekleyebilir" name="can_hibernate"/> - <combo_box.item label="Hazırda bekleyemez, ekran kapanabilir" name="cant_hibernate"/> - <combo_box.item label="Hazırda bekleyemez, ekran kapanamaz" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml b/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml index c83a7512e0..6ef25ad96e 100644 --- a/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml +++ b/indra/newview/skins/default/xui/zh/panel_preferences_setup.xml @@ -36,12 +36,4 @@ 代理伺服器設定: </text> <button label="調整代理伺服器設定" label_selected="瀏覽" name="set_proxy"/> - <text name="os_hibernation_mode"> - 不處於離開狀態時阻止作業系統休眠: - </text> - <combo_box name="os_hibernation_combobox" tool_tip="僅在使用者不處於離開狀態時阻止作業系統睡眠"> - <combo_box.item label="可休眠" name="can_hibernate"/> - <combo_box.item label="不可休眠,可關閉螢幕" name="cant_hibernate"/> - <combo_box.item label="不可休眠,不可關閉螢幕" name="cant_hibernate_screen"/> - </combo_box> </panel> diff --git a/indra/newview/tests/llscripteditorws_test.cpp b/indra/newview/tests/llscripteditorws_test.cpp new file mode 100644 index 0000000000..4cf1bde034 --- /dev/null +++ b/indra/newview/tests/llscripteditorws_test.cpp @@ -0,0 +1,130 @@ +/** + * @file llscripteditorws_test.cpp + * @brief Headless test fixture for LLScriptEditorWSServer with fake connection + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "../llviewerprecompiledheaders.h" +#include "../llscripteditorws.h" +#include "../test/lltut.h" + +namespace +{ +class FakeScriptEditorWSConnection : public LLScriptEditorWSConnection +{ +public: + explicit FakeScriptEditorWSConnection(const LLWebsocketMgr::WSServer::ptr_t& server) + : LLScriptEditorWSConnection(server, LLWebsocketMgr::connection_h()) + { + } + + using LLJSONRPCConnection::processMessage; +}; + +class HeadlessScriptEditorWSServer : public LLScriptEditorWSServer +{ +public: + HeadlessScriptEditorWSServer() + : LLScriptEditorWSServer("script_editor_ws_test_server", 0, true) + { + } + + using LLScriptEditorWSServer::setupConnectionMethods; +}; +} + +namespace tut +{ + struct script_editor_ws_test_data + { + std::shared_ptr<HeadlessScriptEditorWSServer> mServer; + std::shared_ptr<FakeScriptEditorWSConnection> mConnection; + + script_editor_ws_test_data() + { + mServer = std::make_shared<HeadlessScriptEditorWSServer>(); + mConnection = std::make_shared<FakeScriptEditorWSConnection>(mServer); + mServer->setupConnectionMethods(mConnection); + } + }; + + typedef test_group<script_editor_ws_test_data> script_editor_ws_test_group; + typedef script_editor_ws_test_group::object script_editor_ws_test_object; + script_editor_ws_test_group script_editor_ws_tests("LLScriptEditorWSServer"); + + template<> template<> + void script_editor_ws_test_object::test<1>() + { + set_test_name("headless fixture constructs server and fake connection"); + ensure("server should be created", mServer != nullptr); + ensure("fake connection should be created", mConnection != nullptr); + } + + template<> template<> + void script_editor_ws_test_object::test<2>() + { + set_test_name("fake connection can drive JSON-RPC notification dispatch"); + + bool called = false; + mConnection->registerMethod( + "fixture.echo", + [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD + { + called = true; + ensure_equals("method should match", method, "fixture.echo"); + ensure("notification id should be undefined", id.isUndefined()); + ensure_equals("payload should be forwarded", params["value"].asString(), "ok"); + return LLSD(); + }); + + LLSD params; + params["value"] = "ok"; + LLSD notification = LLJSONRPCConnection::makeEnvelope( + LLSD(), + "fixture.echo", + params, + LLSD(), + LLSD()); + + mConnection->processMessage(notification); + ensure("fixture handler should be invoked", called); + } + + template<> template<> + void script_editor_ws_test_object::test<3>() + { + set_test_name("fixture includes default session.ping handler"); + + LLSD params; + params["timestamp"] = 123; + LLSD notification = LLJSONRPCConnection::makeEnvelope( + LLSD(), + "system.ping", + params, + LLSD(), + LLSD()); + + mConnection->processMessage(notification); + ensure("session.ping dispatch should complete in headless mode", true); + } +} diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 326b3adaaf..f451b28475 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -143,6 +143,9 @@ class ViewerManifest(LLManifest): "settings_install.xml", src="environment") + # Include the full Luau license in the packaged viewer. + with self.prefix(src=pkgdir): + self.path("LICENSES/luau.txt") with self.prefix(src_dst="character"): self.path("*.llm") diff --git a/scripts/packages-formatter.py b/scripts/packages-formatter.py index 5d31702e76..340792158e 100755 --- a/scripts/packages-formatter.py +++ b/scripts/packages-formatter.py @@ -122,6 +122,10 @@ if any(pkgs for pkgs in list(dups.values())): print("%s %s" % (args.channel, args.version)) print(viewer_copyright) +# Luau is server-side and isn't listed in the viewer's autobuild packages. +with open(os.path.join(os.path.dirname(__file__), "..", "indra", "newview", "luau.txt"), encoding="utf-8") as luau_license: + print("Luau " + " / ".join(line.strip() for line in luau_license if line.startswith("Copyright"))) +print() version = list(info['versions'].items()) version.sort() for pkg, pkg_version in version: |
