Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 20 additions & 2 deletions ixwebsocket/IXSocketServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,31 @@ namespace ix
stop();
}

void SocketServer::setLogCallback(const LogCallback& callback)
{
std::lock_guard<std::mutex> lock(_logMutex);
_logCallback = callback;
}

void SocketServer::logError(const std::string& str)
{
std::lock_guard<std::mutex> 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<std::mutex> lock(_logMutex);
if (_logCallback)
{
_logCallback(LogLevel::Info, str);
return;
}
fprintf(stdout, "%s\n", str.c_str());
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions ixwebsocket/IXSocketServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,23 @@ namespace ix
{
class Socket;

enum class LogLevel
{
Info,
Error
};

class SocketServer
{
public:
using ConnectionStateFactory = std::function<std::shared_ptr<ConnectionState>()>;

// 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<void(LogLevel level, const std::string& message)>;

// Each connection is handled by its own worker thread.
// We use a list as we only care about remove and append operations.
using ConnectionThreads =
Expand All @@ -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;
Expand Down Expand Up @@ -86,6 +100,7 @@ namespace ix
std::atomic<bool> _stop;

std::mutex _logMutex;
LogCallback _logCallback; // protected by _logMutex

// background thread to wait for incoming connections
std::thread _thread;
Expand Down
55 changes: 55 additions & 0 deletions test/IXWebSocketServerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> errors;
server.setLogCallback([&logMutex, &errors](LogLevel level, const std::string& msg) {
std::lock_guard<std::mutex> 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> 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<std::mutex> lock(logMutex);
logged = !errors.empty();
}
REQUIRE(logged);

{
std::lock_guard<std::mutex> 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
}
Loading