summaryrefslogtreecommitdiff
path: root/indra/llcorehttp
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2025-08-26 21:29:41 -0700
committerRider Linden <rider@lindenlab.com>2025-10-07 09:19:36 -0700
commitf31c194b8674df295edf74693a2f843ca314ca92 (patch)
tree936a587328d122e0bd12f811a2c310d23ec872b8 /indra/llcorehttp
parent7b174ab2a7c54d3e25a56530a2179848db2d46c0 (diff)
Expand the websocket implementation a bit. Added JSONRPC specialization.
Diffstat (limited to 'indra/llcorehttp')
-rw-r--r--indra/llcorehttp/CMakeLists.txt2
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp613
-rw-r--r--indra/llcorehttp/lljsonrpcws.h461
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp35
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h10
5 files changed, 1114 insertions, 7 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index d8d5c577d5..fa37d23126 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -25,6 +25,7 @@ set(llcorehttp_SOURCE_FILES
httpresponse.cpp
httpstats.cpp
llwebsocketmgr.cpp
+ lljsonrpcws.cpp
_httplibcurl.cpp
_httpopcancel.cpp
_httpoperation.cpp
@@ -54,6 +55,7 @@ set(llcorehttp_HEADER_FILES
httpresponse.h
httpstats.h
llwebsocketmgr.h
+ lljsonrpcws.h
_httpinternal.h
_httplibcurl.h
_httpopcancel.h
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp
new file mode 100644
index 0000000000..5de595c595
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.cpp
@@ -0,0 +1,613 @@
+/**
+ * @file lljsonrpcws.cpp
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, 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 "llerror.h"
+#include "llsdjson.h"
+#include "lldate.h"
+
+#include <boost/json.hpp>
+
+//========================================================================
+// LLJSONRPCConnection Implementation
+//========================================================================
+
+void LLJSONRPCConnection::onOpen()
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL;
+}
+
+void LLJSONRPCConnection::onClose()
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC connection closed, clearing "
+ << mPendingRequests.size() << " pending requests" << LL_ENDL;
+
+ // Cancel all pending requests
+ for (auto& [id, callback] : mPendingRequests)
+ {
+ if (callback)
+ {
+ LLSD error;
+ error["code"] = RPCError::CONNECTION_CLOSED; // Use named constant instead of magic number
+ error["message"] = "Connection closed";
+ callback(LLSD(), error);
+ }
+ }
+ mPendingRequests.clear();
+}
+
+void LLJSONRPCConnection::onMessage(const std::string& message)
+{
+ LL_DEBUGS("JSONRPC") << "Received JSON-RPC message: " << message << LL_ENDL;
+
+ try
+ {
+ // Parse JSON message
+ boost::system::error_code ec;
+ boost::json::value json_value = boost::json::parse(message, ec);
+
+ if (ec.failed())
+ {
+ LL_WARNS("JSONRPC") << "Failed to parse JSON: " << ec.message() << LL_ENDL;
+ sendError(LLSD(), ParseError(ec.message()));
+ return;
+ }
+
+ // Convert to LLSD
+ LLSD message_obj = LlsdFromJson(json_value);
+
+ // Handle batch vs single message
+ if (message_obj.isArray())
+ {
+ // Batch request
+ if (message_obj.size() == 0)
+ {
+ sendError(LLSD(), InvalidRequest("Empty batch"));
+ return;
+ }
+
+ // Process each message in the batch
+ for (S32 i = 0; i < message_obj.size(); ++i)
+ {
+ processMessage(message_obj[i]);
+ }
+ }
+ else
+ {
+ // Single message
+ processMessage(message_obj);
+ }
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL;
+ sendError(LLSD(), InternalError(e.what()));
+ }
+}
+
+void LLJSONRPCConnection::processMessage(const LLSD& message_obj)
+{
+ try
+ {
+ // Determine if this is a request, notification, or response
+ if (message_obj.has("method"))
+ {
+ // This is a request or notification
+ validateMessage(message_obj, true);
+ processRequest(message_obj);
+ }
+ else if (message_obj.has("result") || message_obj.has("error"))
+ {
+ // This is a response
+ validateMessage(message_obj, false);
+ processResponse(message_obj);
+ }
+ else
+ {
+ throw InvalidRequest("Message must contain 'method' or 'result'/'error'");
+ }
+ }
+ catch (const RPCError& e)
+ {
+ LLSD id = message_obj.has("id") ? message_obj["id"] : LLSD();
+ sendError(id, e);
+ }
+}
+
+void LLJSONRPCConnection::processRequest(const LLSD& request)
+{
+ std::string method = request["method"].asString();
+ LLSD params = request.has("params") ? request["params"] : LLSD();
+ LLSD id = request.has("id") ? request["id"] : LLSD();
+ bool is_notification = !request.has("id");
+
+ LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request")
+ << " for method: " << method << LL_ENDL;
+
+ // Find method handler
+ auto it = mMethodHandlers.find(method);
+ if (it == mMethodHandlers.end())
+ {
+ if (!is_notification)
+ {
+ sendError(id, MethodNotFound(method));
+ }
+ return;
+ }
+
+ try
+ {
+ // Call the method handler with method name, ID, and parameters
+ LLSD result = it->second(method, id, params);
+
+ // Send response (only for requests, not notifications)
+ if (!is_notification)
+ {
+ sendResponse(id, result);
+ }
+ }
+ catch (const RPCError& e)
+ {
+ if (!is_notification)
+ {
+ sendError(id, e);
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Error in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+ catch (const std::exception& e)
+ {
+ if (!is_notification)
+ {
+ sendError(id, InternalError(e.what()));
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Exception in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+}
+
+void LLJSONRPCConnection::processResponse(const LLSD& response)
+{
+ if (!response.has("id"))
+ {
+ LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL;
+ return;
+ }
+
+ std::string id = response["id"].asString();
+ auto it = mPendingRequests.find(id);
+ if (it == mPendingRequests.end())
+ {
+ LL_WARNS("JSONRPC") << "Received response for unknown request id: " << id << LL_ENDL;
+ return;
+ }
+
+ ResponseCallback callback = it->second;
+ mPendingRequests.erase(it);
+
+ if (callback)
+ {
+ LLSD result = response.has("result") ? response["result"] : LLSD();
+ LLSD error = response.has("error") ? response["error"] : LLSD();
+
+ callback(result, error);
+ }
+}
+
+void LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request)
+{
+ // Check JSON-RPC version
+ if (!message.has("jsonrpc") || message["jsonrpc"].asString() != "2.0")
+ {
+ throw InvalidRequest("Missing or invalid jsonrpc version");
+ }
+
+ if (is_request)
+ {
+ // Request/notification validation
+ if (!message.has("method"))
+ {
+ throw InvalidRequest("Missing method field");
+ }
+
+ if (!message["method"].isString())
+ {
+ throw InvalidRequest("Method must be a string");
+ }
+
+ // Params are optional but must be array or object if present
+ if (message.has("params"))
+ {
+ if (!message["params"].isArray() && !message["params"].isMap())
+ {
+ throw InvalidParams("Params must be array or object");
+ }
+ }
+ }
+ else
+ {
+ // Response validation
+ if (!message.has("id"))
+ {
+ throw InvalidRequest("Response missing id field");
+ }
+
+ // Must have either result or error, but not both
+ bool has_result = message.has("result");
+ bool has_error = message.has("error");
+
+ if (!has_result && !has_error)
+ {
+ throw InvalidRequest("Response must have result or error");
+ }
+
+ if (has_result && has_error)
+ {
+ throw InvalidRequest("Response cannot have both result and error");
+ }
+
+ // Error must be an object with code and message
+ if (has_error)
+ {
+ LLSD error = message["error"];
+ if (!error.isMap())
+ {
+ throw InvalidRequest("Error must be an object");
+ }
+ if (!error.has("code") || !error.has("message"))
+ {
+ throw InvalidRequest("Error must have code and message");
+ }
+ }
+ }
+}
+
+LLSD LLJSONRPCConnection::generateId()
+{
+ // Server-wide atomic counter for efficient unique ID generation
+ // Start from 1000 to avoid conflicts with any manual test IDs
+ static std::atomic<U64> sRequestIdCounter{1000};
+
+ // Generate server-unique sequential ID
+ U64 id = sRequestIdCounter.fetch_add(1);
+ return LLSD(llformat("rpc_%llu", id));
+}
+
+void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler handler)
+{
+ mMethodHandlers[method] = handler;
+ LL_INFOS("JSONRPC") << "Registered method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::unregisterMethod(const std::string& method)
+{
+ mMethodHandlers.erase(method);
+ LL_INFOS("JSONRPC") << "Unregistered method: " << method << LL_ENDL;
+}
+
+LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback)
+{
+ LLSD request;
+ request["jsonrpc"] = "2.0";
+ request["method"] = method;
+
+ if (!params.isUndefined())
+ {
+ request["params"] = params;
+ }
+
+ LLSD id = generateId();
+ request["id"] = id;
+
+ // Store callback if provided
+ if (callback)
+ {
+ mPendingRequests[id.asString()] = callback;
+ }
+
+ // Send the request
+ if (!sendMessage(LlsdToJson(request)))
+ {
+ // Remove from pending if send failed
+ if (callback)
+ {
+ mPendingRequests.erase(id.asString());
+ }
+ throw InternalError("Failed to send request");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent request: " << method << " with id: " << id.asString() << LL_ENDL;
+ return id;
+}
+
+void LLJSONRPCConnection::notify(const std::string& method, const LLSD& params)
+{
+ LLSD notification;
+ notification["jsonrpc"] = "2.0";
+ notification["method"] = method;
+
+ if (!params.isUndefined())
+ {
+ notification["params"] = params;
+ }
+
+ // Notifications don't have an id
+
+ if (!sendMessage(LlsdToJson(notification)))
+ {
+ throw InternalError("Failed to send notification");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent notification: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result)
+{
+ LLSD response;
+ response["jsonrpc"] = "2.0";
+ response["result"] = result;
+ response["id"] = id;
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send response for id: " << id.asString() << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL;
+ }
+}
+
+void LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error)
+{
+ LLSD response;
+ response["jsonrpc"] = "2.0";
+
+ LLSD error_obj;
+ error_obj["code"] = error.getCode();
+ error_obj["message"] = error.what();
+
+ if (!error.getData().isUndefined())
+ {
+ error_obj["data"] = error.getData();
+ }
+
+ response["error"] = error_obj;
+ response["id"] = id.isUndefined() ? LLSD() : id; // null for parse errors
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send error response" << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL;
+ }
+}
+
+void LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback)
+{
+ if (!batch.isArray() || batch.size() == 0)
+ {
+ throw InvalidRequest("Batch must be non-empty array");
+ }
+
+ // For batch requests with callbacks, we need to track multiple responses
+ // This is complex as we need to correlate all responses before calling callback
+ // For now, we'll send the batch but won't support batch response callbacks
+ if (callback)
+ {
+ LL_WARNS("JSONRPC") << "Batch response callbacks not yet implemented" << LL_ENDL;
+ }
+
+ if (!sendMessage(LlsdToJson(batch)))
+ {
+ throw InternalError("Failed to send batch");
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent batch with " << batch.size() << " messages" << LL_ENDL;
+}
+
+//========================================================================
+// LLJSONRPCServer Implementation
+//========================================================================
+
+LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_only)
+ : LLWebsocketMgr::WSServer(name, port, local_only), mServerName(name)
+{
+ LL_INFOS("JSONRPC") << "Created JSON-RPC server: " << name
+ << " on port " << port << LL_ENDL;
+
+ // Register standard JSON-RPC methods
+ registerGlobalMethod("system.listMethods", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ return getMethodList();
+ });
+
+ registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ return getServerStats();
+ });
+
+ registerGlobalMethod("system.ping", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ LLSD result;
+ result["pong"] = LLDate::now().asString();
+ result["params"] = params;
+ return result;
+ });
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
+{
+ auto connection = std::make_shared<LLJSONRPCConnection>(server, handle);
+ setupConnectionMethods(connection);
+ return connection;
+}
+
+void LLJSONRPCServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client connected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client disconnected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection)
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ // Register all global methods on the new connection
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ connection->registerMethod(method, handler);
+ }
+}
+
+void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods[method] = handler;
+ }
+
+ // Apply to all existing connections - we need to iterate through connections
+ // Since mConnections is private, we need to use broadcastMessage or find another approach
+ // For now, we'll only apply to new connections
+
+ LL_INFOS("JSONRPC") << "Registered global method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCServer::unregisterGlobalMethod(const std::string& method)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods.erase(method);
+ }
+
+ // For existing connections, we would need access to them
+ // This is a limitation of the current design - methods added after connection
+ // establishment won't be retroactively applied
+
+ LL_INFOS("JSONRPC") << "Unregistered global method: " << method << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getMethodList() const
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ LLSD methods = LLSD::emptyArray();
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ methods.append(method);
+ }
+
+ return methods;
+}
+
+void LLJSONRPCServer::broadcastNotification(const std::string& method, const LLSD& params)
+{
+ // Use custom broadcast logic since we need to call notify() on each JSON-RPC connection
+ // We can't use the base broadcastMessage() because we need structured JSON-RPC messages
+
+ // Create the notification message
+ LLSD notification;
+ notification["jsonrpc"] = "2.0";
+ notification["method"] = method;
+ if (!params.isUndefined())
+ {
+ notification["params"] = params;
+ }
+
+ // Use the base class broadcast functionality
+ broadcastMessage(boost::json::serialize(LlsdToJson(notification)));
+
+ mTotalNotificationsSent += getConnectionCount();
+ LL_DEBUGS("JSONRPC") << "Broadcast notification: " << method
+ << " to " << getConnectionCount() << " clients" << LL_ENDL;
+}
+
+void LLJSONRPCServer::broadcastCall(const std::string& method, const LLSD& params,
+ BatchResponseCallback callback)
+{
+ if (callback)
+ {
+ LL_WARNS("JSONRPC") << "Broadcast call response callbacks not yet implemented" << LL_ENDL;
+ }
+
+ // Create the request message with a server-unique ID
+ LLSD request;
+ request["jsonrpc"] = "2.0";
+ request["method"] = method;
+
+ // Use the same ID generation as connections for consistency
+ static std::atomic<U64> sBroadcastIdCounter{10000000}; // Start at 10M to clearly distinguish from regular requests
+ U64 id = sBroadcastIdCounter.fetch_add(1);
+ request["id"] = LLSD(llformat("broadcast_%llu", id));
+
+ if (!params.isUndefined())
+ {
+ request["params"] = params;
+ }
+
+ // Use the base class broadcast functionality
+ broadcastMessage(boost::json::serialize(LlsdToJson(request)));
+
+ LL_DEBUGS("JSONRPC") << "Broadcast call: " << method
+ << " to " << getConnectionCount() << " clients" << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getServerStats() const
+{
+ LLSD stats;
+ stats["server_name"] = mServerName;
+ stats["connection_count"] = static_cast<S32>(getConnectionCount());
+ stats["is_running"] = isRunning();
+
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ stats["global_method_count"] = static_cast<S32>(mGlobalMethods.size());
+ }
+
+ stats["total_requests_handled"] = static_cast<LLSD::Integer>(mTotalRequestsHandled.load());
+ stats["total_notifications_sent"] = static_cast<LLSD::Integer>(mTotalNotificationsSent.load());
+ stats["uptime"] = LLDate::now().asString();
+
+ return stats;
+}
diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h
new file mode 100644
index 0000000000..54578c0e93
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.h
@@ -0,0 +1,461 @@
+/**
+ * @file lljsonrpcws.h
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, 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$
+ */
+
+#pragma once
+
+#include "llwebsocketmgr.h"
+#include "llsd.h"
+#include "lluuid.h"
+
+#include <functional>
+#include <unordered_map>
+#include <memory>
+
+/**
+ * @class LLJSONRPCConnection
+ * @brief JSON-RPC 2.0 WebSocket connection implementation
+ *
+ * This class implements the JSON-RPC 2.0 protocol over WebSocket connections.
+ * It handles request/response patterns, notifications, method registration,
+ * and error handling according to the JSON-RPC 2.0 specification.
+ *
+ * ## JSON-RPC 2.0 Protocol Features
+ *
+ * - **Requests**: Method calls that expect a response
+ * - **Notifications**: Method calls that do not expect a response
+ * - **Batch Operations**: Multiple requests/notifications in a single message
+ * - **Error Handling**: Standardized error codes and messages
+ * - **ID Correlation**: Request/response correlation using unique identifiers
+ *
+ * ## Method Handler Registration
+ *
+ * Methods use an enhanced handler signature that provides method name and request ID context:
+ *
+ * @code
+ * connection->registerMethod("echo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Method " << method << " called with ID " << id.asString() << LL_ENDL;
+ * return params; // Echo back the parameters
+ * });
+ *
+ * connection->registerMethod("add", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * if (params.isArray() && params.size() >= 2) {
+ * LL_INFOS("JSONRPC") << "Adding numbers via " << method << LL_ENDL;
+ * return params[0].asReal() + params[1].asReal();
+ * }
+ * throw LLJSONRPCConnection::InvalidParams("Expected array with 2 numbers");
+ * });
+ * @endcode
+ *
+ * The enhanced signature enables:
+ * - Method context awareness for shared handlers
+ * - Request correlation and distributed tracing
+ * - Distinction between notifications (id undefined) and requests
+ * - Enhanced logging and error reporting with context
+ *
+ * ## Making RPC Calls
+ *
+ * @code
+ * // Asynchronous request with callback
+ * LLSD params;
+ * params.append(5);
+ * params.append(3);
+ * connection->call("add", params, [](const LLSD& result, const LLSD& error) {
+ * if (error.isUndefined()) {
+ * LL_INFOS() << "Result: " << result.asReal() << LL_ENDL;
+ * } else {
+ * LL_WARNS() << "Error: " << error["message"].asString() << LL_ENDL;
+ * }
+ * });
+ *
+ * // Fire-and-forget notification
+ * connection->notify("log", LLSD("Server started"));
+ * @endcode
+ */
+class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCConnection>;
+
+ /// Method handler function signature
+ /// @param method The method name that was called
+ /// @param id The request ID (undefined for notifications)
+ /// @param params The parameters passed to the method
+ /// @return The result to return to the caller
+ /// @throw RPCError-derived exceptions for error responses
+ using MethodHandler = std::function<LLSD(const std::string& method, const LLSD& id, const LLSD& params)>;
+
+ /// Response callback function signature
+ /// @param result The result from a successful call (undefined if error occurred)
+ /// @param error The error object if call failed (undefined if successful)
+ using ResponseCallback = std::function<void(const LLSD& result, const LLSD& error)>;
+
+ /**
+ * @brief JSON-RPC error base class
+ */
+ class RPCError : public std::runtime_error
+ {
+ public:
+ // JSON-RPC 2.0 Standard Error Codes
+ static constexpr S32 PARSE_ERROR = -32700; ///< Invalid JSON was received by the server
+ static constexpr S32 INVALID_REQUEST = -32600; ///< The JSON sent is not a valid Request object
+ static constexpr S32 METHOD_NOT_FOUND = -32601; ///< The method does not exist / is not available
+ static constexpr S32 INVALID_PARAMS = -32602; ///< Invalid method parameter(s)
+ static constexpr S32 INTERNAL_ERROR = -32603; ///< Internal JSON-RPC error
+
+ // Server Error Range (-32000 to -32099)
+ static constexpr S32 SERVER_ERROR_MIN = -32099; ///< Server error range minimum
+ static constexpr S32 SERVER_ERROR_MAX = -32000; ///< Server error range maximum
+
+ // Common server-specific errors
+ static constexpr S32 CONNECTION_CLOSED = -32000; ///< Connection closed unexpectedly
+ static constexpr S32 REQUEST_TIMEOUT = -32001; ///< Request timed out
+ static constexpr S32 UNAUTHORIZED = -32002; ///< Authentication required
+ static constexpr S32 FORBIDDEN = -32003; ///< Access denied
+ static constexpr S32 RATE_LIMITED = -32004; ///< Too many requests
+ static constexpr S32 SERVICE_UNAVAILABLE = -32005; ///< Service temporarily unavailable
+ static constexpr S32 MESSAGE_TOO_LARGE = -32006; ///< Message exceeds maximum size
+ static constexpr S32 INVALID_SESSION = -32007; ///< Session expired or invalid
+
+ RPCError(S32 code, const std::string& message, const LLSD& data = LLSD())
+ : std::runtime_error(message), mCode(code), mData(data) {}
+
+ S32 getCode() const { return mCode; }
+ const LLSD& getData() const { return mData; }
+
+ protected:
+ S32 mCode;
+ LLSD mData;
+ };
+
+ /// Standard JSON-RPC error classes using named constants
+ class ParseError : public RPCError {
+ public:
+ ParseError(const std::string& details = "")
+ : RPCError(PARSE_ERROR, "Parse error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InvalidRequest : public RPCError {
+ public:
+ InvalidRequest(const std::string& details = "")
+ : RPCError(INVALID_REQUEST, "Invalid Request" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class MethodNotFound : public RPCError {
+ public:
+ MethodNotFound(const std::string& method = "")
+ : RPCError(METHOD_NOT_FOUND, "Method not found" + (method.empty() ? "" : ": " + method)) {}
+ };
+
+ class InvalidParams : public RPCError {
+ public:
+ InvalidParams(const std::string& details = "")
+ : RPCError(INVALID_PARAMS, "Invalid params" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InternalError : public RPCError {
+ public:
+ InternalError(const std::string& details = "")
+ : RPCError(INTERNAL_ERROR, "Internal error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ /// Server-specific errors (in the -32000 to -32099 range)
+ class ConnectionClosedError : public RPCError {
+ public:
+ ConnectionClosedError(const std::string& details = "Connection closed")
+ : RPCError(CONNECTION_CLOSED, details) {}
+ };
+
+ class RequestTimeoutError : public RPCError {
+ public:
+ RequestTimeoutError(const std::string& details = "Request timed out")
+ : RPCError(REQUEST_TIMEOUT, details) {}
+ };
+
+ class UnauthorizedError : public RPCError {
+ public:
+ UnauthorizedError(const std::string& details = "Authentication required")
+ : RPCError(UNAUTHORIZED, details) {}
+ };
+
+ class ForbiddenError : public RPCError {
+ public:
+ ForbiddenError(const std::string& details = "Access denied")
+ : RPCError(FORBIDDEN, details) {}
+ };
+
+ class RateLimitedError : public RPCError {
+ public:
+ RateLimitedError(const std::string& details = "Too many requests")
+ : RPCError(RATE_LIMITED, details) {}
+ };
+
+ class ServiceUnavailableError : public RPCError {
+ public:
+ ServiceUnavailableError(const std::string& details = "Service temporarily unavailable")
+ : RPCError(SERVICE_UNAVAILABLE, details) {}
+ };
+
+ class MessageTooLargeError : public RPCError {
+ public:
+ MessageTooLargeError(const std::string& details = "Message exceeds maximum size")
+ : RPCError(MESSAGE_TOO_LARGE, details) {}
+ };
+
+ class InvalidSessionError : public RPCError {
+ public:
+ InvalidSessionError(const std::string& details = "Session expired or invalid")
+ : RPCError(INVALID_SESSION, details) {}
+ };
+
+ LLJSONRPCConnection(const LLWebsocketMgr::WSServer::ptr_t server,
+ const LLWebsocketMgr::connection_h& handle)
+ : LLWebsocketMgr::WSConnection(server, handle) {}
+
+ virtual ~LLJSONRPCConnection() = default;
+
+ // WebSocket connection lifecycle
+ void onOpen() override;
+ void onClose() override;
+ void onMessage(const std::string& message) override;
+
+ /**
+ * @brief Register a method handler
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ */
+ void registerMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a method handler
+ * @param method The method name to unregister
+ */
+ void unregisterMethod(const std::string& method);
+
+ /**
+ * @brief Make an asynchronous JSON-RPC call
+ * @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
+ */
+ LLSD call(const std::string& method, const LLSD& params = LLSD(),
+ ResponseCallback callback = nullptr);
+
+ /**
+ * @brief Send a JSON-RPC notification (no response expected)
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ void notify(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Send a successful response to a request
+ * @param id The request ID from the original request
+ * @param result The result to return
+ */
+ void sendResponse(const LLSD& id, const LLSD& result);
+
+ /**
+ * @brief Send an error response to a request
+ * @param id The request ID from the original request (can be null)
+ * @param error The RPCError to send
+ */
+ void sendError(const LLSD& id, const RPCError& error);
+
+ /**
+ * @brief Send a batch of requests/notifications
+ * @param batch Array of request/notification objects
+ * @param callback Callback for batch response (optional)
+ */
+ void sendBatch(const LLSD& batch, ResponseCallback callback = nullptr);
+
+protected:
+ /**
+ * @brief Process a single JSON-RPC message
+ * @param message_obj The parsed JSON message
+ */
+ void processMessage(const LLSD& message_obj);
+
+ /**
+ * @brief Process a JSON-RPC request
+ * @param request The request object
+ */
+ void processRequest(const LLSD& request);
+
+ /**
+ * @brief Process a JSON-RPC response
+ * @param response The response object
+ */
+ void processResponse(const LLSD& response);
+
+ /**
+ * @brief Validate a JSON-RPC message structure
+ * @param message The message to validate
+ * @param is_request True if validating a request, false for response
+ * @throw InvalidRequest if validation fails
+ */
+ void validateMessage(const LLSD& message, bool is_request = true);
+
+ /**
+ * @brief Generate the next unique request ID
+ * @return A server-unique request ID
+ *
+ * Generates a server-wide unique identifier using an atomic counter.
+ * This ensures request IDs are unique across all connections within the
+ * server instance, providing efficient ID generation with guaranteed uniqueness.
+ *
+ * IDs follow the format "rpc_{counter}" where counter is a monotonically
+ * increasing 64-bit value starting from 1. This approach provides:
+ * - Guaranteed uniqueness within server scope
+ * - High performance (atomic increment operation)
+ * - Predictable, sequential ordering for debugging
+ * - Thread-safe generation across multiple connections
+ */
+ LLSD generateId();
+
+private:
+ std::unordered_map<std::string, MethodHandler> mMethodHandlers;
+ std::unordered_map<std::string, ResponseCallback> mPendingRequests;
+};
+
+/**
+ * @class LLJSONRPCServer
+ * @brief JSON-RPC 2.0 WebSocket server implementation
+ *
+ * This server extends the basic WebSocket server to provide JSON-RPC 2.0
+ * protocol support. It manages JSON-RPC connections and provides server-wide
+ * method registration and broadcasting capabilities.
+ *
+ * ## Server-Wide Method Registration
+ *
+ * Methods can be registered at the server level and will be available
+ * on all connections:
+ *
+ * @code
+ * auto server = std::make_shared<LLJSONRPCServer>("rpc_server", 8080);
+ *
+ * server->registerGlobalMethod("getServerInfo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Server info requested via " << method << LL_ENDL;
+ * LLSD info;
+ * info["name"] = "My RPC Server";
+ * info["version"] = "1.0.0";
+ * info["uptime"] = LLDate::now().secondsSinceEpoch();
+ * return info;
+ * });
+ *
+ * server->registerGlobalMethod("listMethods", [server](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * return server->getMethodList();
+ * });
+ * @endcode
+ *
+ * ## Broadcasting and Multi-client Operations
+ *
+ * @code
+ * // Broadcast notification to all connected clients
+ * server->broadcastNotification("serverAlert", LLSD("Server will restart in 5 minutes"));
+ *
+ * // Call a method on all clients and collect responses
+ * server->broadcastCall("getClientStatus", LLSD(), [](const LLSD& responses) {
+ * for (const auto& response : llsd::inArray(responses)) {
+ * LL_INFOS() << "Client status: " << response << LL_ENDL;
+ * }
+ * });
+ * @endcode
+ */
+class LLJSONRPCServer : public LLWebsocketMgr::WSServer
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCServer>;
+ using MethodHandler = LLJSONRPCConnection::MethodHandler;
+ using ResponseCallback = LLJSONRPCConnection::ResponseCallback;
+ using BatchResponseCallback = std::function<void(const LLSD& responses)>;
+
+ LLJSONRPCServer(const std::string& name, U16 port, bool local_only = true);
+ virtual ~LLJSONRPCServer() = default;
+
+ // Server lifecycle callbacks
+ void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+ void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+
+ /**
+ * @brief Register a global method available on all connections
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ */
+ void registerGlobalMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a global method
+ * @param method The method name to unregister
+ */
+ void unregisterGlobalMethod(const std::string& method);
+
+ /**
+ * @brief Get list of registered global methods
+ * @return Array of method names
+ */
+ LLSD getMethodList() const;
+
+ /**
+ * @brief Broadcast a notification to all connected clients
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ void broadcastNotification(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Call a method on all connected clients
+ * @param method The method name
+ * @param params The parameters to pass
+ * @param callback Callback to receive aggregated responses
+ */
+ void broadcastCall(const std::string& method, const LLSD& params = LLSD(),
+ BatchResponseCallback callback = nullptr);
+
+ /**
+ * @brief Get server statistics
+ * @return Statistics object with connection count, method count, etc.
+ */
+ LLSD getServerStats() const;
+
+protected:
+ LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle) override;
+
+ /**
+ * @brief Apply global method handlers to a new connection
+ * @param connection The connection to configure
+ */
+ virtual void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection);
+
+private:
+ std::unordered_map<std::string, MethodHandler> mGlobalMethods;
+ mutable LLMutex mGlobalMethodsMutex;
+
+ std::string mServerName; // Store server name for stats
+ std::atomic<U64> mTotalRequestsHandled{0};
+ std::atomic<U64> mTotalNotificationsSent{0};
+};
diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp
index 3e4262b49b..d44d5d877d 100644
--- a/indra/llcorehttp/llwebsocketmgr.cpp
+++ b/indra/llcorehttp/llwebsocketmgr.cpp
@@ -63,6 +63,32 @@ void LLWebsocketMgr::cleanupSingleton()
stopAllServers();
}
+void LLWebsocketMgr::update()
+{
+ std::vector<WSServer::ptr_t> stops;
+
+ for (auto &[name, server] : mServers)
+ {
+ if (server && server->isRunning())
+ {
+ if (!server->update())
+ {
+ stops.push_back(server);
+ }
+ }
+ }
+
+ for (const auto& server : stops)
+ {
+ if (server)
+ {
+ LL_DEBUGS("WebSocket") << "Stopping server: " << server->mServerName << LL_ENDL;
+ removeServer(server->mServerName);
+ }
+ }
+}
+
+
LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const
{
auto it = mServers.find(std::string(name));
@@ -192,6 +218,7 @@ struct Server_impl
mPort(port),
mLocalOnly(local_only)
{
+
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); });
@@ -647,12 +674,12 @@ void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const s
//------------------------------------------------------------------------
bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) const
{
- if (!mServer)
+ if (mOwningServer.expired())
{
LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL;
return false;
}
- return mServer->sendMessageTo(mConnectionHandle, message);
+ return mOwningServer.lock()->sendMessageTo(mConnectionHandle, message);
}
bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const
@@ -668,7 +695,7 @@ bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const
void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason)
{
- if (!mServer)
+ if (mOwningServer.expired())
{
LL_WARNS("WebSocket") << "Attempted to close connection with null server reference" << LL_ENDL;
return;
@@ -677,7 +704,7 @@ void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string&
LL_INFOS("WebSocket") << "WSConnection closing connection with code " << code
<< " and reason: " << (reason.empty() ? "(no reason)" : reason) << LL_ENDL;
- if (!mServer->closeConnection(mConnectionHandle, code, reason))
+ if (!mOwningServer.lock()->closeConnection(mConnectionHandle, code, reason))
{
LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL;
}
diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h
index 08f6f22dbc..570c8ad6cd 100644
--- a/indra/llcorehttp/llwebsocketmgr.h
+++ b/indra/llcorehttp/llwebsocketmgr.h
@@ -78,7 +78,7 @@ public:
*/
WSConnection(const std::shared_ptr<WSServer> &server, const connection_h& handle):
mConnectionHandle(handle),
- mServer(server)
+ mOwningServer(server)
{}
virtual ~WSConnection() = default;
@@ -188,9 +188,9 @@ public:
*/
void closeConnection(U16 code = 1000, const std::string& reason = std::string());
- private:
+ protected:
connection_h mConnectionHandle;
- std::shared_ptr<WSServer> mServer; // Back-reference to the server this connection belongs to
+ std::weak_ptr<WSServer> mOwningServer; // Back-reference to the server this connection belongs to
};
/**
@@ -271,6 +271,8 @@ public:
}
void broadcastMessage(const std::string& message);
+ virtual bool update() { return true; }
+
protected:
virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle);
@@ -320,6 +322,8 @@ public:
bool startServer(const std::string &name) const;
void stopServer(const std::string &name) const;
+ void update();
+
protected:
void initSingleton() override;
void cleanupSingleton() override;