diff --git a/.github/workflows/unittest_windows_gcc.yml b/.github/workflows/unittest_windows_gcc.yml index c25a3003..4908ae1f 100644 --- a/.github/workflows/unittest_windows_gcc.yml +++ b/.github/workflows/unittest_windows_gcc.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v1 - uses: seanmiddleditch/gha-setup-ninja@master - - uses: egor-tensin/setup-mingw@v2.2.0 + - uses: bsergean/setup-mingw@d79ce405bac9edef3a1726ef00554a56f0bafe66 - run: | mkdir build cd build diff --git a/.gitignore b/.gitignore index 892b7bf6..36e9925a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ ws/.srl ixhttpd makefile a.out +.idea/ +cmake-build-debug/ diff --git a/README.md b/README.md index 4fb3b04d..a0f17753 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## Hello world +(note from the main developer, sadly I don't have too much time to devote to this library anymore, maybe it's time to pass the maintenance to someone else more motivated ?) + IXWebSocket is a C++ library for WebSocket client and server development. It has minimal dependencies (no boost), is very simple to use and support everything you'll likely need for websocket dev (SSL, deflate compression, compiles on most platforms, etc...). HTTP client and server code is also available, but it hasn't received as much testing. It is been used on big mobile video game titles sending and receiving tons of messages since 2017 (iOS and Android). It was tested on macOS, iOS, Linux, Android, Windows and FreeBSD. Two important design goals are simplicity and correctness. diff --git a/docs/usage.md b/docs/usage.md index 57799be7..e7a77c75 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -445,6 +445,17 @@ server.wait(); ``` +### Heartbeat + +You can configure an optional heartbeat / keep-alive for the WebSocket server. The heartbeat interval can be adjusted or disabled when constructing the `WebSocketServer`. Setting the interval to `-1` disables the heartbeat feature; this is the default setting. The parameter you set will be applied to every `WebSocket` object that the server creates. + +To enable a 45 second heartbeat on a `WebSocketServer`: + +```cpp +int pingIntervalSeconds = 45; +ix::WebSocketServer server(port, host, backlog, maxConnections, handshakeTimeoutSecs, addressFamily, pingIntervalSeconds); +``` + ## HTTP client API ```cpp diff --git a/ixwebsocket/IXHttp.cpp b/ixwebsocket/IXHttp.cpp index 46504026..19f17fbc 100644 --- a/ixwebsocket/IXHttp.cpp +++ b/ixwebsocket/IXHttp.cpp @@ -133,16 +133,20 @@ namespace ix if (headers.find("Content-Length") != headers.end()) { int contentLength = 0; - try { - contentLength = std::stoi(headers["Content-Length"]); + const char* p = headers["Content-Length"].c_str(); + char* p_end{}; + errno = 0; + long val = std::strtol(p, &p_end, 10); + if (p_end == p // invalid argument + || errno == ERANGE // out of range + || val < std::numeric_limits::min() + || val > std::numeric_limits::max()) { + return std::make_tuple( + false, "Error parsing HTTP Header 'Content-Length'", httpRequest); + } + contentLength = val; } - catch (const std::exception&) - { - return std::make_tuple( - false, "Error parsing HTTP Header 'Content-Length'", httpRequest); - } - if (contentLength < 0) { return std::make_tuple( diff --git a/ixwebsocket/IXSelectInterruptPipe.cpp b/ixwebsocket/IXSelectInterruptPipe.cpp index 75c42f27..0518994a 100644 --- a/ixwebsocket/IXSelectInterruptPipe.cpp +++ b/ixwebsocket/IXSelectInterruptPipe.cpp @@ -34,8 +34,12 @@ namespace ix SelectInterruptPipe::~SelectInterruptPipe() { - ::close(_fildes[kPipeReadIndex]); - ::close(_fildes[kPipeWriteIndex]); + if (-1 != _fildes[kPipeReadIndex]) { + ::close(_fildes[kPipeReadIndex]); + } + if (-1 != _fildes[kPipeWriteIndex]) { + ::close(_fildes[kPipeWriteIndex]); + } _fildes[kPipeReadIndex] = -1; _fildes[kPipeWriteIndex] = -1; } diff --git a/ixwebsocket/IXSocketMbedTLS.cpp b/ixwebsocket/IXSocketMbedTLS.cpp index 0192dc78..97c87b97 100644 --- a/ixwebsocket/IXSocketMbedTLS.cpp +++ b/ixwebsocket/IXSocketMbedTLS.cpp @@ -352,6 +352,11 @@ namespace ix return res; } + if (res == 0) + { + errno = ECONNRESET; + } + if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE) { errno = EWOULDBLOCK; diff --git a/ixwebsocket/IXSocketOpenSSL.cpp b/ixwebsocket/IXSocketOpenSSL.cpp index e37f3a6e..43d9bf3f 100644 --- a/ixwebsocket/IXSocketOpenSSL.cpp +++ b/ixwebsocket/IXSocketOpenSSL.cpp @@ -26,6 +26,7 @@ #ifdef _WIN32 // For manipulating the certificate store +#include #include #endif @@ -293,10 +294,16 @@ namespace ix */ bool SocketOpenSSL::checkHost(const std::string& host, const char* pattern) { +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + return true; +#else + #ifdef _WIN32 return PathMatchSpecA(host.c_str(), pattern); #else return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH; +#endif + #endif } diff --git a/ixwebsocket/IXUrlParser.cpp b/ixwebsocket/IXUrlParser.cpp index aa1ef408..c5cdb57b 100644 --- a/ixwebsocket/IXUrlParser.cpp +++ b/ixwebsocket/IXUrlParser.cpp @@ -180,7 +180,7 @@ namespace bHasUserName = true; break; } - else if (*LocalString == '/') + else if (*LocalString == '/' || *LocalString == '?') { // end of : specification bHasUserName = false; @@ -242,7 +242,7 @@ namespace LocalString++; break; } - else if (!bHasBracket && (*LocalString == ':' || *LocalString == '/')) + else if (!bHasBracket && (*LocalString == ':' || *LocalString == '/' || *LocalString == '?')) { // port number is specified break; @@ -280,12 +280,14 @@ namespace } // skip '/' - if (*CurrentString != '/') + if (*CurrentString != '/' && *CurrentString != '?') { return clParseURL(LUrlParserError_NoSlash); } - CurrentString++; + if (*CurrentString != '?') { + CurrentString++; + } // parse the path LocalString = CurrentString; diff --git a/ixwebsocket/IXWebSocketProxyServer.cpp b/ixwebsocket/IXWebSocketProxyServer.cpp index 4b78c63b..4df5a115 100644 --- a/ixwebsocket/IXWebSocketProxyServer.cpp +++ b/ixwebsocket/IXWebSocketProxyServer.cpp @@ -57,7 +57,7 @@ namespace ix server.setOnConnectionCallback( [remoteUrl, remoteUrlsMapping](std::weak_ptr webSocket, std::shared_ptr connectionState) { - auto state = std::dynamic_pointer_cast(connectionState); + auto state = std::static_pointer_cast(connectionState); auto remoteIp = connectionState->getRemoteIp(); // Server connection diff --git a/ixwebsocket/IXWebSocketServer.cpp b/ixwebsocket/IXWebSocketServer.cpp index 4518389b..cb6988a5 100644 --- a/ixwebsocket/IXWebSocketServer.cpp +++ b/ixwebsocket/IXWebSocketServer.cpp @@ -19,17 +19,20 @@ namespace ix { const int WebSocketServer::kDefaultHandShakeTimeoutSecs(3); // 3 seconds const bool WebSocketServer::kDefaultEnablePong(true); + const int WebSocketServer::kPingIntervalSeconds(-1); // disable heartbeat WebSocketServer::WebSocketServer(int port, const std::string& host, int backlog, size_t maxConnections, int handshakeTimeoutSecs, - int addressFamily) + int addressFamily, + int pingIntervalSeconds) : SocketServer(port, host, backlog, maxConnections, addressFamily) , _handshakeTimeoutSecs(handshakeTimeoutSecs) , _enablePong(kDefaultEnablePong) , _enablePerMessageDeflate(true) + , _pingIntervalSeconds(pingIntervalSeconds) { } @@ -93,6 +96,7 @@ namespace ix auto webSocket = std::make_shared(); webSocket->setAutoThreadName(false); + webSocket->setPingInterval(_pingIntervalSeconds); if (_onConnectionCallback) { diff --git a/ixwebsocket/IXWebSocketServer.h b/ixwebsocket/IXWebSocketServer.h index dcb21e81..7636074e 100644 --- a/ixwebsocket/IXWebSocketServer.h +++ b/ixwebsocket/IXWebSocketServer.h @@ -33,7 +33,8 @@ namespace ix int backlog = SocketServer::kDefaultTcpBacklog, size_t maxConnections = SocketServer::kDefaultMaxConnections, int handshakeTimeoutSecs = WebSocketServer::kDefaultHandShakeTimeoutSecs, - int addressFamily = SocketServer::kDefaultAddressFamily); + int addressFamily = SocketServer::kDefaultAddressFamily, + int pingIntervalSeconds = WebSocketServer::kPingIntervalSeconds); virtual ~WebSocketServer(); virtual void stop() final; @@ -61,6 +62,7 @@ namespace ix int _handshakeTimeoutSecs; bool _enablePong; bool _enablePerMessageDeflate; + int _pingIntervalSeconds; OnConnectionCallback _onConnectionCallback; OnClientMessageCallback _onClientMessageCallback; @@ -69,6 +71,7 @@ namespace ix std::set> _clients; const static bool kDefaultEnablePong; + const static int kPingIntervalSeconds; // Methods virtual void handleConnection(std::unique_ptr socket, diff --git a/test/IXUrlParserTest.cpp b/test/IXUrlParserTest.cpp index cee3f3bf..dd2786dc 100644 --- a/test/IXUrlParserTest.cpp +++ b/test/IXUrlParserTest.cpp @@ -84,6 +84,40 @@ namespace ix REQUIRE(port == 443); // default port for wss } + SECTION("wss://google.com/?arg=value") + { + std::string url = "wss://google.com/?arg=value&arg2=value2"; + std::string protocol, host, path, query; + int port; + bool res; + + res = UrlParser::parse(url, protocol, host, path, query, port); + + REQUIRE(res); + REQUIRE(protocol == "wss"); + REQUIRE(host == "google.com"); + REQUIRE(path == "/?arg=value&arg2=value2"); + REQUIRE(query == "arg=value&arg2=value2"); + REQUIRE(port == 443); // default port for wss + } + + SECTION("wss://google.com?arg=value") + { + std::string url = "wss://google.com?arg=value&arg2=value2"; + std::string protocol, host, path, query; + int port; + bool res; + + res = UrlParser::parse(url, protocol, host, path, query, port); + + REQUIRE(res); + REQUIRE(protocol == "wss"); + REQUIRE(host == "google.com"); + REQUIRE(path == "/?arg=value&arg2=value2"); + REQUIRE(query == "arg=value&arg2=value2"); + REQUIRE(port == 443); // default port for wss + } + SECTION("real test") { std::string url =