diff --git a/CMakeLists.txt b/CMakeLists.txt index 83a3685..2d3725c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") option(EOSERV_WANT_MYSQL "Enables MariaDB/MySQL server database support." ON) option(EOSERV_WANT_SQLITE "Enables SQLite3 embedded database support." ON) option(EOSERV_WANT_SQLSERVER "Enables Microsoft SQL Server database support." ON) +option(EOSERV_WANT_WEBSOCKET "Enables WebSocket listener for web client connections." OFF) option(EOSERV_USE_PRECOMPILED_HEADERS "Uses a precompiled header to speed up compilation." ON) option(EOSERV_USE_UNITY_BUILD "Compiles multiple source files in one translation unit to speed up compilation." ON) @@ -253,6 +254,15 @@ target_include_directories(eoserv_lib PUBLIC ${CMAKE_SOURCE_DIR}/json) target_link_libraries(etheos eoserv_lib) target_link_libraries(eoserv_lib ${eoserv_LIBRARIES}) +if(EOSERV_WANT_WEBSOCKET) + include(DownloadIXWebSocket) + target_include_directories(etheos PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-src) + target_include_directories(eoserv_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-src) + list(APPEND eoserv_LIBRARIES ixwebsocket) + target_link_libraries(eoserv_lib ixwebsocket) + add_compile_definitions(WEBSOCKET_SUPPORT) +endif() + install(TARGETS etheos RUNTIME DESTINATION .) install(TARGETS eoserv_test RUNTIME DESTINATION ./test) diff --git a/build-linux.sh b/build-linux.sh index a031626..b19a65e 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -15,6 +15,7 @@ function main() { local mariadb="ON" local sqlite="ON" local sqlserver="ON" + local websocket="OFF" local option while [[ "$#" -gt 0 ]] @@ -53,6 +54,10 @@ function main() { sqlserver="$2" shift ;; + --websocket) + websocket="$2" + shift + ;; *) echo "Error: unsupported option \"${option}\"" return 1 @@ -86,6 +91,11 @@ function main() { return 1 fi + if [[ "${websocket}" != "ON" && "${websocket}" != "OFF" ]]; then + echo "Error: acceptable values for option --websocket are ON|OFF" + return 1 + fi + echo "" echo "Build mode: ${build_mode}" echo "Build directory: ${build_dir}" @@ -93,6 +103,7 @@ function main() { echo "MariaDB support: ${mariadb}" echo "SQLite support: ${sqlite}" echo "SQL Server support: ${sqlserver}" + echo "WebSocket support: ${websocket}" if [[ "${opt_clean}" == "true" ]]; then echo "" @@ -114,6 +125,7 @@ function main() { cmake_macros+=("-DEOSERV_WANT_MYSQL=${mariadb}") cmake_macros+=("-DEOSERV_WANT_SQLITE=${sqlite}") cmake_macros+=("-DEOSERV_WANT_SQLSERVER=${sqlserver}") + cmake_macros+=("-DEOSERV_WANT_WEBSOCKET=${websocket}") pushd "${build_dir}" > /dev/null @@ -154,6 +166,7 @@ function display_usage() { echo " --mariadb (ON|OFF) MariaDB/MySQL support [default: OFF]." echo " --sqlite (ON|OFF) SQLite support [default: OFF]." echo " --sqlserver (ON|OFF) SQL Server support [default: ON]." + echo " --websocket (ON|OFF) WebSocket listener support [default: OFF]." echo " -h --help Display this message." } diff --git a/cmake/DownloadIXWebSocket.cmake b/cmake/DownloadIXWebSocket.cmake new file mode 100644 index 0000000..096e681 --- /dev/null +++ b/cmake/DownloadIXWebSocket.cmake @@ -0,0 +1,30 @@ + +# Download and unpack IXWebSocket at configure time +if (NOT EOSERV_OFFLINE) + configure_file(${CMAKE_SOURCE_DIR}/cmake/ixwebsocketproj.cmake ixwebsocket-download/CMakeLists.txt) + + execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-download ) + if(result) + message(FATAL_ERROR "CMake step for IXWebSocket failed: ${result}") + endif() + + execute_process(COMMAND ${CMAKE_COMMAND} --build . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-download ) + if(result) + message(FATAL_ERROR "Build step for IXWebSocket failed: ${result}") + endif() +endif() + +# Configure IXWebSocket options before adding subdirectory +set(USE_TLS OFF CACHE BOOL "" FORCE) +set(USE_OPEN_SSL OFF CACHE BOOL "" FORCE) +set(USE_MBED_TLS OFF CACHE BOOL "" FORCE) +set(IXWEBSOCKET_INSTALL OFF CACHE BOOL "" FORCE) + +# Add IXWebSocket directly to our build +add_subdirectory(${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-src + ${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-build + EXCLUDE_FROM_ALL) diff --git a/cmake/ixwebsocketproj.cmake b/cmake/ixwebsocketproj.cmake new file mode 100644 index 0000000..2034048 --- /dev/null +++ b/cmake/ixwebsocketproj.cmake @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.5) + +project(ixwebsocket-download NONE) + +include(ExternalProject) +ExternalProject_Add(ixwebsocket + GIT_REPOSITORY https://github.com/machinezone/IXWebSocket.git + GIT_TAG v11.4.5 + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-src" + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ixwebsocket-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) diff --git a/config/server.ini b/config/server.ini index 79b1451..4be275b 100644 --- a/config/server.ini +++ b/config/server.ini @@ -160,3 +160,12 @@ ThreadPoolThreads = 0 ## WorldDumpFile (string) # Path to a file used as a json dump for the world when the server crashes or exits WorldDumpFile = ./world.bak.json + +## WebSocketEnabled (bool) +# Enable WebSocket listener for web client connections +# Requires the server to be compiled with EOSERV_WANT_WEBSOCKET (--websocket ON) +WebSocketEnabled = no + +## WebSocketPort (number) +# The port the WebSocket listener should listen on +WebSocketPort = 8082 diff --git a/src/eoserv_config.cpp b/src/eoserv_config.cpp index 9cd1766..f88ccd7 100644 --- a/src/eoserv_config.cpp +++ b/src/eoserv_config.cpp @@ -291,6 +291,8 @@ void eoserv_config_validate_config(Config& config) eoserv_config_default(config, "ThreadPoolThreads" , 0); eoserv_config_default(config, "AutoCreateDatabase" , false); eoserv_config_default(config, "WorldDumpFile" , "./world.bak.json"); + eoserv_config_default(config, "WebSocketEnabled" , false); + eoserv_config_default(config, "WebSocketPort" , 8082); } void eoserv_config_validate_admin(Config& config) diff --git a/src/eoserver.cpp b/src/eoserver.cpp index e7cd582..a2bf4be 100644 --- a/src/eoserver.cpp +++ b/src/eoserver.cpp @@ -28,6 +28,10 @@ #include #include +#ifdef WEBSOCKET_SUPPORT +#include "wsserver.hpp" +#endif + void server_ping_all(void *server_void) { EOServer *server = static_cast(server_void); @@ -67,7 +71,7 @@ void server_check_hangup(void *server_void) { EOClient *client = static_cast(rawclient); - if (client->Connected() && !client->Accepted() && client->start + delay < now) + if (client->Connected() && !client->Accepted() && !client->IsWebSocket() && client->start + delay < now) { server->RecordClientRejection(client->GetRemoteAddr(), "hanging up on idle client"); client->Close(true); @@ -193,6 +197,26 @@ void EOServer::Initialize(std::shared_ptr databaseFactory, cons this->start = Timer::GetTime(); this->UpdateConfig(); + +#ifdef WEBSOCKET_SUPPORT + if (this->world->config["WebSocketEnabled"]) + { + int ws_port = int(this->world->config["WebSocketPort"]); + std::string host = std::string(this->world->config["Host"]); + this->wsserver = new WSServer(this, host, ws_port); + + if (this->wsserver->Start()) + { + Console::Out("WebSocket server listening on %s:%i", host.c_str(), ws_port); + } + else + { + Console::Err("Failed to start WebSocket server on port %i", ws_port); + delete this->wsserver; + this->wsserver = nullptr; + } + } +#endif } Client *EOServer::ClientFactory(const Socket &sock) @@ -295,6 +319,13 @@ void EOServer::Tick() this->BuryTheDead(); +#ifdef WEBSOCKET_SUPPORT + if (this->wsserver) + { + this->wsserver->Tick(); + } +#endif + this->world->timer.Tick(); } @@ -312,8 +343,29 @@ void EOServer::RecordClientRejection(const IPAddress& ip, const char* reason) } } +void EOServer::OnClientRemoved(Client *client) +{ +#ifdef WEBSOCKET_SUPPORT + if (client->IsWebSocket() && this->wsserver) + { + this->wsserver->RemoveClient(client); + } +#else + (void)client; +#endif +} + EOServer::~EOServer() { +#ifdef WEBSOCKET_SUPPORT + if (this->wsserver) + { + this->wsserver->Stop(); + delete this->wsserver; + this->wsserver = nullptr; + } +#endif + // All clients must be fully closed before the world ends UTIL_FOREACH(this->clients, client) { diff --git a/src/eoserver.hpp b/src/eoserver.hpp index f39330e..7798dd3 100644 --- a/src/eoserver.hpp +++ b/src/eoserver.hpp @@ -18,6 +18,10 @@ #include "socket.hpp" +#ifdef WEBSOCKET_SUPPORT +#include "fwd/wsserver.hpp" +#endif + #include #include #include @@ -66,7 +70,13 @@ class EOServer : public Server void RecordClientRejection(const IPAddress& ip, const char* reason); void CleanupConnectionLog(); + void OnClientRemoved(Client *client) override; + ~EOServer(); + +#ifdef WEBSOCKET_SUPPORT + WSServer *wsserver = nullptr; +#endif }; #endif // EOSERVER_HPP_INCLUDED diff --git a/src/fwd/wsclient.hpp b/src/fwd/wsclient.hpp new file mode 100644 index 0000000..66c217d --- /dev/null +++ b/src/fwd/wsclient.hpp @@ -0,0 +1,14 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifndef FWD_WSCLIENT_HPP_INCLUDED +#define FWD_WSCLIENT_HPP_INCLUDED + +#ifdef WEBSOCKET_SUPPORT +class WSClient; +#endif // WEBSOCKET_SUPPORT + +#endif // FWD_WSCLIENT_HPP_INCLUDED diff --git a/src/fwd/wsserver.hpp b/src/fwd/wsserver.hpp new file mode 100644 index 0000000..4d8ad1b --- /dev/null +++ b/src/fwd/wsserver.hpp @@ -0,0 +1,14 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifndef FWD_WSSERVER_HPP_INCLUDED +#define FWD_WSSERVER_HPP_INCLUDED + +#ifdef WEBSOCKET_SUPPORT +class WSServer; +#endif // WEBSOCKET_SUPPORT + +#endif // FWD_WSSERVER_HPP_INCLUDED diff --git a/src/socket.hpp b/src/socket.hpp index 8cdec29..b202a83 100644 --- a/src/socket.hpp +++ b/src/socket.hpp @@ -230,6 +230,7 @@ class Client Server *server; bool connected; bool accepted = false; + bool is_websocket = false; std::time_t closed_time; std::time_t connect_time; @@ -270,9 +271,10 @@ class Client bool Accepted() const { return accepted; } void MarkAccepted() { accepted = true; } + bool IsWebSocket() const { return is_websocket; } virtual bool Connected() const; - IPAddress GetRemoteAddr() const; + virtual IPAddress GetRemoteAddr() const; void AsyncOpPending(bool asyncOpPending) { this->async_op_pending = asyncOpPending; } bool IsAsyncOpPending() const { return this->async_op_pending; } @@ -437,7 +439,9 @@ class Server return this->maxconn; } - virtual ~Server(); + virtual void OnClientRemoved(Client *) {} + + virtual ~Server(); }; diff --git a/src/wsclient.cpp b/src/wsclient.cpp new file mode 100644 index 0000000..43296c0 --- /dev/null +++ b/src/wsclient.cpp @@ -0,0 +1,221 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifdef WEBSOCKET_SUPPORT + +#include "wsclient.hpp" + +#include "console.hpp" +#include "eoserver.hpp" +#include "packet.hpp" +#include "player.hpp" +#include "character.hpp" +#include "world.hpp" +#include "socket.hpp" + +#include +#include +#include + +WSClient::WSClient(std::nullptr_t, EOServer *server, const IPAddress &addr) + : EOClient(server) + , remote_addr(addr) +{ + this->connected = true; + this->is_websocket = true; + this->connect_time = std::time(0); + + this->SetRecvBuffer(32768); + this->SetSendBuffer(32768); +} + +void WSClient::SetWebSocketSender(SendFunction sender) +{ + ws_send = sender; +} + +void WSClient::QueueRecvData(const std::string &data) +{ + std::lock_guard lock(recv_mutex); + recv_queue.push(data); +} + +void WSClient::PumpRecvQueue() +{ + std::lock_guard lock(recv_mutex); + + while (!recv_queue.empty()) + { + const std::string &data = recv_queue.front(); + + if (data.length() > this->recv_buffer.length() - this->recv_buffer_used) + { + Console::Wrn("WSClient recv buffer overflow, disconnecting"); + recv_queue = std::queue(); + this->Close(true); + return; + } + + const std::size_t mask = this->recv_buffer.length() - 1; + + for (std::size_t i = 0; i < data.length(); ++i) + { + this->recv_buffer_ppos = (this->recv_buffer_ppos + 1) & mask; + this->recv_buffer[this->recv_buffer_ppos] = data[i]; + } + + this->recv_buffer_used += data.length(); + + recv_queue.pop(); + } +} + +bool WSClient::NeedTick() +{ + return false; +} + +bool WSClient::Upload(FileType type, int id, InitReply init_reply) +{ + char mapbuf[7]; + std::sprintf(mapbuf, "%05i", int(std::abs(id))); + + std::string filename; + switch (type) + { + case FILE_MAP: filename = std::string(server()->world->config["MapDir"]) + mapbuf + ".emf"; break; + case FILE_ITEM: filename = std::string(server()->world->config["EIF"]); break; + case FILE_NPC: filename = std::string(server()->world->config["ENF"]); break; + case FILE_SPELL: filename = std::string(server()->world->config["ESF"]); break; + case FILE_CLASS: filename = std::string(server()->world->config["ECF"]); break; + default: return false; + } + + std::FILE *fh = std::fopen(filename.c_str(), "rb"); + if (!fh) + return false; + + std::fseek(fh, 0, SEEK_END); + std::size_t file_size = std::ftell(fh); + std::fseek(fh, 0, SEEK_SET); + + std::string file_data(file_size, '\0'); + if (std::fread(&file_data[0], 1, file_size, fh) != file_size) + { + std::fclose(fh); + return false; + } + std::fclose(fh); + + // Dynamically rewrite map bytes for GlobalPK + if (type == FILE_MAP && server()->world->config["GlobalPK"] && !server()->world->PKExcept(player->character->mapid)) + { + if (file_size > 0x03) + file_data[0x03] = (char)0xFF; + if (file_size > 0x04) + file_data[0x04] = (char)0x01; + if (file_size > 0x1F) + file_data[0x1F] = (char)0x04; + } + + // Build the header packet + PacketBuilder builder(PACKET_F_INIT, PACKET_A_INIT, 2); + builder.AddChar(init_reply); + + if (type != FILE_MAP) + builder.AddChar(1); + + builder.AddSize(file_size); + + LogPacket(PACKET_F_INIT, PACKET_A_INIT, builder.Length(), "UPLD"); + + // Encode the header + std::string header = this->processor.Encode(builder); + + // Combine header + raw file data into one message + std::string combined; + combined.reserve(header.size() + file_data.size()); + combined.append(header); + combined.append(file_data); + + // Send as a single WebSocket binary message + if (ws_send && ws_connected) + { + ws_send(combined); + } + + return true; +} + +void WSClient::Send(const PacketBuilder &builder) +{ + std::lock_guard lock(send_mutex); + + auto fam = PacketFamily(PacketProcessor::EPID(builder.GetID())[1]); + auto act = PacketAction(PacketProcessor::EPID(builder.GetID())[0]); + this->LogPacket(fam, act, builder.Length(), "SEND"); + + std::string data = this->processor.Encode(builder); + + if (this->upload_fh) + { + if (data.length() > this->send_buffer2.length() - this->send_buffer2_used) + { + this->Close(true); + return; + } + + const std::size_t mask = this->send_buffer2.length() - 1; + + for (std::size_t i = 0; i < data.length(); ++i) + { + this->send_buffer2_ppos = (this->send_buffer2_ppos + 1) & mask; + this->send_buffer2[this->send_buffer2_ppos] = data[i]; + } + + this->send_buffer2_used += data.length(); + } + else + { + if (ws_send && ws_connected) + { + ws_send(data); + } + } +} + +bool WSClient::Connected() const +{ + return this->connected && this->ws_connected; +} + +IPAddress WSClient::GetRemoteAddr() const +{ + return remote_addr; +} + +void WSClient::Close(bool force) +{ + (void)force; + + this->connected = false; + this->ws_connected = false; + this->closed_time = std::time(0); + + if (force) + { + this->closed_time = 0; + } + + ws_send = nullptr; +} + +WSClient::~WSClient() +{ + ws_send = nullptr; +} + +#endif // WEBSOCKET_SUPPORT diff --git a/src/wsclient.hpp b/src/wsclient.hpp new file mode 100644 index 0000000..4934477 --- /dev/null +++ b/src/wsclient.hpp @@ -0,0 +1,68 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifndef WSCLIENT_HPP_INCLUDED +#define WSCLIENT_HPP_INCLUDED + +#ifdef WEBSOCKET_SUPPORT + +#include "fwd/wsclient.hpp" + +#include "eoclient.hpp" +#include "eoserver.hpp" + +#include +#include +#include +#include + +/** + * An EOClient subclass that communicates over a WebSocket connection + * instead of a raw TCP socket. + * + * Incoming WebSocket binary messages are queued and fed into the + * normal EOClient::Tick() pipeline via PumpRecvQueue(). + * + * Outgoing packets are intercepted by overriding Send() and routed + * through the WebSocket connection via a sender callback. + */ +class WSClient : public EOClient +{ + public: + using SendFunction = std::function; + + private: + SendFunction ws_send; + IPAddress remote_addr; + + std::queue recv_queue; + std::mutex recv_mutex; + + bool ws_connected = true; + + public: + WSClient(std::nullptr_t, EOServer *server, const IPAddress &addr); + + void SetWebSocketSender(SendFunction sender); + + void PumpRecvQueue(); + void QueueRecvData(const std::string &data); + + virtual bool Upload(FileType type, int id, InitReply init_reply) override; + virtual bool NeedTick() override; + virtual void Send(const PacketBuilder &packet) override; + virtual bool Connected() const override; + + IPAddress GetRemoteAddr() const; + + virtual void Close(bool force = false) override; + + virtual ~WSClient(); +}; + +#endif // WEBSOCKET_SUPPORT + +#endif // WSCLIENT_HPP_INCLUDED diff --git a/src/wsserver.cpp b/src/wsserver.cpp new file mode 100644 index 0000000..ff1d198 --- /dev/null +++ b/src/wsserver.cpp @@ -0,0 +1,196 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifdef WEBSOCKET_SUPPORT + +#include "wsserver.hpp" + +#include "wsclient.hpp" +#include "eoserver.hpp" + +#include "console.hpp" +#include "socket.hpp" + +#include +#include + +#include +#include +#include + +WSServer::WSServer(EOServer *eo_server, const std::string &host, int port) + : server(port, host) + , eo_server(eo_server) +{ + ix::initNetSystem(); + + server.setOnClientMessageCallback( + [this](std::shared_ptr connectionState, + ix::WebSocket &webSocket, + const ix::WebSocketMessagePtr &msg) + { + std::string connId = connectionState->getId(); + + if (msg->type == ix::WebSocketMessageType::Open) + { + std::string remote_ip = connectionState->getRemoteIp(); + IPAddress addr = IPAddress::Lookup(remote_ip); + + Console::Out("WebSocket connection from %s (id: %s)", remote_ip.c_str(), connId.c_str()); + + // Create a WSClient for this WebSocket connection + // We don't use shared_ptr for the WebSocket since the server manages its lifetime + WSClient *wsclient = new WSClient(nullptr, this->eo_server, addr); + + { + std::lock_guard lock(this->clients_mutex); + this->ws_clients[connId] = wsclient; + } + + // Set a per-connection message callback to avoid the shared_ptr issue + webSocket.setOnMessageCallback( + [this, connId, &webSocket](const ix::WebSocketMessagePtr &innerMsg) + { + if (innerMsg->type == ix::WebSocketMessageType::Message) + { + if (innerMsg->binary) + { + std::lock_guard lock(this->clients_mutex); + auto it = this->ws_clients.find(connId); + if (it != this->ws_clients.end()) + { + it->second->QueueRecvData(innerMsg->str); + } + } + } + else if (innerMsg->type == ix::WebSocketMessageType::Close) + { + std::lock_guard lock(this->clients_mutex); + auto it = this->ws_clients.find(connId); + if (it != this->ws_clients.end()) + { + it->second->Close(); + this->ws_clients.erase(it); + } + } + } + ); + + // Store the webSocket reference for sending in WSClient + // We need to set the send callback on the WSClient + wsclient->SetWebSocketSender( + [&webSocket](const std::string &data) + { + webSocket.sendBinary(data); + } + ); + + // Add to the EOServer's client list so timers (ping, hangup, pump_queue) work + this->eo_server->clients.push_back(wsclient); + } + else if (msg->type == ix::WebSocketMessageType::Message) + { + if (msg->binary) + { + std::lock_guard lock(this->clients_mutex); + auto it = this->ws_clients.find(connId); + if (it != this->ws_clients.end()) + { + it->second->QueueRecvData(msg->str); + } + } + } + else if (msg->type == ix::WebSocketMessageType::Close) + { + std::lock_guard lock(this->clients_mutex); + auto it = this->ws_clients.find(connId); + if (it != this->ws_clients.end()) + { + it->second->Close(); + this->ws_clients.erase(it); + } + } + else if (msg->type == ix::WebSocketMessageType::Error) + { + Console::Wrn("WebSocket error: %s", msg->errorInfo.reason.c_str()); + + std::lock_guard lock(this->clients_mutex); + auto it = this->ws_clients.find(connId); + if (it != this->ws_clients.end()) + { + it->second->Close(); + this->ws_clients.erase(it); + } + } + } + ); +} + +bool WSServer::Start() +{ + auto res = server.listen(); + if (!res.first) + { + Console::Err("Failed to start WebSocket server: %s", res.second.c_str()); + return false; + } + + server.start(); + return true; +} + +void WSServer::Tick() +{ + std::lock_guard lock(clients_mutex); + + for (auto &pair : ws_clients) + { + WSClient *client = pair.second; + + if (!client->Connected()) + continue; + + // Pump queued WebSocket messages into the recv buffer + client->PumpRecvQueue(); + } +} + +void WSServer::Stop() +{ + server.stop(); + + std::lock_guard lock(clients_mutex); + + for (auto &pair : ws_clients) + { + pair.second->Close(); + } + + ws_clients.clear(); + + ix::uninitNetSystem(); +} + +WSServer::~WSServer() +{ + Stop(); +} + +void WSServer::RemoveClient(Client *client) +{ + std::lock_guard lock(clients_mutex); + + for (auto it = ws_clients.begin(); it != ws_clients.end(); ++it) + { + if (it->second == client) + { + ws_clients.erase(it); + break; + } + } +} + +#endif // WEBSOCKET_SUPPORT diff --git a/src/wsserver.hpp b/src/wsserver.hpp new file mode 100644 index 0000000..dfc4b70 --- /dev/null +++ b/src/wsserver.hpp @@ -0,0 +1,50 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifndef WSSERVER_HPP_INCLUDED +#define WSSERVER_HPP_INCLUDED + +#ifdef WEBSOCKET_SUPPORT + +#include "fwd/wsserver.hpp" +#include "fwd/eoserver.hpp" + +#include + +#include +#include +#include +#include + +class WSClient; + +/** + * WebSocket server that listens for web client connections and bridges + * them into the existing EOServer client pipeline via WSClient. + */ +class WSServer +{ + private: + ix::WebSocketServer server; + EOServer *eo_server; + + std::mutex clients_mutex; + std::unordered_map ws_clients; + + public: + WSServer(EOServer *eo_server, const std::string &host, int port); + + bool Start(); + void Tick(); + void Stop(); + void RemoveClient(Client *client); + + ~WSServer(); +}; + +#endif // WEBSOCKET_SUPPORT + +#endif // WSSERVER_HPP_INCLUDED