Add initial webserver sources

This commit is contained in:
2022-06-30 01:35:02 +02:00
parent 2721be2d22
commit 7b93dfa6cf
26 changed files with 1018 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.qmake.stash
Makefile
build-*/
*.o
moc_predefs.h
target_wrapper.sh
*.moc

43
CMakeLists.txt Normal file
View File

@ -0,0 +1,43 @@
set(headers
src/asio_webserver/clientconnection.h
src/asio_webserver/responsehandler.h
src/asio_webserver/webserver.h
)
set(sources
src/asio_webserver/clientconnection.cpp
src/asio_webserver/responsehandler.cpp
src/asio_webserver/webserver.cpp
)
set(dependencies
asio
log
cpputils
espchrono
espcpputils
date
espchrono
expected
fmt
)
idf_component_register(
INCLUDE_DIRS
src
SRCS
${headers}
${sources}
REQUIRES
${dependencies}
)
target_compile_options(${COMPONENT_TARGET}
PRIVATE
-fstack-reuse=all
-fstack-protector-all
-Wno-unused-function
-Wno-deprecated-declarations
-Wno-missing-field-initializers
-Wno-parentheses
)

1
asio_webserver.pri Normal file
View File

@ -0,0 +1 @@
INCLUDEPATH += $$PWD/src

9
asio_webserver_src.pri Normal file
View File

@ -0,0 +1,9 @@
HEADERS += \
$$PWD/src/asio_webserver/clientconnection.h \
$$PWD/src/asio_webserver/responsehandler.h \
$$PWD/src/asio_webserver/webserver.h
SOURCES += \
$$PWD/src/asio_webserver/clientconnection.cpp \
$$PWD/src/asio_webserver/responsehandler.cpp \
$$PWD/src/asio_webserver/webserver.cpp

View File

@ -0,0 +1,220 @@
#include "clientconnection.h"
#include <cstdio>
#include <utility>
#include <esp_log.h>
#include "webserver.h"
#include "responsehandler.h"
namespace {
constexpr const char * const TAG = "ASIO_WEBSERVER";
} // namespace
ClientConnection::ClientConnection(Webserver &webserver, asio::ip::tcp::socket socket) :
m_webserver{webserver},
m_socket{std::move(socket)},
m_remote_endpoint{m_socket.remote_endpoint()}
{
ESP_LOGI(TAG, "new client (%s:%hi)",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
}
ClientConnection::~ClientConnection()
{
ESP_LOGI(TAG, "client destroyed (%s:%hi)",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
}
void ClientConnection::start()
{
doRead();
}
void ClientConnection::responseFinished(std::error_code ec)
{
if (ec)
{
ESP_LOGW(TAG, "error: %i (%s:%hi)", ec.value(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return;
}
if constexpr (false) // Connection: Keep
{
// ESP_LOGD(TAG, "state changed to RequestLine");
m_state = State::RequestLine;
doRead();
}
else
m_socket.close();
}
void ClientConnection::doRead()
{
m_socket.async_read_some(asio::buffer(m_receiveBuffer, max_length),
[this, self=shared_from_this()](std::error_code ec, std::size_t length)
{ readyRead(ec, length); });
}
void ClientConnection::readyRead(std::error_code ec, std::size_t length)
{
if (ec)
{
ESP_LOGI(TAG, "error: %i (%s:%hi)", ec.value(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
return;
}
// ESP_LOGV(TAG, "received: %zd \"%.*s\"", length, length, m_data);
m_parsingBuffer.append(m_receiveBuffer, length);
bool shouldDoRead{true};
while (true)
{
constexpr std::string_view newLine{"\r\n"};
const auto index = m_parsingBuffer.find(newLine.data(), 0, newLine.size());
if (index == std::string::npos)
break;
std::string_view line{m_parsingBuffer.data(), index};
// ESP_LOGD(TAG, "line: %zd \"%.*s\"", line.size(), line.size(), line.data());
if (!readyReadLine(line))
shouldDoRead = false;
m_parsingBuffer.erase(std::begin(m_parsingBuffer), std::next(std::begin(m_parsingBuffer), line.size() + newLine.size()));
}
if (shouldDoRead)
doRead();
}
bool ClientConnection::readyReadLine(std::string_view line)
{
switch (m_state)
{
case State::RequestLine:
// ESP_LOGV(TAG, "case State::RequestLine:");
return parseRequestLine(line);
case State::RequestHeaders:
// ESP_LOGV(TAG, "case State::RequestHeaders:");
return parseRequestHeader(line);
case State::RequestBody:
// ESP_LOGV(TAG, "case State::RequestBody:");
ESP_LOGW(TAG, "unexpected state=RequestBody (%s:%hi)",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
return true;
case State::Response:
// ESP_LOGV(TAG, "case State::Response:");
ESP_LOGW(TAG, "unexpected state=Response (%s:%hi)",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
return true;
default:
ESP_LOGW(TAG, "unknown state %i (%s:%hi)", std::to_underlying(m_state),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
return true;
}
}
bool ClientConnection::parseRequestLine(std::string_view line)
{
if (const auto index = line.find(' '); index == std::string::npos)
{
ESP_LOGW(TAG, "invalid request line (1): \"%.*s\" (%s:%hi)", line.size(), line.data(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
else
{
const std::string_view method { line.data(), index };
// ESP_LOGV(TAG, "request method: %zd \"%.*s\"", method.size(), method.size(), method.data());
if (const auto index2 = line.find(' ', index + 1); index2 == std::string::npos)
{
ESP_LOGW(TAG, "invalid request line (2): \"%.*s\" (%s:%hi)", line.size(), line.data(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
else
{
const std::string_view path { line.data() + index + 1, line.data() + index2 };
// ESP_LOGV(TAG, "request path: %zd \"%.*s\"", path.size(), path.size(), path.data());
const std::string_view protocol { line.cbegin() + index2 + 1, line.cend() };
// ESP_LOGV(TAG, "request protocol: %zd \"%.*s\"", protocol.size(), protocol.size(), protocol.data());
m_responseHandler = m_webserver.makeResponseHandler(*this, method, path, protocol);
if (!m_responseHandler)
{
ESP_LOGW(TAG, "invalid response handler method=\"%.*s\" path=\"%.*s\" protocol=\"%.*s\" (%s:%hi)",
method.size(), method.data(), path.size(), path.data(), protocol.size(), protocol.data(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
// ESP_LOGV(TAG, "state changed to RequestHeaders");
m_state = State::RequestHeaders;
return true;
}
}
}
bool ClientConnection::parseRequestHeader(std::string_view line)
{
if (!line.empty())
{
constexpr std::string_view sep{": "};
if (const auto index = line.find(sep.data(), 0, sep.size()); index == std::string_view::npos)
{
ESP_LOGW(TAG, "invalid request header: %zd \"%.*s\" (%s:%hi)", line.size(), line.size(), line.data(),
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
else
{
std::string_view key{line.data(), index};
std::string_view value{std::begin(line) + index + sep.size(), std::end(line)};
// ESP_LOGD(TAG, "header key=\"%.*s\" value=\"%.*s\"", key.size(), key.data(), value.size(), value.data());
if (!m_responseHandler)
{
ESP_LOGW(TAG, "invalid response handler (%s:%hi)",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
m_responseHandler->requestHeaderReceived(key, value);
return true;
}
}
else
{
// ESP_LOGV(TAG, "state changed to Response");
m_state = State::Response;
if (!m_responseHandler)
{
ESP_LOGW(TAG, "invalid response handler ESP_LOGI",
m_remote_endpoint.address().to_string().c_str(), m_remote_endpoint.port());
m_socket.close();
return false;
}
m_responseHandler->sendResponse();
return false;
}
}

View File

@ -0,0 +1,51 @@
#pragma once
#include <asio.hpp>
#include <memory>
#include <string_view>
#include <string>
class Webserver;
class ResponseHandler;
class ClientConnection : public std::enable_shared_from_this<ClientConnection>
{
public:
ClientConnection(Webserver &webserver, asio::ip::tcp::socket socket);
~ClientConnection();
Webserver &webserver() { return m_webserver; }
const Webserver &webserver() const { return m_webserver; }
asio::ip::tcp::socket &socket() { return m_socket; }
const asio::ip::tcp::socket &socket() const { return m_socket; }
const asio::ip::tcp::endpoint &remote_endpoint() const { return m_remote_endpoint; }
void start();
void responseFinished(std::error_code ec);
private:
void doRead();
void readyRead(std::error_code ec, std::size_t length);
bool parseRequestLine(std::string_view line);
bool readyReadLine(std::string_view line);
bool parseRequestHeader(std::string_view line);
Webserver &m_webserver;
asio::ip::tcp::socket m_socket;
const asio::ip::tcp::endpoint m_remote_endpoint;
static constexpr const std::size_t max_length = 128;
char m_receiveBuffer[max_length];
std::string m_parsingBuffer;
enum class State { RequestLine, RequestHeaders, RequestBody, Response };
State m_state { State::RequestLine };
std::size_t m_requestBodySize{};
std::unique_ptr<ResponseHandler> m_responseHandler;
};

View File

@ -0,0 +1 @@
#include "responsehandler.h"

View File

@ -0,0 +1,12 @@
#pragma once
#include <string_view>
class ResponseHandler
{
public:
virtual ~ResponseHandler() = default;
virtual void requestHeaderReceived(std::string_view key, std::string_view value) = 0;
virtual void sendResponse() = 0;
};

View File

@ -0,0 +1,38 @@
#include "webserver.h"
#include <esp_log.h>
#include "clientconnection.h"
namespace {
constexpr const char * const TAG = "ASIO_WEBSERVER";
} // namespace
Webserver::Webserver(asio::io_context &io_context, short port)
: m_acceptor{io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)}
{
ESP_LOGI(TAG, "create webserver on port %hi", port);
doAccept();
}
void Webserver::doAccept()
{
m_acceptor.async_accept(
[this](std::error_code ec, asio::ip::tcp::socket socket)
{ acceptClient(ec, std::move(socket)); });
}
void Webserver::acceptClient(std::error_code ec, asio::ip::tcp::socket socket)
{
if (ec)
{
ESP_LOGI(TAG, "error: %i", ec.value());
doAccept();
return;
}
std::make_shared<ClientConnection>(*this, std::move(socket))->start();
doAccept();
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <memory>
#include <string_view>
#include <asio.hpp>
class ResponseHandler;
class ClientConnection;
class Webserver
{
public:
Webserver(asio::io_context& io_context, short port);
virtual ~Webserver() = default;
virtual std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) = 0;
private:
void doAccept();
void acceptClient(std::error_code ec, asio::ip::tcp::socket socket);
asio::ip::tcp::acceptor m_acceptor;
};

6
test/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pro.user*
cpputils/
date/
espchrono/
expected/
fmt/

24
test/asio_webserver.pro Normal file
View File

@ -0,0 +1,24 @@
TEMPLATE = lib
QT += core testlib
CONFIG += c++latest
win32: DESTDIR=$${OUT_PWD}
include(paths.pri)
isEmpty(ASIO_WEBSERVER_DIR): error("ASIO_WEBSERVER_DIR not set")
isEmpty(CPPUTILS_DIR): error("CPPUTILS_DIR not set")
isEmpty(ESPCHRONO_DIR): error("ESPCHRONO_DIR not set")
isEmpty(FMT_DIR): error("FMT_DIR not set")
include(dependencies.pri)
include($$ASIO_WEBSERVER_DIR/asio_webserver_src.pri)
include($$CPPUTILS_DIR/cpputils_src.pri)
include($$CPPUTILS_DIR/test/cpputilstestutils_src.pri)
include($$ESPCHRONO_DIR/espchrono_src.pri)
include($$ESPCHRONO_DIR/test/espchronotestutils_src.pri)
include($$FMT_DIR/fmt_src.pri)
HEADERS += esp_log.h

View File

@ -0,0 +1,77 @@
TEMPLATE = subdirs
equals(CLONE_CPPUTILS, 1) {
CPPUTILS_DIR = $$PWD/cpputils
message("Checking out cpputils...")
exists($$CPPUTILS_DIR/.git) {
system("git -C $$CPPUTILS_DIR pull")
} else {
isEmpty(CPPUTILS_URL) {
CPPUTILS_URL = https://github.com/0xFEEDC0DE64/cpputils.git
}
system("git clone $$CPPUTILS_URL $$CPPUTILS_DIR")
}
}
equals(CLONE_DATE, 1) {
DATE_DIR = $$PWD/date
message("Checking out date...")
exists($$DATE_DIR/.git): {
system("git -C $$DATE_DIR pull")
} else {
isEmpty(DATE_URL) {
DATE_URL = https://github.com/0xFEEDC0DE64/date.git
}
system("git clone $$DATE_URL $$DATE_DIR")
}
}
equals(CLONE_ESPCHRONO, 1) {
ESPCHRONO_DIR = $$PWD/espchrono
message("Checking out espchrono...")
exists($$ESPCHRONO_DIR/.git): {
system("git -C $$ESPCHRONO_DIR pull")
} else {
isEmpty(ESPCHRONO_URL) {
ESPCHRONO_URL = https://github.com/0xFEEDC0DE64/espchrono.git
}
system("git clone $$ESPCHRONO_URL $$ESPCHRONO_DIR")
}
}
equals(CLONE_EXPECTED, 1) {
EXPECTED_DIR = $$PWD/expected
message("Checking out expected...")
exists($$EXPECTED_DIR/.git) {
system("git -C $$EXPECTED_DIR pull")
} else {
isEmpty(EXPECTED_URL) {
EXPECTED_URL = https://github.com/0xFEEDC0DE64/expected.git
}
system("git clone $$EXPECTED_URL $$EXPECTED_DIR")
}
}
equals(CLONE_FMT, 1) {
FMT_DIR = $$PWD/fmt
message("Checking out fmt...")
exists($$FMT_DIR/.git) {
system("git -C $$FMT_DIR pull")
} else {
isEmpty(FMT_URL) {
FMT_URL = https://github.com/0xFEEDC0DE64/fmt.git
}
system("git clone $$FMT_URL $$FMT_DIR")
}
}
SUBDIRS += \
asio_webserver.pro \
webserver_example
sub-webserver_example.depends += sub-asio_webserver-pro

17
test/dependencies.pri Normal file
View File

@ -0,0 +1,17 @@
isEmpty(ASIO_WEBSERVER_DIR): error("ASIO_WEBSERVER_DIR not set")
isEmpty(CPPUTILS_DIR): error("CPPUTILS_DIR not set")
isEmpty(DATE_DIR): error("DATE_DIR not set")
isEmpty(ESPCHRONO_DIR): error("ESPCHRONO_DIR not set")
isEmpty(EXPECTED_DIR): error("EXPECTED_DIR not set")
isEmpty(FMT_DIR): error("FMT_DIR not set")
include($$ASIO_WEBSERVER_DIR/asio_webserver.pri)
include($$CPPUTILS_DIR/cpputils.pri)
include($$CPPUTILS_DIR/test/cpputilstestutils.pri)
include($$DATE_DIR/date.pri)
include($$ESPCHRONO_DIR/espchrono.pri)
include($$ESPCHRONO_DIR/test/espchronotestutils.pri)
include($$EXPECTED_DIR/expected.pri)
include($$FMT_DIR/fmt.pri)
QMAKE_CXXFLAGS += -Wno-missing-field-initializers

9
test/esp_log.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <QDebug>
#define ESP_LOGV(TAG, ...) qDebug(__VA_ARGS__)
#define ESP_LOGD(TAG, ...) qDebug(__VA_ARGS__)
#define ESP_LOGI(TAG, ...) qInfo(__VA_ARGS__)
#define ESP_LOGW(TAG, ...) qWarning(__VA_ARGS__)
#define ESP_LOGE(TAG, ...) qCritical(__VA_ARGS__)

56
test/paths.pri Normal file
View File

@ -0,0 +1,56 @@
ASIO_WEBSERVER_DIR = $$PWD/..
equals(CLONE_CPPUTILS, 1) {
CPPUTILS_DIR = $$PWD/cpputils
!exists($$CPPUTILS_DIR) {
error("$$CPPUTILS_DIR not found, please check all dependencies")
}
} else: exists($$PWD/../../cpputils/src) {
CPPUTILS_DIR = $$PWD/../../cpputils
} else {
error("cpputils not found, please check all dependencies")
}
equals(CLONE_DATE, 1) {
DATE_DIR = $$PWD/date
!exists($$DATE_DIR) {
error("$$DATE_DIR not found, please check all dependencies")
}
} else: exists($$PWD/../../date/include) {
DATE_DIR = $$PWD/../../date
} else {
error("date not found, please check all dependencies")
}
equals(CLONE_ESPCHRONO, 1) {
ESPCHRONO_DIR = $$PWD/espchrono
!exists($$ESPCHRONO_DIR) {
error("$$ESPCHRONO_DIR not found, please check all dependencies")
}
} else: exists($$PWD/../../espchrono/src) {
ESPCHRONO_DIR = $$PWD/../../espchrono
} else {
error("espchrono not found, please check all dependencies")
}
equals(CLONE_EXPECTED, 1) {
EXPECTED_DIR = $$PWD/expected
!exists($$EXPECTED_DIR) {
error("$$EXPECTED_DIR not found, please check all dependencies")
}
} else: exists($$PWD/../../expected/include) {
EXPECTED_DIR = $$PWD/../../expected
} else {
error("expected not found, please check all dependencies")
}
equals(CLONE_FMT, 1) {
FMT_DIR = $$PWD/fmt
!exists($$FMT_DIR) {
error("$$FMT_DIR not found, please check all dependencies")
}
} else: exists($$PWD/../../fmt/include) {
FMT_DIR = $$PWD/../../fmt
} else {
error("fmt not found, please check all dependencies")
}

View File

@ -0,0 +1,114 @@
#include "debugresponsehandler.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
DebugResponseHandler::DebugResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) :
m_clientConnection{clientConnection}, m_method{method}, m_path{path}, m_protocol{protocol}
{
ESP_LOGI(TAG, "constructed for %.*s %.*s (%s:%hi)", m_method.size(), m_method.data(), path.size(), path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
}
DebugResponseHandler::~DebugResponseHandler()
{
ESP_LOGI(TAG, "destructed for %.*s %.*s (%s:%hi)", m_method.size(), m_method.data(), m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
}
void DebugResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
{
m_requestHeaders.emplace_back(std::make_pair(std::string{key}, std::string{value}));
}
void DebugResponseHandler::sendResponse()
{
ESP_LOGI(TAG, "sending response for %.*s %.*s (%s:%hi)", m_method.size(), m_method.data(), m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
m_response = fmt::format("<html>"
"<head>"
"<title>Test</title>"
"</head>"
"<body>"
"<h1>Request details:</h1>"
"<table>"
"<tbody>"
"<tr><th>Method:</th><td>{}</td></tr>"
"<tr><th>Path:</th><td>{}</td></tr>"
"<tr><th>Protocol:</th><td>{}</td></tr>"
"<tr>"
"<th>Request headers:</th>"
"<td>"
"<table border=\"1\">",
m_method, m_path, m_protocol);
for (const auto &pair : m_requestHeaders)
m_response += fmt::format( "<tr><th>{}</th><td>{}</td></tr>",
pair.first, pair.second);
m_response += fmt::format( "</table>"
"</td>"
"</tr>"
"</tbody>"
"</table>"
"<form method=\"GET\">"
"<fieldset>"
"<legend>GET form with line edit</legend>"
"<label>Text-Input: <input type=\"text\" name=\"inputName\" /></label>"
"<button type=\"submit\">Go</button>"
"</fieldset>"
"</form>"
"<form method=\"POST\">"
"<fieldset>"
"<legend>POST form with line edit</legend>"
"<label>Text-Input: <input type=\"text\" name=\"inputName\" /></label>"
"<button type=\"submit\">Go</button>"
"</fieldset>"
"</form>"
"<form method=\"POST\">"
"<fieldset>"
"<legend>POST form with multiline edit</legend>"
"<label>Text-Input: <textarea name=\"inputName\"></textarea></label>"
"<button type=\"submit\">Go</button>"
"</fieldset>"
"</form>"
"<form method=\"POST\" enctype=\"multipart/form-data\">"
"<fieldset>"
"<legend>POST form with multipart form-data and line edit</legend>"
"<label>Text-Input: <input type=\"text\" name=\"inputName\" /></label>"
"<button type=\"submit\">Go</button>"
"</fieldset>"
"</form>"
"</body>"
"</html>");
m_response = fmt::format("HTTP/1.1 200 Ok\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 DebugResponseHandler::written(std::error_code ec, std::size_t length)
{
ESP_LOGI(TAG, "expected=%zd actual=%zd for %.*s %.*s (%s:%hi)", m_response.size(), length,
m_method.size(), m_method.data(), m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
m_clientConnection.responseFinished(ec);
}

View File

@ -0,0 +1,36 @@
#pragma once
// system includes
#include <string_view>
#include <string>
#include <utility>
#include <vector>
#include <system_error>
// 3rdparty lib includes
#include <asio_webserver/responsehandler.h>
// forward declarations
class ClientConnection;
class DebugResponseHandler : public ResponseHandler
{
public:
DebugResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol);
~DebugResponseHandler() override;
void requestHeaderReceived(std::string_view key, std::string_view value) final;
void sendResponse() final;
private:
void written(std::error_code ec, std::size_t length);
ClientConnection &m_clientConnection;
std::string m_method;
std::string m_path;
std::string m_protocol;
std::vector<std::pair<std::string, std::string>> m_requestHeaders;
std::string m_response;
};

View File

@ -0,0 +1,58 @@
#include "errorresponsehandler.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
ErrorResponseHandler::ErrorResponseHandler(ClientConnection &clientConnection, std::string_view path) :
m_clientConnection{clientConnection},
m_path{path}
{
ESP_LOGI(TAG, "constructed for %.*s (%s:%hi)", path.size(), path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
}
ErrorResponseHandler::~ErrorResponseHandler()
{
ESP_LOGI(TAG, "destructed for %.*s (%s:%hi)", m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
}
void ErrorResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
{
}
void ErrorResponseHandler::sendResponse()
{
ESP_LOGI(TAG, "sending response for %.*s (%s:%hi)", m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
m_response = fmt::format("Error 404 Not Found: {}", m_path);
m_response = fmt::format("HTTP/1.1 404 Not Found\r\n"
"Connection: close\r\n"
"Content-Type: text/plain\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 ErrorResponseHandler::written(std::error_code ec, std::size_t length)
{
ESP_LOGI(TAG, "expected=%zd actual=%zd for %.*s (%s:%hi)", m_response.size(), length, m_path.size(), m_path.data(),
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
m_clientConnection.responseFinished(ec);
}

View File

@ -0,0 +1,30 @@
#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 ErrorResponseHandler : public ResponseHandler
{
public:
ErrorResponseHandler(ClientConnection &clientConnection, std::string_view path);
~ErrorResponseHandler() override;
void requestHeaderReceived(std::string_view key, std::string_view value) final;
void sendResponse() final;
private:
void written(std::error_code ec, std::size_t length);
ClientConnection &m_clientConnection;
std::string m_path;
std::string m_response;
};

View File

@ -0,0 +1,21 @@
#include "examplewebserver.h"
#include "rootresponsehandler.h"
#include "debugresponsehandler.h"
#include "errorresponsehandler.h"
std::unique_ptr<ResponseHandler> ExampleWebserver::makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol)
{
const std::string_view processedPath{[&](){
const auto index = path.find('?');
return index == std::string_view::npos ?
path : path.substr(0, index);
}()};
if (processedPath.empty() || processedPath == "/")
return std::make_unique<RootResponseHandler>(clientConnection);
else if (processedPath == "/debug" || processedPath.starts_with("/debug/") )
return std::make_unique<DebugResponseHandler>(clientConnection, method, path, protocol);
else
return std::make_unique<ErrorResponseHandler>(clientConnection, path);
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "asio_webserver/webserver.h"
class ExampleWebserver : public Webserver
{
public:
using Webserver::Webserver;
std::unique_ptr<ResponseHandler> makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) final;
};

View File

@ -0,0 +1,27 @@
#include <QLoggingCategory>
#include <asio.hpp>
#include <esp_log.h>
#include "examplewebserver.h"
int main(int argc, char *argv[])
{
qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} "
"["
"%{if-debug}D%{endif}"
"%{if-info}I%{endif}"
"%{if-warning}W%{endif}"
"%{if-critical}C%{endif}"
"%{if-fatal}F%{endif}"
"] "
"%{function}(): "
"%{message}"));
asio::io_context io_context;
ExampleWebserver server{io_context, (short int)8080};
ESP_LOGI("running mainloop");
io_context.run();
}

View File

@ -0,0 +1,65 @@
#include "rootresponsehandler.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
RootResponseHandler::RootResponseHandler(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());
}
RootResponseHandler::~RootResponseHandler()
{
ESP_LOGI(TAG, "destructed for (%s:%hi)",
m_clientConnection.remote_endpoint().address().to_string().c_str(), m_clientConnection.remote_endpoint().port());
}
void RootResponseHandler::requestHeaderReceived(std::string_view key, std::string_view value)
{
}
void RootResponseHandler::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 = fmt::format("<html>"
"<head>"
"<title>asio test webserver</title>"
"</head>"
"<body>"
"<h1>asio test webserver</h1>"
"<a href=\"/debug\">Debug</a>"
"</body>"
"</html>");
m_response = fmt::format("HTTP/1.1 200 Ok\r\n"
"Connection: close\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 RootResponseHandler::written(std::error_code ec, std::size_t length)
{
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);
}

View File

@ -0,0 +1,29 @@
#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 RootResponseHandler : public ResponseHandler
{
public:
RootResponseHandler(ClientConnection &clientConnection);
~RootResponseHandler() override;
void requestHeaderReceived(std::string_view key, std::string_view value) final;
void sendResponse() final;
private:
void written(std::error_code ec, std::size_t length);
ClientConnection &m_clientConnection;
std::string m_response;
};

View File

@ -0,0 +1,32 @@
TEMPLATE = app
QT += core
CONFIG += c++latest
HEADERS += \
debugresponsehandler.h \
errorresponsehandler.h \
examplewebserver.h \
rootresponsehandler.h
SOURCES += \
debugresponsehandler.cpp \
errorresponsehandler.cpp \
examplewebserver.cpp \
main.cpp \
rootresponsehandler.cpp
unix: TARGET=webserver_example.bin
DESTDIR=$${OUT_PWD}/..
INCLUDEPATH += $$PWD/..
include(../paths.pri)
include(../dependencies.pri)
unix: {
LIBS += -Wl,-rpath=\\\$$ORIGIN
}
LIBS += -L$${OUT_PWD}/..
LIBS += -lasio_webserver