Merge branch 'master' of github.com:machinezone/IXWebSocket

This commit is contained in:
Benjamin Sergeant 2024-03-27 22:03:54 -07:00
commit 93e673da9f
13 changed files with 96 additions and 18 deletions

View File

@ -11,7 +11,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: seanmiddleditch/gha-setup-ninja@master - uses: seanmiddleditch/gha-setup-ninja@master
- uses: egor-tensin/setup-mingw@v2.2.0 - uses: bsergean/setup-mingw@d79ce405bac9edef3a1726ef00554a56f0bafe66
- run: | - run: |
mkdir build mkdir build
cd build cd build

2
.gitignore vendored
View File

@ -8,3 +8,5 @@ ws/.srl
ixhttpd ixhttpd
makefile makefile
a.out a.out
.idea/
cmake-build-debug/

View File

@ -1,5 +1,7 @@
## Hello world ## 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. 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. 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.

View File

@ -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 ## HTTP client API
```cpp ```cpp

View File

@ -133,16 +133,20 @@ namespace ix
if (headers.find("Content-Length") != headers.end()) if (headers.find("Content-Length") != headers.end())
{ {
int contentLength = 0; 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<int>::min()
|| val > std::numeric_limits<int>::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) if (contentLength < 0)
{ {
return std::make_tuple( return std::make_tuple(

View File

@ -34,8 +34,12 @@ namespace ix
SelectInterruptPipe::~SelectInterruptPipe() SelectInterruptPipe::~SelectInterruptPipe()
{ {
::close(_fildes[kPipeReadIndex]); if (-1 != _fildes[kPipeReadIndex]) {
::close(_fildes[kPipeWriteIndex]); ::close(_fildes[kPipeReadIndex]);
}
if (-1 != _fildes[kPipeWriteIndex]) {
::close(_fildes[kPipeWriteIndex]);
}
_fildes[kPipeReadIndex] = -1; _fildes[kPipeReadIndex] = -1;
_fildes[kPipeWriteIndex] = -1; _fildes[kPipeWriteIndex] = -1;
} }

View File

@ -352,6 +352,11 @@ namespace ix
return res; return res;
} }
if (res == 0)
{
errno = ECONNRESET;
}
if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE) if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE)
{ {
errno = EWOULDBLOCK; errno = EWOULDBLOCK;

View File

@ -26,6 +26,7 @@
#ifdef _WIN32 #ifdef _WIN32
// For manipulating the certificate store // For manipulating the certificate store
#include <windows.h>
#include <wincrypt.h> #include <wincrypt.h>
#endif #endif
@ -293,10 +294,16 @@ namespace ix
*/ */
bool SocketOpenSSL::checkHost(const std::string& host, const char* pattern) bool SocketOpenSSL::checkHost(const std::string& host, const char* pattern)
{ {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
return true;
#else
#ifdef _WIN32 #ifdef _WIN32
return PathMatchSpecA(host.c_str(), pattern); return PathMatchSpecA(host.c_str(), pattern);
#else #else
return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH; return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH;
#endif
#endif #endif
} }

View File

@ -180,7 +180,7 @@ namespace
bHasUserName = true; bHasUserName = true;
break; break;
} }
else if (*LocalString == '/') else if (*LocalString == '/' || *LocalString == '?')
{ {
// end of <host>:<port> specification // end of <host>:<port> specification
bHasUserName = false; bHasUserName = false;
@ -242,7 +242,7 @@ namespace
LocalString++; LocalString++;
break; break;
} }
else if (!bHasBracket && (*LocalString == ':' || *LocalString == '/')) else if (!bHasBracket && (*LocalString == ':' || *LocalString == '/' || *LocalString == '?'))
{ {
// port number is specified // port number is specified
break; break;
@ -280,12 +280,14 @@ namespace
} }
// skip '/' // skip '/'
if (*CurrentString != '/') if (*CurrentString != '/' && *CurrentString != '?')
{ {
return clParseURL(LUrlParserError_NoSlash); return clParseURL(LUrlParserError_NoSlash);
} }
CurrentString++; if (*CurrentString != '?') {
CurrentString++;
}
// parse the path // parse the path
LocalString = CurrentString; LocalString = CurrentString;

View File

@ -57,7 +57,7 @@ namespace ix
server.setOnConnectionCallback( server.setOnConnectionCallback(
[remoteUrl, remoteUrlsMapping](std::weak_ptr<ix::WebSocket> webSocket, [remoteUrl, remoteUrlsMapping](std::weak_ptr<ix::WebSocket> webSocket,
std::shared_ptr<ConnectionState> connectionState) { std::shared_ptr<ConnectionState> connectionState) {
auto state = std::dynamic_pointer_cast<ProxyConnectionState>(connectionState); auto state = std::static_pointer_cast<ProxyConnectionState>(connectionState);
auto remoteIp = connectionState->getRemoteIp(); auto remoteIp = connectionState->getRemoteIp();
// Server connection // Server connection

View File

@ -19,17 +19,20 @@ namespace ix
{ {
const int WebSocketServer::kDefaultHandShakeTimeoutSecs(3); // 3 seconds const int WebSocketServer::kDefaultHandShakeTimeoutSecs(3); // 3 seconds
const bool WebSocketServer::kDefaultEnablePong(true); const bool WebSocketServer::kDefaultEnablePong(true);
const int WebSocketServer::kPingIntervalSeconds(-1); // disable heartbeat
WebSocketServer::WebSocketServer(int port, WebSocketServer::WebSocketServer(int port,
const std::string& host, const std::string& host,
int backlog, int backlog,
size_t maxConnections, size_t maxConnections,
int handshakeTimeoutSecs, int handshakeTimeoutSecs,
int addressFamily) int addressFamily,
int pingIntervalSeconds)
: SocketServer(port, host, backlog, maxConnections, addressFamily) : SocketServer(port, host, backlog, maxConnections, addressFamily)
, _handshakeTimeoutSecs(handshakeTimeoutSecs) , _handshakeTimeoutSecs(handshakeTimeoutSecs)
, _enablePong(kDefaultEnablePong) , _enablePong(kDefaultEnablePong)
, _enablePerMessageDeflate(true) , _enablePerMessageDeflate(true)
, _pingIntervalSeconds(pingIntervalSeconds)
{ {
} }
@ -93,6 +96,7 @@ namespace ix
auto webSocket = std::make_shared<WebSocket>(); auto webSocket = std::make_shared<WebSocket>();
webSocket->setAutoThreadName(false); webSocket->setAutoThreadName(false);
webSocket->setPingInterval(_pingIntervalSeconds);
if (_onConnectionCallback) if (_onConnectionCallback)
{ {

View File

@ -33,7 +33,8 @@ namespace ix
int backlog = SocketServer::kDefaultTcpBacklog, int backlog = SocketServer::kDefaultTcpBacklog,
size_t maxConnections = SocketServer::kDefaultMaxConnections, size_t maxConnections = SocketServer::kDefaultMaxConnections,
int handshakeTimeoutSecs = WebSocketServer::kDefaultHandShakeTimeoutSecs, int handshakeTimeoutSecs = WebSocketServer::kDefaultHandShakeTimeoutSecs,
int addressFamily = SocketServer::kDefaultAddressFamily); int addressFamily = SocketServer::kDefaultAddressFamily,
int pingIntervalSeconds = WebSocketServer::kPingIntervalSeconds);
virtual ~WebSocketServer(); virtual ~WebSocketServer();
virtual void stop() final; virtual void stop() final;
@ -61,6 +62,7 @@ namespace ix
int _handshakeTimeoutSecs; int _handshakeTimeoutSecs;
bool _enablePong; bool _enablePong;
bool _enablePerMessageDeflate; bool _enablePerMessageDeflate;
int _pingIntervalSeconds;
OnConnectionCallback _onConnectionCallback; OnConnectionCallback _onConnectionCallback;
OnClientMessageCallback _onClientMessageCallback; OnClientMessageCallback _onClientMessageCallback;
@ -69,6 +71,7 @@ namespace ix
std::set<std::shared_ptr<WebSocket>> _clients; std::set<std::shared_ptr<WebSocket>> _clients;
const static bool kDefaultEnablePong; const static bool kDefaultEnablePong;
const static int kPingIntervalSeconds;
// Methods // Methods
virtual void handleConnection(std::unique_ptr<Socket> socket, virtual void handleConnection(std::unique_ptr<Socket> socket,

View File

@ -84,6 +84,40 @@ namespace ix
REQUIRE(port == 443); // default port for wss 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") SECTION("real test")
{ {
std::string url = std::string url =