Skip to content

Commit 652279d

Browse files
bsergeanclaude
andauthored
Make server-side pre-connection errors observable via a log callback (fix #593) (#595)
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 <noreply@anthropic.com>
1 parent e120cfc commit 652279d

4 files changed

Lines changed: 109 additions & 2 deletions

File tree

‎docs/usage.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,25 @@ int pingIntervalSeconds = 45;
457457
ix::WebSocketServer server(port, host, backlog, maxConnections, handshakeTimeoutSecs, addressFamily, pingIntervalSeconds);
458458
```
459459

460+
### Server log callback
461+
462+
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.
463+
464+
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.
465+
466+
```cpp
467+
server.setLogCallback([](ix::LogLevel level, const std::string& msg) {
468+
if (level == ix::LogLevel::Error)
469+
{
470+
myLogger.error(msg);
471+
}
472+
else
473+
{
474+
myLogger.info(msg);
475+
}
476+
});
477+
```
478+
460479
## HTTP client API
461480

462481
```cpp

‎ixwebsocket/IXSocketServer.cpp‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,31 @@ namespace ix
4646
stop();
4747
}
4848

49+
void SocketServer::setLogCallback(const LogCallback& callback)
50+
{
51+
std::lock_guard<std::mutex> lock(_logMutex);
52+
_logCallback = callback;
53+
}
54+
4955
void SocketServer::logError(const std::string& str)
5056
{
5157
std::lock_guard<std::mutex> lock(_logMutex);
58+
if (_logCallback)
59+
{
60+
_logCallback(LogLevel::Error, str);
61+
return;
62+
}
5263
fprintf(stderr, "%s\n", str.c_str());
5364
}
5465

5566
void SocketServer::logInfo(const std::string& str)
5667
{
5768
std::lock_guard<std::mutex> lock(_logMutex);
69+
if (_logCallback)
70+
{
71+
_logCallback(LogLevel::Info, str);
72+
return;
73+
}
5874
fprintf(stdout, "%s\n", str.c_str());
5975
}
6076

@@ -421,7 +437,8 @@ namespace ix
421437

422438
if (socket == nullptr)
423439
{
424-
logError("SocketServer::run() cannot create socket: " + errorMsg);
440+
logError("SocketServer::run() cannot create socket for client " + remoteIp + ":" +
441+
std::to_string(remotePort) + ": " + errorMsg);
425442
Socket::closeSocket(clientFd);
426443
continue;
427444
}
@@ -431,7 +448,8 @@ namespace ix
431448

432449
if (!socket->accept(errorMsg))
433450
{
434-
logError("SocketServer::run() tls accept failed: " + errorMsg);
451+
logError("SocketServer::run() tls accept failed for client " + remoteIp + ":" +
452+
std::to_string(remotePort) + ": " + errorMsg);
435453
Socket::closeSocket(clientFd);
436454
continue;
437455
}

‎ixwebsocket/IXSocketServer.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@ namespace ix
2525
{
2626
class Socket;
2727

28+
enum class LogLevel
29+
{
30+
Info,
31+
Error
32+
};
33+
2834
class SocketServer
2935
{
3036
public:
3137
using ConnectionStateFactory = std::function<std::shared_ptr<ConnectionState>()>;
3238

39+
// Application-provided sink for server log messages. When set, messages
40+
// that would otherwise be written to stderr/stdout (such as TLS accept
41+
// failures happening before a connection is established) are delivered
42+
// to the callback instead.
43+
using LogCallback = std::function<void(LogLevel level, const std::string& message)>;
44+
3345
// Each connection is handled by its own worker thread.
3446
// We use a list as we only care about remove and append operations.
3547
using ConnectionThreads =
@@ -48,6 +60,8 @@ namespace ix
4860
// that inherits from ConnectionState but has its own methods.
4961
void setConnectionStateFactory(const ConnectionStateFactory& connectionStateFactory);
5062

63+
void setLogCallback(const LogCallback& callback);
64+
5165
const static int kDefaultPort;
5266
const static std::string kDefaultHost;
5367
const static int kDefaultTcpBacklog;
@@ -86,6 +100,7 @@ namespace ix
86100
std::atomic<bool> _stop;
87101

88102
std::mutex _logMutex;
103+
LogCallback _logCallback; // protected by _logMutex
89104

90105
// background thread to wait for incoming connections
91106
std::thread _thread;

‎test/IXWebSocketServerTest.cpp‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,59 @@ TEST_CASE("Websocket_server", "[websocket_server]")
195195
REQUIRE(connectionId == "foobarConnectionId");
196196
REQUIRE(server.getClients().size() == 0);
197197
}
198+
199+
#if defined(IXWEBSOCKET_USE_OPEN_SSL) || defined(IXWEBSOCKET_USE_MBED_TLS)
200+
SECTION("TLS server: a failed TLS handshake is reported through the log callback")
201+
{
202+
int port = getFreePort();
203+
ix::WebSocketServer server(port);
204+
server.setTLSOptions(makeServerTLSOptions(true));
205+
206+
std::mutex logMutex;
207+
std::vector<std::string> errors;
208+
server.setLogCallback([&logMutex, &errors](LogLevel level, const std::string& msg) {
209+
std::lock_guard<std::mutex> lock(logMutex);
210+
if (level == LogLevel::Error)
211+
{
212+
errors.push_back(msg);
213+
}
214+
});
215+
216+
std::string connectionId;
217+
REQUIRE(startServer(server, connectionId));
218+
219+
// Talk plaintext HTTP to the TLS endpoint: this cannot be a valid
220+
// ClientHello, so the server fails the connection during the TLS
221+
// handshake, before any WebSocket exists.
222+
std::string errMsg;
223+
bool tls = false;
224+
SocketTLSOptions tlsOptions;
225+
std::shared_ptr<Socket> socket = createSocket(tls, -1, errMsg, tlsOptions);
226+
std::string host("127.0.0.1");
227+
auto isCancellationRequested = []() -> bool { return false; };
228+
bool success = socket->connect(host, port, errMsg, isCancellationRequested);
229+
REQUIRE(success);
230+
231+
socket->writeBytes("GET / HTTP/1.1\r\n\r\n", isCancellationRequested);
232+
233+
bool logged = false;
234+
for (int i = 0; i < 100 && !logged; ++i)
235+
{
236+
ix::msleep(100);
237+
std::lock_guard<std::mutex> lock(logMutex);
238+
logged = !errors.empty();
239+
}
240+
REQUIRE(logged);
241+
242+
{
243+
std::lock_guard<std::mutex> lock(logMutex);
244+
TLogger() << "server error log: " << errors[0];
245+
REQUIRE(errors[0].find("tls accept failed") != std::string::npos);
246+
REQUIRE(errors[0].find("127.0.0.1") != std::string::npos);
247+
}
248+
249+
server.stop();
250+
REQUIRE(server.getClients().size() == 0);
251+
}
252+
#endif
198253
}

0 commit comments

Comments
 (0)