keep alive setting for server and websocket improvements
This commit is contained in:
@@ -53,7 +53,7 @@ void ClientConnection::responseFinished(std::error_code ec)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (true) // Connection: Keep
|
if (m_webserver.connectionKeepAlive())
|
||||||
{
|
{
|
||||||
// ESP_LOGD(TAG, "state changed to RequestLine");
|
// ESP_LOGD(TAG, "state changed to RequestLine");
|
||||||
m_state = State::RequestLine;
|
m_state = State::RequestLine;
|
||||||
|
@@ -18,6 +18,8 @@ public:
|
|||||||
Webserver(asio::io_context& io_context, unsigned short port);
|
Webserver(asio::io_context& io_context, unsigned short port);
|
||||||
virtual ~Webserver() = default;
|
virtual ~Webserver() = default;
|
||||||
|
|
||||||
|
virtual bool connectionKeepAlive() const = 0;
|
||||||
|
|
||||||
virtual std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) = 0;
|
virtual std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
// 3rdparty lib includes
|
// 3rdparty lib includes
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <asio_webserver/clientconnection.h>
|
#include <asio_webserver/clientconnection.h>
|
||||||
|
#include <asio_webserver/webserver.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
@@ -15,13 +16,13 @@ constexpr const char * const TAG = "ASIO_WEBSERVER";
|
|||||||
ChunkedResponseHandler::ChunkedResponseHandler(ClientConnection &clientConnection) :
|
ChunkedResponseHandler::ChunkedResponseHandler(ClientConnection &clientConnection) :
|
||||||
m_clientConnection{clientConnection}
|
m_clientConnection{clientConnection}
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "constructed for (%s:%hi)",
|
ESP_LOGV(TAG, "constructed for (%s:%hi)",
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
}
|
}
|
||||||
|
|
||||||
ChunkedResponseHandler::~ChunkedResponseHandler()
|
ChunkedResponseHandler::~ChunkedResponseHandler()
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "destructed for (%s:%hi)",
|
ESP_LOGV(TAG, "destructed for (%s:%hi)",
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +40,11 @@ void ChunkedResponseHandler::sendResponse()
|
|||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: {}\r\n"
|
||||||
"Content-Type: text/html\r\n"
|
"Content-Type: text/html\r\n"
|
||||||
"Transfer-Encoding: chunked\r\n"
|
"Transfer-Encoding: chunked\r\n"
|
||||||
"\r\n");
|
"\r\n",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close");
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
asio::async_write(m_clientConnection.socket(),
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
// 3rdparty lib includes
|
// 3rdparty lib includes
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <asio_webserver/clientconnection.h>
|
#include <asio_webserver/clientconnection.h>
|
||||||
|
#include <asio_webserver/webserver.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
@@ -114,10 +115,12 @@ void DebugResponseHandler::sendResponse()
|
|||||||
"</html>";
|
"</html>";
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
||||||
|
"Connection: {}\r\n"
|
||||||
"Content-Type: text/html\r\n"
|
"Content-Type: text/html\r\n"
|
||||||
"Content-Length: {}\r\n"
|
"Content-Length: {}\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"{}",
|
"{}",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close",
|
||||||
m_response.size(), m_response);
|
m_response.size(), m_response);
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
asio::async_write(m_clientConnection.socket(),
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
// 3rdparty lib includes
|
// 3rdparty lib includes
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <asio_webserver/clientconnection.h>
|
#include <asio_webserver/clientconnection.h>
|
||||||
|
#include <asio_webserver/webserver.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
@@ -42,11 +43,13 @@ void ErrorResponseHandler::sendResponse()
|
|||||||
m_response = fmt::format("Error 404 Not Found: {}", m_path);
|
m_response = fmt::format("Error 404 Not Found: {}", m_path);
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 404 Not Found\r\n"
|
m_response = fmt::format("HTTP/1.1 404 Not Found\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: {}\r\n"
|
||||||
"Content-Type: text/plain\r\n"
|
"Content-Type: text/plain\r\n"
|
||||||
"Content-Length: {}\r\n"
|
"Content-Length: {}\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"{}", m_response.size(), m_response);
|
"{}",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close",
|
||||||
|
m_response.size(), m_response);
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
asio::async_write(m_clientConnection.socket(),
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
@@ -8,8 +8,7 @@
|
|||||||
#include "debugresponsehandler.h"
|
#include "debugresponsehandler.h"
|
||||||
#include "chunkedresponsehandler.h"
|
#include "chunkedresponsehandler.h"
|
||||||
#include "errorresponsehandler.h"
|
#include "errorresponsehandler.h"
|
||||||
#include "websocketfrontendresponsehandler.h"
|
#include "websocketresponsehandler.h"
|
||||||
#include "websocketbackendresponsehandler.h"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
@@ -31,10 +30,8 @@ std::unique_ptr<ResponseHandler> ExampleWebserver::makeResponseHandler(ClientCon
|
|||||||
return std::make_unique<DebugResponseHandler>(clientConnection, method, path, protocol);
|
return std::make_unique<DebugResponseHandler>(clientConnection, method, path, protocol);
|
||||||
else if (processedPath == "/chunked")
|
else if (processedPath == "/chunked")
|
||||||
return std::make_unique<ChunkedResponseHandler>(clientConnection);
|
return std::make_unique<ChunkedResponseHandler>(clientConnection);
|
||||||
else if (processedPath == "/websocket")
|
|
||||||
return std::make_unique<WebsocketFrontendResponseHandler>(clientConnection);
|
|
||||||
else if (processedPath == "/ws")
|
else if (processedPath == "/ws")
|
||||||
return std::make_unique<WebsocketBackendResponseHandler>(clientConnection);
|
return std::make_unique<WebsocketResponseHandler>(clientConnection);
|
||||||
else
|
else
|
||||||
return std::make_unique<ErrorResponseHandler>(clientConnection, path);
|
return std::make_unique<ErrorResponseHandler>(clientConnection, path);
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,12 @@
|
|||||||
// 3rdparty lib includes
|
// 3rdparty lib includes
|
||||||
#include <asio_webserver/webserver.h>
|
#include <asio_webserver/webserver.h>
|
||||||
|
|
||||||
class ExampleWebserver : public Webserver
|
class ExampleWebserver final : public Webserver
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using Webserver::Webserver;
|
using Webserver::Webserver;
|
||||||
|
|
||||||
|
bool connectionKeepAlive() const final { return true; }
|
||||||
|
|
||||||
std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) final;
|
std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) final;
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
// 3rdparty lib includes
|
// 3rdparty lib includes
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <asio_webserver/clientconnection.h>
|
#include <asio_webserver/clientconnection.h>
|
||||||
|
#include <asio_webserver/webserver.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
@@ -47,17 +48,19 @@ void RootResponseHandler::sendResponse()
|
|||||||
"<ul>"
|
"<ul>"
|
||||||
"<li><a href=\"/debug\">Debug</a></li>"
|
"<li><a href=\"/debug\">Debug</a></li>"
|
||||||
"<li><a href=\"/chunked\">Chunked</a></li>"
|
"<li><a href=\"/chunked\">Chunked</a></li>"
|
||||||
"<li><a href=\"/websocket\">Websocket</a></li>"
|
"<li><a href=\"/ws\">WebSocket</a></li>"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
"</body>"
|
"</body>"
|
||||||
"</html>";
|
"</html>";
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: {}\r\n"
|
||||||
"Content-Type: text/html\r\n"
|
"Content-Type: text/html\r\n"
|
||||||
"Content-Length: {}\r\n"
|
"Content-Length: {}\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"{}", m_response.size(), m_response);
|
"{}",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close",
|
||||||
|
m_response.size(), m_response);
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
asio::async_write(m_clientConnection.socket(),
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
@@ -10,8 +10,7 @@ HEADERS += \
|
|||||||
errorresponsehandler.h \
|
errorresponsehandler.h \
|
||||||
examplewebserver.h \
|
examplewebserver.h \
|
||||||
rootresponsehandler.h \
|
rootresponsehandler.h \
|
||||||
websocketbackendresponsehandler.h \
|
websocketresponsehandler.h
|
||||||
websocketfrontendresponsehandler.h
|
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
chunkedresponsehandler.cpp \
|
chunkedresponsehandler.cpp \
|
||||||
@@ -20,8 +19,7 @@ SOURCES += \
|
|||||||
examplewebserver.cpp \
|
examplewebserver.cpp \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
rootresponsehandler.cpp \
|
rootresponsehandler.cpp \
|
||||||
websocketbackendresponsehandler.cpp \
|
websocketresponsehandler.cpp
|
||||||
websocketfrontendresponsehandler.cpp
|
|
||||||
|
|
||||||
unix: TARGET=webserver_example.bin
|
unix: TARGET=webserver_example.bin
|
||||||
DESTDIR=$${OUT_PWD}/..
|
DESTDIR=$${OUT_PWD}/..
|
||||||
|
@@ -1,153 +0,0 @@
|
|||||||
#include "websocketbackendresponsehandler.h"
|
|
||||||
|
|
||||||
// system includes
|
|
||||||
#include <openssl/sha.h>
|
|
||||||
|
|
||||||
// esp-idf includes
|
|
||||||
#include <asio.hpp>
|
|
||||||
#include <esp_log.h>
|
|
||||||
|
|
||||||
// 3rdparty lib includes
|
|
||||||
#include <fmt/core.h>
|
|
||||||
#include <asio_webserver/clientconnection.h>
|
|
||||||
#include <strutils.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
WebsocketBackendResponseHandler::WebsocketBackendResponseHandler(ClientConnection &clientConnection) :
|
|
||||||
m_clientConnection{clientConnection}
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "constructed for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
}
|
|
||||||
|
|
||||||
WebsocketBackendResponseHandler::~WebsocketBackendResponseHandler()
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "destructed for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketBackendResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
|
|
||||||
{
|
|
||||||
// ESP_LOGV(TAG, "key=\"%.*s\" value=\"%.*s\"", key.size(), key.data(), value.size(), value.data());
|
|
||||||
|
|
||||||
if (cpputils::stringEqualsIgnoreCase(key, "Connection"))
|
|
||||||
{
|
|
||||||
m_connectionUpgrade = cpputils::stringEqualsIgnoreCase(value, "Upgrade") ||
|
|
||||||
value.contains("Upgrade");
|
|
||||||
}
|
|
||||||
else if (cpputils::stringEqualsIgnoreCase(key, "Upgrade"))
|
|
||||||
{
|
|
||||||
m_upgradeWebsocket = cpputils::stringEqualsIgnoreCase(value, "websocket");
|
|
||||||
}
|
|
||||||
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Version"))
|
|
||||||
{
|
|
||||||
m_secWebsocketVersion = value;
|
|
||||||
}
|
|
||||||
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Key"))
|
|
||||||
{
|
|
||||||
m_secWebsocketKey = value;
|
|
||||||
}
|
|
||||||
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Extensions"))
|
|
||||||
{
|
|
||||||
m_secWebsocketExtensions = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketBackendResponseHandler::requestBodyReceived(std::string_view body)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketBackendResponseHandler::sendResponse()
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "sending response for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
|
|
||||||
const auto sendErrorResponse = [&](std::string_view message){
|
|
||||||
ESP_LOGW(TAG, "%.*s", message.size(), message.data());
|
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 400 Bad Request\r\n"
|
|
||||||
"Connection: keep-alive\r\n"
|
|
||||||
"Content-Type: text/plain\r\n"
|
|
||||||
"Content-Length: {}\r\n"
|
|
||||||
"\r\n"
|
|
||||||
"{}", message.size(), message);
|
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
|
||||||
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
|
||||||
{ writtenError(ec, length); });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!m_connectionUpgrade)
|
|
||||||
{
|
|
||||||
sendErrorResponse("Only websocket clients are allowed on this endpoint (header missing Connection: Upgrade)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_upgradeWebsocket)
|
|
||||||
{
|
|
||||||
sendErrorResponse("Only websocket clients are allowed on this endpoint (header missing Upgrade: websocket)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_secWebsocketKey.empty())
|
|
||||||
{
|
|
||||||
sendErrorResponse("Only websocket clients are allowed on this endpoint (header missing Sec-WebSocket-Key)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr std::string_view magic_uuid{"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"};
|
|
||||||
m_secWebsocketKey.append(magic_uuid);
|
|
||||||
|
|
||||||
unsigned char hash[SHA_DIGEST_LENGTH]; // == 20
|
|
||||||
SHA1((const unsigned char *)m_secWebsocketKey.data(), m_secWebsocketKey.size(), hash);
|
|
||||||
|
|
||||||
const auto base64Hash = cpputils::toBase64String({hash, SHA_DIGEST_LENGTH});
|
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 101 Switching Protocols\r\n"
|
|
||||||
"Upgrade: websocket\r\n"
|
|
||||||
"Connection: Upgrade\r\n"
|
|
||||||
"Sec-WebSocket-Accept: {}\r\n"
|
|
||||||
"Sec-WebSocket-Protocol: chat\r\n"
|
|
||||||
"\r\n", base64Hash);
|
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
|
||||||
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
|
||||||
{ written(ec, length); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketBackendResponseHandler::writtenError(std::error_code ec, std::size_t length)
|
|
||||||
{
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
m_clientConnection.responseFinished(ec);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
|
|
||||||
m_clientConnection.responseFinished(ec);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketBackendResponseHandler::written(std::error_code ec, std::size_t length)
|
|
||||||
{
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
m_clientConnection.responseFinished(ec);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
|
|
||||||
m_clientConnection.upgradeWebsocket();
|
|
||||||
}
|
|
@@ -1,170 +0,0 @@
|
|||||||
#include "websocketfrontendresponsehandler.h"
|
|
||||||
|
|
||||||
// esp-idf includes
|
|
||||||
#include <asio.hpp>
|
|
||||||
#include <esp_log.h>
|
|
||||||
|
|
||||||
// 3rdparty lib includes
|
|
||||||
#include <fmt/core.h>
|
|
||||||
#include <asio_webserver/clientconnection.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
WebsocketFrontendResponseHandler::WebsocketFrontendResponseHandler(ClientConnection &clientConnection) :
|
|
||||||
m_clientConnection{clientConnection}
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "constructed for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
}
|
|
||||||
|
|
||||||
WebsocketFrontendResponseHandler::~WebsocketFrontendResponseHandler()
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "destructed for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketFrontendResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketFrontendResponseHandler::requestBodyReceived(std::string_view body)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketFrontendResponseHandler::sendResponse()
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "sending response for (%s:%hi)",
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
|
|
||||||
m_response = R"END(
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Websocket test</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Websocket test</h1>
|
|
||||||
|
|
||||||
<div style="border: 1px solid black;">
|
|
||||||
<button id="connectButton">Connect</button>
|
|
||||||
<span id="statusSpan">Not connected</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="sendForm">
|
|
||||||
<fieldset>
|
|
||||||
<legend>Send msg</legend>
|
|
||||||
<input type="text" id="sendInput" />
|
|
||||||
<button type="submit">Send</button>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<pre id="logOutput"></pre>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var websocket = null;
|
|
||||||
|
|
||||||
const connectButton = document.getElementById('connectButton');
|
|
||||||
const statusSpan = document.getElementById('statusSpan');
|
|
||||||
const sendForm = document.getElementById('sendForm');
|
|
||||||
const sendInput = document.getElementById('sendInput');
|
|
||||||
const logOutput = document.getElementById('logOutput');
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function(event) {
|
|
||||||
console.log('loaded');
|
|
||||||
connectButton.addEventListener('click', connectButtonClicked);
|
|
||||||
sendForm.addEventListener('submit', sendMsg);
|
|
||||||
});
|
|
||||||
|
|
||||||
function logLine(msg) {
|
|
||||||
logOutput.appendChild(document.createTextNode(msg + "\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectButtonClicked() {
|
|
||||||
console.log('clicked');
|
|
||||||
if (websocket === null) {
|
|
||||||
connectButton.textContent = 'Disconnect';
|
|
||||||
|
|
||||||
const url = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws';
|
|
||||||
|
|
||||||
logLine('Connecting to ' + url);
|
|
||||||
|
|
||||||
statusSpan.textContent = "Connecting...";
|
|
||||||
|
|
||||||
websocket = new WebSocket(url);
|
|
||||||
websocket.onopen = function (event) {
|
|
||||||
statusSpan.textContent = "Connected";
|
|
||||||
logLine('Connected');
|
|
||||||
};
|
|
||||||
websocket.onclose = function(event) {
|
|
||||||
statusSpan.textContent = "Lost connection";
|
|
||||||
logLine('Lost connection');
|
|
||||||
};
|
|
||||||
websocket.onerror = function(event) {
|
|
||||||
statusSpan.textContent = "Error occured";
|
|
||||||
logLine('Error occured');
|
|
||||||
};
|
|
||||||
websocket.onmessage = function(event) {
|
|
||||||
if (typeof event.data === 'string' || event.data instanceof String) {
|
|
||||||
logLine('Received text message: ' + event.data);
|
|
||||||
} else if (typeof event.data == 'object') {
|
|
||||||
logLine('Received binary message');
|
|
||||||
} else {
|
|
||||||
logLine('Received unknown message');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
connectButton.textContent = 'Connect';
|
|
||||||
|
|
||||||
websocket.close();
|
|
||||||
websocket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMsg(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (websocket === null) {
|
|
||||||
alert('not connected!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
websocket.send(sendInput.value);
|
|
||||||
logLine('Sent text message: ' + sendInput.value);
|
|
||||||
sendInput.value = '';
|
|
||||||
sendInput.focus();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)END";
|
|
||||||
|
|
||||||
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
|
||||||
"Connection: keep-alive\r\n"
|
|
||||||
"Content-Type: text/html\r\n"
|
|
||||||
"Content-Length: {}\r\n"
|
|
||||||
"\r\n"
|
|
||||||
"{}", m_response.size(), m_response);
|
|
||||||
|
|
||||||
asio::async_write(m_clientConnection.socket(),
|
|
||||||
asio::buffer(m_response.data(), m_response.size()),
|
|
||||||
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
|
||||||
{ written(ec, length); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebsocketFrontendResponseHandler::written(std::error_code ec, std::size_t length)
|
|
||||||
{
|
|
||||||
if (ec)
|
|
||||||
{
|
|
||||||
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
m_clientConnection.responseFinished(ec);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
|
||||||
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
|
||||||
|
|
||||||
m_clientConnection.responseFinished(ec);
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// system includes
|
|
||||||
#include <string_view>
|
|
||||||
#include <string>
|
|
||||||
#include <system_error>
|
|
||||||
|
|
||||||
// 3rdparty lib includes
|
|
||||||
#include <asio_webserver/responsehandler.h>
|
|
||||||
|
|
||||||
// forward declarations
|
|
||||||
class ClientConnection;
|
|
||||||
|
|
||||||
class WebsocketFrontendResponseHandler final : public ResponseHandler
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
WebsocketFrontendResponseHandler(ClientConnection &clientConnection);
|
|
||||||
~WebsocketFrontendResponseHandler() override;
|
|
||||||
|
|
||||||
void requestHeaderReceived(std::string_view key, std::string_view value) final;
|
|
||||||
void requestBodyReceived(std::string_view body) final;
|
|
||||||
void sendResponse() final;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void written(std::error_code ec, std::size_t length);
|
|
||||||
|
|
||||||
ClientConnection &m_clientConnection;
|
|
||||||
|
|
||||||
std::string m_response;
|
|
||||||
};
|
|
291
test/webserver_example/websocketresponsehandler.cpp
Normal file
291
test/webserver_example/websocketresponsehandler.cpp
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
#include "websocketresponsehandler.h"
|
||||||
|
|
||||||
|
// system includes
|
||||||
|
#include <openssl/sha.h>
|
||||||
|
|
||||||
|
// esp-idf includes
|
||||||
|
#include <asio.hpp>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
|
// 3rdparty lib includes
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <asio_webserver/clientconnection.h>
|
||||||
|
#include <asio_webserver/webserver.h>
|
||||||
|
#include <strutils.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char * const TAG = "ASIO_WEBSERVER";
|
||||||
|
|
||||||
|
constexpr std::string_view html{R"END(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Websocket test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Websocket test</h1>
|
||||||
|
|
||||||
|
<form id="connectForm">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Connection</legend>
|
||||||
|
<input type="url" id="urlInput" required />
|
||||||
|
<button id="connectButton" type="submit">Connect</button>
|
||||||
|
<span id="statusSpan">Not connected</span>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="sendForm">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Send msg</legend>
|
||||||
|
<input type="text" id="sendInput" />
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<pre id="logOutput"></pre>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var websocket = null;
|
||||||
|
|
||||||
|
const connectForm = document.getElementById('connectForm');
|
||||||
|
const urlInput = document.getElementById('urlInput');
|
||||||
|
const connectButton = document.getElementById('connectButton');
|
||||||
|
const statusSpan = document.getElementById('statusSpan');
|
||||||
|
const sendForm = document.getElementById('sendForm');
|
||||||
|
const sendInput = document.getElementById('sendInput');
|
||||||
|
const logOutput = document.getElementById('logOutput');
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
urlInput.value = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws'
|
||||||
|
|
||||||
|
connectForm.addEventListener('submit', connectWebsocket);
|
||||||
|
sendForm.addEventListener('submit', sendMsg);
|
||||||
|
});
|
||||||
|
|
||||||
|
function logLine(msg) {
|
||||||
|
logOutput.appendChild(document.createTextNode(msg + "\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebsocket(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (websocket === null) {
|
||||||
|
const url = urlInput.value;
|
||||||
|
|
||||||
|
logLine('Connecting to ' + url);
|
||||||
|
|
||||||
|
statusSpan.textContent = "Connecting...";
|
||||||
|
|
||||||
|
websocket = new WebSocket(url);
|
||||||
|
websocket.onopen = function (event) {
|
||||||
|
statusSpan.textContent = "Connected";
|
||||||
|
logLine('Connected');
|
||||||
|
};
|
||||||
|
websocket.onclose = function(event) {
|
||||||
|
statusSpan.textContent = "Lost connection";
|
||||||
|
logLine('Lost connection');
|
||||||
|
};
|
||||||
|
websocket.onerror = function(event) {
|
||||||
|
statusSpan.textContent = "Error occured";
|
||||||
|
logLine('Error occured');
|
||||||
|
};
|
||||||
|
websocket.onmessage = function(event) {
|
||||||
|
if (typeof event.data === 'string' || event.data instanceof String) {
|
||||||
|
logLine('Received text message: ' + event.data);
|
||||||
|
} else if (typeof event.data == 'object') {
|
||||||
|
logLine('Received binary message');
|
||||||
|
} else {
|
||||||
|
logLine('Received unknown message');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connectButton.textContent = 'Disconnect';
|
||||||
|
urlInput.readOnly = true;
|
||||||
|
} else {
|
||||||
|
connectButton.textContent = 'Connect';
|
||||||
|
urlInput.readOnly = false;
|
||||||
|
|
||||||
|
websocket.close();
|
||||||
|
websocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMsg(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (websocket === null) {
|
||||||
|
alert('not connected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.send(sendInput.value);
|
||||||
|
logLine('Sent text message: ' + sendInput.value);
|
||||||
|
sendInput.value = '';
|
||||||
|
sendInput.focus();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)END"};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WebsocketResponseHandler::WebsocketResponseHandler(ClientConnection &clientConnection) :
|
||||||
|
m_clientConnection{clientConnection}
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "constructed for (%s:%hi)",
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
}
|
||||||
|
|
||||||
|
WebsocketResponseHandler::~WebsocketResponseHandler()
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "destructed for (%s:%hi)",
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
|
||||||
|
{
|
||||||
|
// ESP_LOGV(TAG, "key=\"%.*s\" value=\"%.*s\"", key.size(), key.data(), value.size(), value.data());
|
||||||
|
|
||||||
|
if (cpputils::stringEqualsIgnoreCase(key, "Connection"))
|
||||||
|
{
|
||||||
|
m_connectionUpgrade = cpputils::stringEqualsIgnoreCase(value, "Upgrade") ||
|
||||||
|
value.contains("Upgrade");
|
||||||
|
}
|
||||||
|
else if (cpputils::stringEqualsIgnoreCase(key, "Upgrade"))
|
||||||
|
{
|
||||||
|
m_upgradeWebsocket = cpputils::stringEqualsIgnoreCase(value, "websocket");
|
||||||
|
}
|
||||||
|
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Version"))
|
||||||
|
{
|
||||||
|
m_secWebsocketVersion = value;
|
||||||
|
}
|
||||||
|
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Key"))
|
||||||
|
{
|
||||||
|
m_secWebsocketKey = value;
|
||||||
|
}
|
||||||
|
else if (cpputils::stringEqualsIgnoreCase(key, "Sec-WebSocket-Extensions"))
|
||||||
|
{
|
||||||
|
m_secWebsocketExtensions = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::requestBodyReceived(std::string_view body)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::sendResponse()
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "sending response for (%s:%hi)",
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
|
||||||
|
if (!m_connectionUpgrade || !m_upgradeWebsocket)
|
||||||
|
{
|
||||||
|
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
|
||||||
|
"Connection: {}\r\n"
|
||||||
|
"Content-Type: text/html\r\n"
|
||||||
|
"Content-Length: {}\r\n"
|
||||||
|
"\r\n",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close",
|
||||||
|
html.size());
|
||||||
|
|
||||||
|
asio::async_write(m_clientConnection.socket(),
|
||||||
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
||||||
|
{ writtenHtmlHeader(ec, length); });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto showError = [&](std::string_view msg){
|
||||||
|
m_response = fmt::format("HTTP/1.1 400 Bad Request\r\n"
|
||||||
|
"Connection: {}\r\n"
|
||||||
|
"Content-Type: text/html\r\n"
|
||||||
|
"Content-Length: {}\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"{}",
|
||||||
|
m_clientConnection.webserver().connectionKeepAlive() ? "keep-alive" : "close",
|
||||||
|
msg.size(), msg);
|
||||||
|
|
||||||
|
asio::async_write(m_clientConnection.socket(),
|
||||||
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
||||||
|
{ writtenHtml(ec, length); });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (m_secWebsocketKey.empty())
|
||||||
|
{
|
||||||
|
showError("Header Sec-WebSocket-Key empty or missing!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string_view magic_uuid{"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"};
|
||||||
|
m_secWebsocketKey.append(magic_uuid);
|
||||||
|
|
||||||
|
unsigned char sha1[SHA_DIGEST_LENGTH]; // == 20
|
||||||
|
SHA1((const unsigned char *)m_secWebsocketKey.data(), m_secWebsocketKey.size(), sha1);
|
||||||
|
|
||||||
|
const auto base64Sha1 = cpputils::toBase64String({sha1, SHA_DIGEST_LENGTH});
|
||||||
|
|
||||||
|
m_response = fmt::format("HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
"Upgrade: websocket\r\n"
|
||||||
|
"Connection: Upgrade\r\n"
|
||||||
|
"Sec-WebSocket-Accept: {}\r\n"
|
||||||
|
"Sec-WebSocket-Protocol: chat\r\n"
|
||||||
|
"\r\n", base64Sha1);
|
||||||
|
|
||||||
|
asio::async_write(m_clientConnection.socket(),
|
||||||
|
asio::buffer(m_response.data(), m_response.size()),
|
||||||
|
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
||||||
|
{ writtenWebsocket(ec, length); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::writtenHtmlHeader(std::error_code ec, std::size_t length)
|
||||||
|
{
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
m_clientConnection.responseFinished(ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
|
||||||
|
asio::async_write(m_clientConnection.socket(),
|
||||||
|
asio::buffer(html.data(), html.size()),
|
||||||
|
[this, self=m_clientConnection.shared_from_this()](std::error_code ec, std::size_t length)
|
||||||
|
{ writtenHtml(ec, length); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::writtenHtml(std::error_code ec, std::size_t length)
|
||||||
|
{
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
m_clientConnection.responseFinished(ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
|
||||||
|
m_clientConnection.responseFinished(ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebsocketResponseHandler::writtenWebsocket(std::error_code ec, std::size_t length)
|
||||||
|
{
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
m_clientConnection.responseFinished(ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "expected=%zd actual=%zd for (%s:%hi)", m_response.size(), length,
|
||||||
|
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
|
||||||
|
|
||||||
|
m_clientConnection.responseFinished(ec);
|
||||||
|
}
|
@@ -13,19 +13,20 @@
|
|||||||
// forward declarations
|
// forward declarations
|
||||||
class ClientConnection;
|
class ClientConnection;
|
||||||
|
|
||||||
class WebsocketBackendResponseHandler final : public ResponseHandler
|
class WebsocketResponseHandler final : public ResponseHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WebsocketBackendResponseHandler(ClientConnection &clientConnection);
|
WebsocketResponseHandler(ClientConnection &clientConnection);
|
||||||
~WebsocketBackendResponseHandler() override;
|
~WebsocketResponseHandler() override;
|
||||||
|
|
||||||
void requestHeaderReceived(std::string_view key, std::string_view value) final;
|
void requestHeaderReceived(std::string_view key, std::string_view value) final;
|
||||||
void requestBodyReceived(std::string_view body) final;
|
void requestBodyReceived(std::string_view body) final;
|
||||||
void sendResponse() final;
|
void sendResponse() final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void writtenError(std::error_code ec, std::size_t length);
|
void writtenHtmlHeader(std::error_code ec, std::size_t length);
|
||||||
void written(std::error_code ec, std::size_t length);
|
void writtenHtml(std::error_code ec, std::size_t length);
|
||||||
|
void writtenWebsocket(std::error_code ec, std::size_t length);
|
||||||
|
|
||||||
ClientConnection &m_clientConnection;
|
ClientConnection &m_clientConnection;
|
||||||
|
|
Reference in New Issue
Block a user