summaryrefslogtreecommitdiff
path: root/indra/llcorehttp
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2025-08-22 16:16:23 -0700
committerRider Linden <rider@lindenlab.com>2025-10-07 09:19:30 -0700
commitdcf6d8c5024c831d54f0f5f140551e65748ed513 (patch)
tree00751ad690d7b8b0ab74489e15cc7113ed284b82 /indra/llcorehttp
parent0b7bc35131ce4a526c1e696904c4e8332e1b0fe9 (diff)
Initial simple websockets server with managing singleton.
Diffstat (limited to 'indra/llcorehttp')
-rw-r--r--indra/llcorehttp/CMakeLists.txt6
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp411
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h207
3 files changed, 623 insertions, 1 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index 05b788a433..d8d5c577d5 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -12,6 +12,7 @@ include(LLAddBuildTest)
include(LLCommon)
include(Tut)
include(bugsplat)
+include(websocketpp)
set(llcorehttp_SOURCE_FILES
bufferarray.cpp
@@ -23,6 +24,7 @@ set(llcorehttp_SOURCE_FILES
httprequest.cpp
httpresponse.cpp
httpstats.cpp
+ llwebsocketmgr.cpp
_httplibcurl.cpp
_httpopcancel.cpp
_httpoperation.cpp
@@ -51,6 +53,7 @@ set(llcorehttp_HEADER_FILES
httprequest.h
httpresponse.h
httpstats.h
+ llwebsocketmgr.h
_httpinternal.h
_httplibcurl.h
_httpopcancel.h
@@ -85,6 +88,7 @@ target_link_libraries(
ll::libcurl
ll::openssl
ll::nghttp2
+ ll::websocketpp
)
target_include_directories( llcorehttp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
# llmessage depends on llcorehttp, yet llcorehttp also depends on llmessage (at least for includes).
@@ -131,7 +135,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
${PYTHON_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llcorehttp_peer.py"
)
-
+
#
# Example Programs
#
diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp
new file mode 100644
index 0000000000..0c61e0f68d
--- /dev/null
+++ b/indra/llcorehttp/llwebsocketmgr.cpp
@@ -0,0 +1,411 @@
+/**
+ * @file llwebsocketmgr.cpp
+ * @brief WebSocket manager singleton 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 "llwebsocketmgr.h"
+#include "llerror.h"
+#include "llsdserialize.h"
+#include "llhost.h"
+
+#include <websocketpp/config/boost_config.hpp>
+#include <websocketpp/config/asio_no_tls.hpp>
+#include <websocketpp/client.hpp>
+#include <websocketpp/server.hpp>
+
+//------------------------------------------------------------------------
+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>;
+}
+
+//------------------------------------------------------------------------
+
+//------------------------------------------------------------------------
+// LLWebsocketMgr Implementation
+//
+
+void LLWebsocketMgr::initSingleton()
+{ }
+
+void LLWebsocketMgr::cleanupSingleton()
+{
+ stopAllServers();
+}
+
+LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const
+{
+ auto it = mServers.find(std::string(name));
+ if (it != mServers.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+bool LLWebsocketMgr::addServer(const LLWebsocketMgr::WSServer::ptr_t& server)
+{
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "Attempted to add a null server" << LL_ENDL;
+ return false;
+ }
+
+ auto it = mServers.find(server->mServerName);
+ if (it != mServers.end())
+ {
+ LL_WARNS("WebSocket") << "Server with name " << server->mServerName << " already exists" << LL_ENDL;
+ return false;
+ }
+ mServers[server->mServerName] = server;
+ LL_INFOS("WebSocket") << "Added WebSocket server: " << server->mServerName << LL_ENDL;
+ return true;
+}
+
+bool LLWebsocketMgr::removeServer(const std::string& name)
+{
+ auto it = mServers.find(name);
+ if (it == mServers.end())
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to remove" << LL_ENDL;
+ return false;
+ }
+ if (it->second && it->second->isRunning())
+ it->second->stop();
+ mServers.erase(it);
+ LL_INFOS("WebSocket") << "Removed WebSocket server: " << name << LL_ENDL;
+ return true;
+}
+
+
+bool LLWebsocketMgr::startServer(const std::string &name) const
+{
+ LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name);
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to start" << LL_ENDL;
+ return false;
+ }
+ if (server->isRunning())
+ {
+ LL_WARNS("WebSocket") << "Server " << name << " is already running" << LL_ENDL;
+ return false;
+ }
+ return server->start();
+}
+
+void LLWebsocketMgr::stopServer(const std::string& name) const
+{
+ LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name);
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to stop" << LL_ENDL;
+ return;
+ }
+ if (!server->isRunning())
+ {
+ LL_WARNS("WebSocket") << "Server " << name << " is not running" << LL_ENDL;
+ return;
+ }
+ server->stop();
+}
+
+void LLWebsocketMgr::stopAllServers()
+{
+ for (auto &[name, server] : mServers)
+ {
+ if (server && server->isRunning())
+ {
+ LL_INFOS("WebSocket") << "Stopping server: " << name << LL_ENDL;
+ server->stop();
+ }
+ }
+
+ mServers.clear();
+}
+
+
+//------------------------------------------------------------------------
+struct Server_impl
+{
+ Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) :
+ mOwner(owner),
+ 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); });
+ }
+
+ ~Server_impl() = default;
+
+ void init()
+ {
+ mServer.init_asio();
+ if (mLocalOnly)
+ {
+ std::stringstream port_str;
+ port_str << mPort;
+ mServer.listen("127.0.0.1", port_str.str());
+ }
+ else
+ {
+ mServer.listen(mPort);
+ }
+ }
+
+ bool start()
+ {
+ if (!mServer.stopped())
+ {
+ LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL;
+ return false;
+ }
+ try
+ {
+ mServer.start_accept();
+ mServer.run();
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server encountered an error: " << e.what() << LL_ENDL;
+ return false;
+ }
+ return true;
+ }
+
+ void stop()
+ {
+ if (mServer.stopped())
+ {
+ return;
+ }
+ try
+ {
+ mServer.stop_listening();
+ mServer.stop();
+ }
+ catch (const std::exception&)
+ {
+ LL_WARNS("WebSocket") << "Error stopping WebSocket server" << LL_ENDL;
+ }
+ }
+
+ void onOpen(websocketpp::connection_hdl hdl) const
+ {
+ assert(mOwner); // mOwner should never be null. If it is, something is very wrong!
+
+ mOwner->handleOpenConnection(hdl);
+ }
+
+ void onClose(websocketpp::connection_hdl hdl) const
+ {
+ assert(mOwner); // mOwner should never be null
+ mOwner->handleCloseConnection(hdl);
+ }
+
+ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const
+ {
+ assert(mOwner); // mOwner should never be null
+ LLWebsocketMgr::WSConnection::ptr_t connection = mOwner->getConnection(hdl);
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << "Received message for unknown connection" << LL_ENDL;
+ return;
+ }
+
+ // TODO: check the FIN bit and handle fragmented messages if needed
+ // TODO: check terminal and close codes and handle connection closure if needed
+ // TODO: handle binary messages if needed
+ mOwner->handleMessage(hdl, msg->get_payload());
+ }
+
+ //-------------------------------------------
+ Server_t mServer;
+ LLWebsocketMgr::WSServer* mOwner{ nullptr }; // Back-reference to the owning server, note that this pointer can never be null
+ U16 mPort{ 0 };
+ bool mLocalOnly{ true };
+};
+
+//------------------------------------------------------------------------
+LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_only):
+ mServerName(name),
+ mImpl(std::make_unique<Server_impl>(this, port, local_only))
+{
+ mImpl->init();
+
+ // Initialize the server with the given name and host
+ LL_INFOS("WebSocket") << "Creating WebSocket server: " << name <<
+ " listening " << (mImpl->mLocalOnly ? "locally" : "ON ALL INTERFACES") <<
+ " on port " << mImpl->mPort << LL_ENDL;
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
+{
+ return std::make_shared<LLWebsocketMgr::WSConnection>(server, handle);
+}
+
+bool LLWebsocketMgr::WSServer::start()
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ return mImpl->start();
+}
+
+void LLWebsocketMgr::WSServer::stop()
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ mImpl->stop();
+}
+
+bool LLWebsocketMgr::WSServer::isRunning() const
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ return !mImpl->mServer.stopped();
+}
+
+void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message)
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ LLMutexLock lock(&mConnectionMutex);
+ for (const auto& [handle, conn] : mConnections)
+ {
+ sendMessageTo(handle, message);
+ }
+}
+
+bool LLWebsocketMgr::WSServer::sendMessageTo(const connection_h& handle, const std::string& message)
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ websocketpp::lib::error_code ec;
+ mImpl->mServer.send(handle, message, websocketpp::frame::opcode::text, ec);
+ if (ec)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to send message: " << ec.message() << LL_ENDL;
+ return false;
+ }
+ return true;
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(const connection_h& handle)
+{
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it != mConnections.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle)
+{
+ WSConnection::ptr_t connection;
+ size_t size(0);
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it == mConnections.end())
+ {
+ connection = connectionFactory(shared_from_this(), handle);
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << "Failed to create connection for websocket server " << mServerName << LL_ENDL;
+ return;
+ }
+ mConnections[handle] = connection;
+ }
+ else
+ {
+ connection = it->second;
+ }
+
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL;
+ return;
+ }
+ mConnections[handle] = connection;
+ size = mConnections.size();
+ }
+
+ onConnectionOpened(connection); // TODO: consider letting the server reject the connection here
+ connection->onOpen();
+ LL_INFOS("WebSocket") << mServerName << " opened new connection, total connections: " << size << LL_ENDL;
+}
+
+void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle)
+{
+ size_t size(0);
+ WSConnection::ptr_t connection;
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it != mConnections.end())
+ {
+ connection = it->second;
+ mConnections.erase(it);
+ }
+ size = mConnections.size();
+ }
+ if (connection)
+ {
+ connection->onClose();
+ onConnectionClosed(connection);
+ LL_INFOS("WebSocket") << mServerName << " closed connection, total connections: " << size << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("WebSocket") << mServerName << " attempted to close unknown connection" << LL_ENDL;
+ }
+}
+
+void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const std::string& message)
+{
+ WSConnection::ptr_t connection = getConnection(handle);
+ if (connection)
+ {
+ connection->onMessage(message);
+ }
+ else
+ {
+ LL_WARNS("WebSocket") << mServerName << " received message for unknown connection" << LL_ENDL;
+ }
+}
+
+//------------------------------------------------------------------------
+bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message)
+{
+ if (!mServer)
+ {
+ LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL;
+ return false;
+ }
+ return mServer->sendMessageTo(mConnectionHandle, message);
+}
diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h
new file mode 100644
index 0000000000..8cf0562262
--- /dev/null
+++ b/indra/llcorehttp/llwebsocketmgr.h
@@ -0,0 +1,207 @@
+/**
+ * @file llwebsocketmgr.h
+ * @brief WebSocket manager singleton for managing WebSocket servers and connections
+ *
+ * $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 "llsingleton.h"
+#include "llsd.h"
+#include "lluuid.h"
+#include "llhost.h"
+#include "llmutex.h"
+
+#include <memory>
+#include <map>
+#include <vector>
+#include <functional>
+
+#include <websocketpp/common/connection_hdl.hpp>
+
+#if 0
+// Forward declarations
+namespace websocketpp {
+ namespace config {
+ struct asio_client;
+ struct asio;
+ }
+ template <typename config>
+ class client;
+ template <typename config>
+ class server;
+ class connection_hdl;
+}
+
+namespace LLCore {
+ namespace WebSocket {
+
+ /// WebSocket connection state enumeration
+ enum class ConnectionState
+ {
+ DISCONNECTED,
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTING,
+ FAILED
+ };
+
+ /// WebSocket message types
+ enum class MessageType
+ {
+ TEXT,
+ BINARY
+ };
+
+ /// WebSocket event types for callbacks
+ enum class EventType
+ {
+ OPEN,
+ CLOSE,
+ MESSAGE,
+ ERROR
+ };
+
+ /// Forward declarations for internal classes
+ class WSConnection;
+ class WSServer;
+
+ /// Callback function types
+ using EventCallback = std::function<void(EventType event, const LLUUID& connection_id, const LLSD& data)>;
+ using MessageCallback = std::function<void(const LLUUID& connection_id, const std::string& message, MessageType type)>;
+
+ /// WebSocket connection handle type
+ using ConnectionHandle = LLUUID;
+ using ServerHandle = LLUUID;
+
+ } // namespace WebSocket
+} // namespace LLCore
+#endif
+
+
+struct Server_impl;
+
+/**
+ * @class LLWebsocketMgr
+ * @brief Singleton manager for WebSocket connections and servers
+ *
+ * This class provides a high-level interface for managing WebSocket connections
+ * and servers using websocketpp library. It handles both client and server
+ * connections, provides thread-safe operations, and integrates with the
+ * existing Linden Lab infrastructure.
+ */
+class LLWebsocketMgr: public LLSingleton<LLWebsocketMgr>
+{
+ LLSINGLETON(LLWebsocketMgr) = default;
+ virtual ~LLWebsocketMgr() = default;
+ LOG_CLASS(LLWebsocketMgr);
+
+public:
+ using connection_h = websocketpp::connection_hdl;
+ class WSServer;
+
+ class WSConnection
+ {
+ friend class LLWebsocketMgr;
+
+ public:
+ using ptr_t = std::shared_ptr<WSConnection>;
+ WSConnection(const std::shared_ptr<WSServer> &server, const connection_h& handle):
+ mConnectionHandle(handle),
+ mServer(server)
+ {}
+
+ virtual ~WSConnection() = default;
+
+ virtual void onOpen() {}
+ virtual void onClose() {}
+ virtual void onMessage(const std::string& message) {}
+
+ bool sendMessage(const std::string& message);
+
+ private:
+ connection_h mConnectionHandle;
+ std::shared_ptr<WSServer> mServer; // Back-reference to the server this connection belongs to
+ };
+
+ class WSServer: public std::enable_shared_from_this<WSServer>
+ {
+ friend struct Server_impl;
+ friend class WSConnection;
+ friend class LLWebsocketMgr;
+
+ public:
+ using ptr_t = std::shared_ptr<WSServer>;
+
+ WSServer(std::string_view name, U16 port, bool local_only = true);
+ virtual ~WSServer() = default;
+
+ virtual void onConnectionOpened(const WSConnection::ptr_t& connection) { }
+ virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { }
+
+ bool isRunning() const;
+
+ void broadcastMessage(const std::string& message);
+ protected:
+ virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle);
+
+ bool start();
+ void stop();
+
+ bool sendMessageTo(const connection_h& handle, const std::string& message);
+
+ private:
+ using connection_map_t = std::map<connection_h, WSConnection::ptr_t, std::owner_less<connection_h> >;
+
+ WSConnection::ptr_t getConnection(const connection_h& handle);
+
+ void handleOpenConnection(const connection_h& handle);
+ void handleCloseConnection(const connection_h& handle);
+ void handleMessage(const connection_h& handle, const std::string& message);
+
+ std::string mServerName;
+ std::unique_ptr<Server_impl> mImpl;
+ connection_map_t mConnections;
+ LLMutex mConnectionMutex;
+ };
+
+ // Server and Connection Management
+ WSServer::ptr_t findServerByName(const std::string &name) const;
+
+ bool addServer(const WSServer::ptr_t& server);
+ bool removeServer(const std::string &name);
+
+ bool startServer(const std::string &name) const;
+ void stopServer(const std::string &name) const;
+
+protected:
+ void initSingleton() override;
+ void cleanupSingleton() override;
+
+private:
+ using server_map_t = std::map<std::string, WSServer::ptr_t>;
+
+ void stopAllServers();
+
+ server_map_t mServers;
+};