From 9eae96a936006519a568133d6eb01d5a456e2fec Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Wed, 15 Jul 2026 14:34:02 -0700 Subject: [PATCH] Make server-side pre-connection errors observable via a log callback (fix #593) Server-side TLS handshake failures (and other pre-connection errors such as accept failures or hitting max connections) were only written to stderr by SocketServer::logError, so applications wired to setOnClientMessageCallback never saw them - unlike the client side, which reports TLS failures through the Error message callback. - Add SocketServer::setLogCallback(LogLevel, message): when set, server log messages are delivered to the callback instead of stderr/stdout. Applies to WebSocketServer and HttpServer alike. - Include the client ip and port in the tls accept failure and socket creation error messages. - Add a unittest exercising a failed TLS handshake against a wss server and asserting the error reaches the callback with the peer address. Co-Authored-By: Claude Fable 5 --- docs/usage.md | 19 ++++++++++++ ixwebsocket/IXSocketServer.cpp | 22 ++++++++++++-- ixwebsocket/IXSocketServer.h | 15 ++++++++++ test/IXWebSocketServerTest.cpp | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 0a02e28a..c79b8a6c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -457,6 +457,25 @@ int pingIntervalSeconds = 45; ix::WebSocketServer server(port, host, backlog, maxConnections, handshakeTimeoutSecs, addressFamily, pingIntervalSeconds); ``` +### Server log callback + +By default the server writes internal errors to stderr (and some info messages to stdout). Errors that happen before a connection is fully established — for example a failed TLS handshake when a client presents a bad certificate — cannot be reported through `setOnClientMessageCallback`, since no WebSocket object exists yet at that point. + +To capture those messages in your application logs, set a log callback. It applies to `WebSocketServer` as well as `HttpServer`. When a callback is set, messages are delivered to it instead of being written to stderr/stdout. + +```cpp +server.setLogCallback([](ix::LogLevel level, const std::string& msg) { + if (level == ix::LogLevel::Error) + { + myLogger.error(msg); + } + else + { + myLogger.info(msg); + } +}); +``` + ## HTTP client API ```cpp diff --git a/ixwebsocket/IXSocketServer.cpp b/ixwebsocket/IXSocketServer.cpp index 6a2e6808..6d4f342c 100644 --- a/ixwebsocket/IXSocketServer.cpp +++ b/ixwebsocket/IXSocketServer.cpp @@ -46,15 +46,31 @@ namespace ix stop(); } + void SocketServer::setLogCallback(const LogCallback& callback) + { + std::lock_guard lock(_logMutex); + _logCallback = callback; + } + void SocketServer::logError(const std::string& str) { std::lock_guard lock(_logMutex); + if (_logCallback) + { + _logCallback(LogLevel::Error, str); + return; + } fprintf(stderr, "%s\n", str.c_str()); } void SocketServer::logInfo(const std::string& str) { std::lock_guard lock(_logMutex); + if (_logCallback) + { + _logCallback(LogLevel::Info, str); + return; + } fprintf(stdout, "%s\n", str.c_str()); } @@ -421,7 +437,8 @@ namespace ix if (socket == nullptr) { - logError("SocketServer::run() cannot create socket: " + errorMsg); + logError("SocketServer::run() cannot create socket for client " + remoteIp + ":" + + std::to_string(remotePort) + ": " + errorMsg); Socket::closeSocket(clientFd); continue; } @@ -431,7 +448,8 @@ namespace ix if (!socket->accept(errorMsg)) { - logError("SocketServer::run() tls accept failed: " + errorMsg); + logError("SocketServer::run() tls accept failed for client " + remoteIp + ":" + + std::to_string(remotePort) + ": " + errorMsg); Socket::closeSocket(clientFd); continue; } diff --git a/ixwebsocket/IXSocketServer.h b/ixwebsocket/IXSocketServer.h index c56b650e..96be7578 100644 --- a/ixwebsocket/IXSocketServer.h +++ b/ixwebsocket/IXSocketServer.h @@ -25,11 +25,23 @@ namespace ix { class Socket; + enum class LogLevel + { + Info, + Error + }; + class SocketServer { public: using ConnectionStateFactory = std::function()>; + // Application-provided sink for server log messages. When set, messages + // that would otherwise be written to stderr/stdout (such as TLS accept + // failures happening before a connection is established) are delivered + // to the callback instead. + using LogCallback = std::function; + // Each connection is handled by its own worker thread. // We use a list as we only care about remove and append operations. using ConnectionThreads = @@ -48,6 +60,8 @@ namespace ix // that inherits from ConnectionState but has its own methods. void setConnectionStateFactory(const ConnectionStateFactory& connectionStateFactory); + void setLogCallback(const LogCallback& callback); + const static int kDefaultPort; const static std::string kDefaultHost; const static int kDefaultTcpBacklog; @@ -86,6 +100,7 @@ namespace ix std::atomic _stop; std::mutex _logMutex; + LogCallback _logCallback; // protected by _logMutex // background thread to wait for incoming connections std::thread _thread; diff --git a/test/IXWebSocketServerTest.cpp b/test/IXWebSocketServerTest.cpp index bce933d0..e43bf0cc 100644 --- a/test/IXWebSocketServerTest.cpp +++ b/test/IXWebSocketServerTest.cpp @@ -195,4 +195,59 @@ TEST_CASE("Websocket_server", "[websocket_server]") REQUIRE(connectionId == "foobarConnectionId"); REQUIRE(server.getClients().size() == 0); } + +#if defined(IXWEBSOCKET_USE_OPEN_SSL) || defined(IXWEBSOCKET_USE_MBED_TLS) + SECTION("TLS server: a failed TLS handshake is reported through the log callback") + { + int port = getFreePort(); + ix::WebSocketServer server(port); + server.setTLSOptions(makeServerTLSOptions(true)); + + std::mutex logMutex; + std::vector errors; + server.setLogCallback([&logMutex, &errors](LogLevel level, const std::string& msg) { + std::lock_guard lock(logMutex); + if (level == LogLevel::Error) + { + errors.push_back(msg); + } + }); + + std::string connectionId; + REQUIRE(startServer(server, connectionId)); + + // Talk plaintext HTTP to the TLS endpoint: this cannot be a valid + // ClientHello, so the server fails the connection during the TLS + // handshake, before any WebSocket exists. + std::string errMsg; + bool tls = false; + SocketTLSOptions tlsOptions; + std::shared_ptr socket = createSocket(tls, -1, errMsg, tlsOptions); + std::string host("127.0.0.1"); + auto isCancellationRequested = []() -> bool { return false; }; + bool success = socket->connect(host, port, errMsg, isCancellationRequested); + REQUIRE(success); + + socket->writeBytes("GET / HTTP/1.1\r\n\r\n", isCancellationRequested); + + bool logged = false; + for (int i = 0; i < 100 && !logged; ++i) + { + ix::msleep(100); + std::lock_guard lock(logMutex); + logged = !errors.empty(); + } + REQUIRE(logged); + + { + std::lock_guard lock(logMutex); + TLogger() << "server error log: " << errors[0]; + REQUIRE(errors[0].find("tls accept failed") != std::string::npos); + REQUIRE(errors[0].find("127.0.0.1") != std::string::npos); + } + + server.stop(); + REQUIRE(server.getClients().size() == 0); + } +#endif }