/** * @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 "llsdjson.h" #include #include #include #include #include #include #include //------------------------------------------------------------------------ namespace { using Server_t = websocketpp::server; using Client_t = websocketpp::client; using Connection_t = websocketpp::connection; } //------------------------------------------------------------------------ //------------------------------------------------------------------------ // LLWebsocketMgr Implementation // void LLWebsocketMgr::initSingleton() { } void LLWebsocketMgr::cleanupSingleton() { stopAllServers(); } void LLWebsocketMgr::update() { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; std::vector 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)); 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; /** * @brief Initialize the websocketpp server and configure listening * * Performs the initial setup of the websocketpp server by calling init_asio() * to initialize the ASIO networking layer, then configures the server to listen * on the specified port. The binding behavior depends on the mLocalOnly flag: * - If mLocalOnly is true: binds to "127.0.0.1" (localhost only) * - If mLocalOnly is false: binds to all available network interfaces */ void init() { mServer.init_asio(); try { if (mLocalOnly) { std::stringstream port_str; port_str << mPort; mServer.listen("127.0.0.1", port_str.str()); } else { mServer.listen(mPort); } } catch (const websocketpp::exception& e) { LL_WARNS("WebSocket") << "WebSocket server listen exception: " << e.what() << LL_ENDL; } catch (const std::exception& e) { LL_WARNS("WebSocket") << "WebSocket server listen std::exception: " << e.what() << LL_ENDL; } catch (...) { LL_WARNS("WebSocket") << "WebSocket server listen unknown exception" << LL_ENDL; } } /** * @brief Start the websocket server and begin accepting connections * @return true if server started successfully, false on error */ bool start() { //if (!mServer.stopped()) //{ // LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL; // return false; //} try { LL_INFOS("WebSocket") << "Starting WebSocket server on port " << mPort << (mLocalOnly ? " (localhost only)" : " (all interfaces)") << LL_ENDL; mServer.start_accept(); // Run controlled event loop with periodic stop flag checking while (!mOwner->mShouldStop && !mServer.stopped()) { LL_PROFILE_ZONE_NAMED_CATEGORY_WEBSOCKET("ws server run_for"); // Process events for up to 100ms, then check the stop flag std::chrono::milliseconds timeout(100); std::size_t handlers_run = mServer.get_io_service().run_for(timeout); // If no handlers were run and the server isn't stopped, // reset the io_service for the next iteration if (handlers_run == 0 && !mServer.stopped() && !mOwner->mShouldStop) { mServer.get_io_service().restart(); } } LL_INFOS("WebSocket") << "WebSocket server event loop exited cleanly" << LL_ENDL; return true; } catch (const websocketpp::exception& e) { LL_WARNS("WebSocket") << "WebSocket server exception: " << e.what() << LL_ENDL; return false; } catch (const std::exception& e) { LL_WARNS("WebSocket") << "WebSocket server std::exception: " << e.what() << LL_ENDL; return false; } catch (...) { LL_WARNS("WebSocket") << "WebSocket server unknown exception" << LL_ENDL; return false; } } 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; } } /** * @brief Handle new connection establishment event * @param hdl WebSocket connection handle from websocketpp * * Called automatically by the websocketpp library when a new client connection * is successfully established. This method serves as a bridge between the * low-level websocketpp callback and the high-level WSServer interface. */ void onOpen(websocketpp::connection_hdl hdl) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null. If it is, something is very wrong!" << LL_ENDL; mOwner->handleOpenConnection(hdl); } /** * @brief Handle connection closure event * @param hdl WebSocket connection handle from websocketpp */ void onClose(websocketpp::connection_hdl hdl) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; mOwner->handleCloseConnection(hdl); } /** * @brief Handle incoming message from client * @param hdl WebSocket connection handle identifying the sender * @param msg Shared pointer to the message object containing payload and metadata * * Called automatically by the websocketpp library when a complete message is * received from a client. This method validates the connection exists, extracts * the message payload, and forwards it to the appropriate connection handler. * * Currently handles text messages only. */ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL; 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 mOwner->handleMessage(hdl, msg->get_payload()); } //------------------------------------------- Server_t mServer; ///< The underlying websocketpp server instance LLWebsocketMgr::WSServer* mOwner{ nullptr }; ///< Back-reference to the owning WSServer instance (guaranteed non-null) U16 mPort{ 0 }; ///< TCP port number the server listens on bool mLocalOnly{ true }; ///< Whether to bind to localhost only (true) or all interfaces (false) }; //------------------------------------------------------------------------ LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_only): mServerName(name), mImpl(std::make_unique(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::WSServer::~WSServer() { // Ensure the server is stopped before destruction stop(); } LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) { return std::make_shared(server, handle); } bool LLWebsocketMgr::WSServer::start() { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; LLMutexLock lock(&mThreadMutex); // Check if already running if (isRunning()) { LL_WARNS("WebSocket") << "Server " << mServerName << " is already running" << LL_ENDL; return false; } // Reset the stop flag mShouldStop = false; // Start the server thread mServerThread = std::thread([this]() { LL_INFOS("WebSocket") << "WebSocket server thread starting for: " << mServerName << LL_ENDL; // Run the controlled server loop that checks the stop flag // Server_impl accesses mShouldStop through the mOwner pointer bool success = mImpl->start(); if (!success) { LL_WARNS("WebSocket") << "WebSocket server thread failed to start for: " << mServerName << LL_ENDL; } LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL; }); onStarted(); LL_INFOS("WebSocket") << "Started WebSocket server thread: " << mServerName << LL_ENDL; return true; } void LLWebsocketMgr::WSServer::stop() { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; { LLMutexLock lock(&mThreadMutex); // Check if already stopped if (!isRunning()) { return; } LL_INFOS("WebSocket") << "Stopping WebSocket server: " << mServerName << LL_ENDL; mShouldStop = true; // Send close frames to all connected clients before stopping the ASIO loop closeAllConnections(1001, "Server shutting down"); // Stop the websocket server (this will cause the controlled run loop to exit) mImpl->stop(); } // Release the lock here if (mServerThread.joinable()) { mServerThread.join(); LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL; } onStopped(); } 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; } void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message) { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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; } bool LLWebsocketMgr::WSServer::closeConnection(const connection_h& handle, U16 code, const std::string& reason) { LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL; try { websocketpp::lib::error_code ec; mImpl->mServer.close(handle, code, reason, ec); if (ec) { LL_WARNS("WebSocket") << mServerName << " failed to close connection: " << ec.message() << LL_ENDL; return false; } LL_INFOS("WebSocket") << mServerName << " initiated close for connection with code " << code << " and reason: " << reason << LL_ENDL; return true; } catch (const websocketpp::exception& e) { LL_WARNS("WebSocket") << mServerName << " exception closing connection: " << e.what() << LL_ENDL; return false; } catch (const std::exception& e) { LL_WARNS("WebSocket") << mServerName << " std::exception closing connection: " << e.what() << LL_ENDL; return false; } catch (...) { LL_WARNS("WebSocket") << mServerName << " unknown exception closing connection" << LL_ENDL; return false; } } 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; } LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const { websocketpp::lib::error_code ec; auto con = mImpl->mServer.get_con_from_hdl(handle, ec); if (ec) { LL_WARNS("WebSocket") << mServerName << " failed to get connection state: " << ec.message() << LL_ENDL; websocketpp::session::state::value state = websocketpp::session::state::closed; return connection_closed; } return static_cast(con->get_state()); } void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle) { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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; } // Removed redundant assignment to mConnections[handle] 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) { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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) { LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET; 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) const { if (mOwningServer.expired()) { LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL; return false; } return mOwningServer.lock()->sendMessageTo(mConnectionHandle, message); } bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const { std::string message = boost::json::serialize(json); return sendMessage(message); } bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const { return sendMessage(LlsdToJson(data)); } void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason) { if (mOwningServer.expired()) { LL_WARNS("WebSocket") << "Attempted to close connection with null server reference" << LL_ENDL; return; } LL_INFOS("WebSocket") << "WSConnection closing connection with code " << code << " and reason: " << (reason.empty() ? "(no reason)" : reason) << LL_ENDL; if (!mOwningServer.lock()->closeConnection(mConnectionHandle, code, reason)) { LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL; } } bool LLWebsocketMgr::WSConnection::isConnected() const { if (mOwningServer.expired()) { return false; } LLWebsocketMgr::WSServer::ptr_t server = mOwningServer.lock(); if (!server) { return false; } return server->getConnectionState(mConnectionHandle) == connection_open; } LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSConnection::getSelfPtr() { auto server = mOwningServer.lock(); if (!server) return nullptr; return server->getConnection(mConnectionHandle); } void LLWebsocketMgr::WSServer::closeAllConnections(U16 code, const std::string& reason) { std::vector handles; { LLMutexLock lock(&mConnectionMutex); for (const auto& [handle, conn] : mConnections) { handles.push_back(handle); } } for (const auto& handle : handles) { closeConnection(handle, code, reason); } }