Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
c5a02f1066 | |||
e03c0be8a4 | |||
3b66efbb6a | |||
f29906c72f | |||
872f516ede | |||
014d43eb13 | |||
d77067e50f | |||
ed5b1a0895 | |||
ef57e3a2b1 | |||
28832f8732 | |||
0dd284267a | |||
a7019631b7 | |||
632ee31509 |
27
.github/workflows/unittest_windows.yml
vendored
27
.github/workflows/unittest_windows.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: windows
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: seanmiddleditch/gha-setup-vsdevenv@master
|
|
||||||
- uses: seanmiddleditch/gha-setup-ninja@master
|
|
||||||
- run: |
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake -GNinja -DCMAKE_CXX_COMPILER=cl.exe -DCMAKE_C_COMPILER=cl.exe -DUSE_WS=1 -DUSE_TEST=1 -DUSE_ZLIB=OFF -DBUILD_SHARED_LIBS=OFF ..
|
|
||||||
- run: |
|
|
||||||
cd build
|
|
||||||
ninja
|
|
||||||
- run: |
|
|
||||||
cd build
|
|
||||||
ninja test
|
|
||||||
|
|
||||||
#- run: ../build/test/ixwebsocket_unittest.exe
|
|
||||||
# working-directory: test
|
|
2
.github/workflows/unittest_windows_gcc.yml
vendored
2
.github/workflows/unittest_windows_gcc.yml
vendored
@ -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
|
- uses: bsergean/setup-mingw@d79ce405bac9edef3a1726ef00554a56f0bafe66
|
||||||
- run: |
|
- run: |
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
|
11
README.md
11
README.md
@ -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.
|
||||||
@ -99,10 +101,10 @@ Starting with the 11.0.8 release, IXWebSocket should be fully C++11 compatible.
|
|||||||
If your company or project is using this library, feel free to open an issue or PR to amend this list.
|
If your company or project is using this library, feel free to open an issue or PR to amend this list.
|
||||||
|
|
||||||
- [Machine Zone](https://www.mz.com)
|
- [Machine Zone](https://www.mz.com)
|
||||||
- [Tokio](https://gitlab.com/HCInk/tokio), a discord library focused on audio playback with node bindings.
|
- [Tokio](https://github.com/liz3/tokio), a discord library focused on audio playback with node bindings.
|
||||||
- [libDiscordBot](https://github.com/tostc/libDiscordBot/tree/master), an easy to use Discord-bot framework.
|
- [libDiscordBot](https://github.com/tostc/libDiscordBot/tree/master), an easy to use Discord-bot framework.
|
||||||
- [gwebsocket](https://github.com/norrbotten/gwebsocket), a websocket (lua) module for Garry's Mod
|
- [gwebsocket](https://github.com/norrbotten/gwebsocket), a websocket (lua) module for Garry's Mod
|
||||||
- [DisCPP](https://github.com/DisCPP/DisCPP), a simple but feature rich Discord API wrapper
|
- [DisCPP](https://github.com/DisCPP/DisCPP), a simple but feature rich Discord API wrapper (archived as of Oct 8, 2021)
|
||||||
- [discord.cpp](https://github.com/luccanunes/discord.cpp), a discord library for making bots
|
- [discord.cpp](https://github.com/luccanunes/discord.cpp), a discord library for making bots
|
||||||
- [Teleport](http://teleportconnect.com/), Teleport is your own personal remote robot avatar
|
- [Teleport](http://teleportconnect.com/), Teleport is your own personal remote robot avatar
|
||||||
- [Abaddon](https://github.com/uowuo/abaddon), An alternative Discord client made with C++/gtkmm
|
- [Abaddon](https://github.com/uowuo/abaddon), An alternative Discord client made with C++/gtkmm
|
||||||
@ -115,8 +117,8 @@ There are plenty of great websocket libraries out there, which might work for yo
|
|||||||
|
|
||||||
* [websocketpp](https://github.com/zaphoyd/websocketpp) - C++
|
* [websocketpp](https://github.com/zaphoyd/websocketpp) - C++
|
||||||
* [beast](https://github.com/boostorg/beast) - C++
|
* [beast](https://github.com/boostorg/beast) - C++
|
||||||
|
* [µWebSockets](https://github.com/uNetworking/uWebSockets) - C++
|
||||||
* [libwebsockets](https://libwebsockets.org/) - C
|
* [libwebsockets](https://libwebsockets.org/) - C
|
||||||
* [µWebSockets](https://github.com/uNetworking/uWebSockets) - C
|
|
||||||
* [wslay](https://github.com/tatsuhiro-t/wslay) - C
|
* [wslay](https://github.com/tatsuhiro-t/wslay) - C
|
||||||
|
|
||||||
[uvweb](https://github.com/bsergean/uvweb) is a library written by the IXWebSocket author which is built on top of [uvw](https://github.com/skypjack/uvw), which is a C++ wrapper for [libuv](https://libuv.org/). It has more dependencies and does not support SSL at this point, but it can be used to open multiple connections within a single OS thread thanks to libuv.
|
[uvweb](https://github.com/bsergean/uvweb) is a library written by the IXWebSocket author which is built on top of [uvw](https://github.com/skypjack/uvw), which is a C++ wrapper for [libuv](https://libuv.org/). It has more dependencies and does not support SSL at this point, but it can be used to open multiple connections within a single OS thread thanks to libuv.
|
||||||
@ -134,9 +136,7 @@ To check the performance of a websocket library, you can look at the [autoroute]
|
|||||||
| Windows | Disabled | None | [![Build2][5]][0] |
|
| Windows | Disabled | None | [![Build2][5]][0] |
|
||||||
| UWP | Disabled | None | [![Build2][6]][0] |
|
| UWP | Disabled | None | [![Build2][6]][0] |
|
||||||
| Linux | OpenSSL | Address Sanitizer | [![Build2][7]][0] |
|
| Linux | OpenSSL | Address Sanitizer | [![Build2][7]][0] |
|
||||||
| Mingw | Disabled | None | [![Build2][8]][0] |
|
|
||||||
|
|
||||||
* ASAN fails on Linux because of a known problem, we need a
|
|
||||||
* Some tests are disabled on Windows/UWP because of a pathing problem
|
* Some tests are disabled on Windows/UWP because of a pathing problem
|
||||||
* TLS and ZLIB are disabled on Windows/UWP because enabling make the CI run takes a lot of time, for setting up vcpkg.
|
* TLS and ZLIB are disabled on Windows/UWP because enabling make the CI run takes a lot of time, for setting up vcpkg.
|
||||||
|
|
||||||
@ -148,5 +148,4 @@ To check the performance of a websocket library, you can look at the [autoroute]
|
|||||||
[5]: https://github.com/machinezone/IXWebSocket/workflows/windows/badge.svg
|
[5]: https://github.com/machinezone/IXWebSocket/workflows/windows/badge.svg
|
||||||
[6]: https://github.com/machinezone/IXWebSocket/workflows/uwp/badge.svg
|
[6]: https://github.com/machinezone/IXWebSocket/workflows/uwp/badge.svg
|
||||||
[7]: https://github.com/machinezone/IXWebSocket/workflows/linux_asan/badge.svg
|
[7]: https://github.com/machinezone/IXWebSocket/workflows/linux_asan/badge.svg
|
||||||
[8]: https://github.com/machinezone/IXWebSocket/workflows/unittest_windows_gcc/badge.svg
|
|
||||||
|
|
||||||
|
@ -27,9 +27,13 @@
|
|||||||
|
|
||||||
// mingw build quirks
|
// mingw build quirks
|
||||||
#if defined(_WIN32) && defined(__GNUC__)
|
#if defined(_WIN32) && defined(__GNUC__)
|
||||||
|
#ifndef AI_NUMERICSERV
|
||||||
#define AI_NUMERICSERV NI_NUMERICSERV
|
#define AI_NUMERICSERV NI_NUMERICSERV
|
||||||
|
#endif
|
||||||
|
#ifndef AI_ADDRCONFIG
|
||||||
#define AI_ADDRCONFIG LUP_ADDRCONFIG
|
#define AI_ADDRCONFIG LUP_ADDRCONFIG
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace ix
|
namespace ix
|
||||||
{
|
{
|
||||||
|
@ -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(
|
||||||
|
@ -148,6 +148,7 @@ namespace ix
|
|||||||
content = gzipCompress(content);
|
content = gzipCompress(content);
|
||||||
headers["Content-Encoding"] = "gzip";
|
headers["Content-Encoding"] = "gzip";
|
||||||
}
|
}
|
||||||
|
headers["Accept-Encoding"] = "gzip";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Log request
|
// Log request
|
||||||
@ -161,11 +162,6 @@ namespace ix
|
|||||||
// headers["Content-Type"] = "application/octet-stream";
|
// headers["Content-Type"] = "application/octet-stream";
|
||||||
headers["Accept-Ranges"] = "none";
|
headers["Accept-Ranges"] = "none";
|
||||||
|
|
||||||
for (auto&& it : request->headers)
|
|
||||||
{
|
|
||||||
headers[it.first] = it.second;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::make_shared<HttpResponse>(
|
return std::make_shared<HttpResponse>(
|
||||||
200, "OK", HttpErrorCode::Ok, headers, content);
|
200, "OK", HttpErrorCode::Ok, headers, content);
|
||||||
});
|
});
|
||||||
|
@ -69,7 +69,7 @@ namespace ix
|
|||||||
{
|
{
|
||||||
// We must deselect the networkevents from the socket event. Otherwise the
|
// We must deselect the networkevents from the socket event. Otherwise the
|
||||||
// socket will report states that aren't there.
|
// socket will report states that aren't there.
|
||||||
if (_fd != nullptr && _fd->fd != -1)
|
if (_fd != nullptr && (int)_fd->fd != -1)
|
||||||
WSAEventSelect(_fd->fd, _event, 0);
|
WSAEventSelect(_fd->fd, _event, 0);
|
||||||
WSACloseEvent(_event);
|
WSACloseEvent(_event);
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ namespace ix
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
// WSAWaitForMultipleEvents returns the index of the first signaled event. And to emulate WSAPoll()
|
// WSAWaitForMultipleEvents returns the index of the first signaled event. And to emulate WSAPoll()
|
||||||
// all the signaled events must be processed.
|
// all the signaled events must be processed.
|
||||||
while (socketIndex < socketEvents.size())
|
while (socketIndex < (int)socketEvents.size())
|
||||||
{
|
{
|
||||||
struct pollfd* fd = socketEvents[socketIndex];
|
struct pollfd* fd = socketEvents[socketIndex];
|
||||||
|
|
||||||
@ -345,7 +345,7 @@ namespace ix
|
|||||||
buf[best] = buf[best + 1] = ':';
|
buf[best] = buf[best + 1] = ':';
|
||||||
memmove(buf + best + 2, buf + best + max, i - best - max + 1);
|
memmove(buf + best + 2, buf + best + max, i - best - max + 1);
|
||||||
}
|
}
|
||||||
if (strlen(buf) < l)
|
if (strlen(buf) < (size_t)l)
|
||||||
{
|
{
|
||||||
strcpy(s, buf);
|
strcpy(s, buf);
|
||||||
return s;
|
return s;
|
||||||
|
@ -49,7 +49,7 @@ namespace ix
|
|||||||
mbedtls_pk_init(&_pkey);
|
mbedtls_pk_init(&_pkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SocketMbedTLS::loadSystemCertificates(std::string& /* errorMsg */)
|
bool SocketMbedTLS::loadSystemCertificates(std::string& errorMsg)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
DWORD flags = CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG |
|
DWORD flags = CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG |
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ namespace
|
|||||||
X509_STORE* opensslStore = SSL_CTX_get_cert_store(ssl);
|
X509_STORE* opensslStore = SSL_CTX_get_cert_store(ssl);
|
||||||
|
|
||||||
int certificateCount = 0;
|
int certificateCount = 0;
|
||||||
while (certificateIterator = CertEnumCertificatesInStore(systemStore, certificateIterator))
|
while ((certificateIterator = CertEnumCertificatesInStore(systemStore, certificateIterator)))
|
||||||
{
|
{
|
||||||
X509* x509 = d2i_X509(NULL,
|
X509* x509 = d2i_X509(NULL,
|
||||||
(const unsigned char**) &certificateIterator->pbCertEncoded,
|
(const unsigned char**) &certificateIterator->pbCertEncoded,
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ namespace ix
|
|||||||
, _enablePong(kDefaultEnablePong)
|
, _enablePong(kDefaultEnablePong)
|
||||||
, _pingIntervalSecs(kDefaultPingIntervalSecs)
|
, _pingIntervalSecs(kDefaultPingIntervalSecs)
|
||||||
, _pingType(SendMessageKind::Ping)
|
, _pingType(SendMessageKind::Ping)
|
||||||
|
, _autoThreadName(true)
|
||||||
{
|
{
|
||||||
_ws.setOnCloseCallback(
|
_ws.setOnCloseCallback(
|
||||||
[this](uint16_t code, const std::string& reason, size_t wireSize, bool remote)
|
[this](uint16_t code, const std::string& reason, size_t wireSize, bool remote)
|
||||||
@ -370,7 +371,10 @@ namespace ix
|
|||||||
|
|
||||||
void WebSocket::run()
|
void WebSocket::run()
|
||||||
{
|
{
|
||||||
setThreadName(getUrl());
|
if (_autoThreadName)
|
||||||
|
{
|
||||||
|
setThreadName(getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
bool firstConnectionAttempt = true;
|
bool firstConnectionAttempt = true;
|
||||||
|
|
||||||
@ -627,4 +631,9 @@ namespace ix
|
|||||||
std::lock_guard<std::mutex> lock(_configMutex);
|
std::lock_guard<std::mutex> lock(_configMutex);
|
||||||
return _subProtocols;
|
return _subProtocols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebSocket::setAutoThreadName(bool enabled)
|
||||||
|
{
|
||||||
|
_autoThreadName = enabled;
|
||||||
|
}
|
||||||
} // namespace ix
|
} // namespace ix
|
||||||
|
@ -119,6 +119,8 @@ namespace ix
|
|||||||
uint32_t getMinWaitBetweenReconnectionRetries() const;
|
uint32_t getMinWaitBetweenReconnectionRetries() const;
|
||||||
const std::vector<std::string>& getSubProtocols();
|
const std::vector<std::string>& getSubProtocols();
|
||||||
|
|
||||||
|
void setAutoThreadName(bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
WebSocketSendInfo sendMessage(const IXWebSocketSendData& message,
|
WebSocketSendInfo sendMessage(const IXWebSocketSendData& message,
|
||||||
SendMessageKind sendMessageKind,
|
SendMessageKind sendMessageKind,
|
||||||
@ -182,6 +184,9 @@ namespace ix
|
|||||||
// Subprotocols
|
// Subprotocols
|
||||||
std::vector<std::string> _subProtocols;
|
std::vector<std::string> _subProtocols;
|
||||||
|
|
||||||
|
// enable or disable auto set thread name
|
||||||
|
bool _autoThreadName;
|
||||||
|
|
||||||
friend class WebSocketServer;
|
friend class WebSocketServer;
|
||||||
};
|
};
|
||||||
} // namespace ix
|
} // namespace ix
|
||||||
|
@ -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
|
||||||
|
@ -91,6 +91,9 @@ namespace ix
|
|||||||
setThreadName("Srv:ws:" + connectionState->getId());
|
setThreadName("Srv:ws:" + connectionState->getId());
|
||||||
|
|
||||||
auto webSocket = std::make_shared<WebSocket>();
|
auto webSocket = std::make_shared<WebSocket>();
|
||||||
|
|
||||||
|
webSocket->setAutoThreadName(false);
|
||||||
|
|
||||||
if (_onConnectionCallback)
|
if (_onConnectionCallback)
|
||||||
{
|
{
|
||||||
_onConnectionCallback(webSocket, connectionState);
|
_onConnectionCallback(webSocket, connectionState);
|
||||||
|
@ -1174,7 +1174,22 @@ namespace ix
|
|||||||
{
|
{
|
||||||
_requestInitCancellation = true;
|
_requestInitCancellation = true;
|
||||||
|
|
||||||
if (_readyState == ReadyState::CLOSING || _readyState == ReadyState::CLOSED) return;
|
if (_readyState == ReadyState::CLOSING || _readyState == ReadyState::CLOSED)
|
||||||
|
{
|
||||||
|
// Wake up the socket polling thread, as
|
||||||
|
// Socket::isReadyToRead() might be still waiting the
|
||||||
|
// interrupt event to happen.
|
||||||
|
bool wakeUpPoll = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_socketMutex);
|
||||||
|
wakeUpPoll = (_socket && _socket->isWakeUpFromPollSupported());
|
||||||
|
}
|
||||||
|
if (wakeUpPoll)
|
||||||
|
{
|
||||||
|
wakeUpFromPoll(SelectInterrupt::kCloseRequest);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (closeWireSize == 0)
|
if (closeWireSize == 0)
|
||||||
{
|
{
|
||||||
|
@ -60,6 +60,7 @@ TEST_CASE("http server", "[httpd]")
|
|||||||
REQUIRE(response->errorCode == HttpErrorCode::Ok);
|
REQUIRE(response->errorCode == HttpErrorCode::Ok);
|
||||||
REQUIRE(response->statusCode == 200);
|
REQUIRE(response->statusCode == 200);
|
||||||
REQUIRE(response->headers["Accept-Encoding"] == "gzip");
|
REQUIRE(response->headers["Accept-Encoding"] == "gzip");
|
||||||
|
REQUIRE(response->headers["Content-Encoding"] == "gzip");
|
||||||
|
|
||||||
server.stop();
|
server.stop();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user