diff options
| author | Rider Linden <rider@lindenlab.com> | 2025-08-22 16:16:23 -0700 |
|---|---|---|
| committer | Rider Linden <rider@lindenlab.com> | 2025-10-07 09:19:30 -0700 |
| commit | dcf6d8c5024c831d54f0f5f140551e65748ed513 (patch) | |
| tree | 00751ad690d7b8b0ab74489e15cc7113ed284b82 /indra/llcorehttp | |
| parent | 0b7bc35131ce4a526c1e696904c4e8332e1b0fe9 (diff) | |
Initial simple websockets server with managing singleton.
Diffstat (limited to 'indra/llcorehttp')
| -rw-r--r-- | indra/llcorehttp/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.cpp | 411 | ||||
| -rw-r--r-- | indra/llcorehttp/llwebsocketmgr.h | 207 |
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; +}; |
