diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bb05b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.qmake.stash +Makefile +build-*/ +*.o +moc_predefs.h +target_wrapper.sh +*.moc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9031f6f --- /dev/null +++ b/CMakeLists.txt @@ -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 +) diff --git a/asio_webserver.pri b/asio_webserver.pri new file mode 100644 index 0000000..6916033 --- /dev/null +++ b/asio_webserver.pri @@ -0,0 +1 @@ +INCLUDEPATH += $$PWD/src diff --git a/asio_webserver_src.pri b/asio_webserver_src.pri new file mode 100644 index 0000000..51174e1 --- /dev/null +++ b/asio_webserver_src.pri @@ -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 diff --git a/src/asio_webserver/clientconnection.cpp b/src/asio_webserver/clientconnection.cpp new file mode 100644 index 0000000..af54f60 --- /dev/null +++ b/src/asio_webserver/clientconnection.cpp @@ -0,0 +1,220 @@ +#include "clientconnection.h" + +#include +#include + +#include + +#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; + } +} diff --git a/src/asio_webserver/clientconnection.h b/src/asio_webserver/clientconnection.h new file mode 100644 index 0000000..892f9a0 --- /dev/null +++ b/src/asio_webserver/clientconnection.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include +#include + +class Webserver; +class ResponseHandler; + +class ClientConnection : public std::enable_shared_from_this +{ +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 m_responseHandler; +}; diff --git a/src/asio_webserver/responsehandler.cpp b/src/asio_webserver/responsehandler.cpp new file mode 100644 index 0000000..dff8835 --- /dev/null +++ b/src/asio_webserver/responsehandler.cpp @@ -0,0 +1 @@ +#include "responsehandler.h" diff --git a/src/asio_webserver/responsehandler.h b/src/asio_webserver/responsehandler.h new file mode 100644 index 0000000..a6ce364 --- /dev/null +++ b/src/asio_webserver/responsehandler.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class ResponseHandler +{ +public: + virtual ~ResponseHandler() = default; + + virtual void requestHeaderReceived(std::string_view key, std::string_view value) = 0; + virtual void sendResponse() = 0; +}; diff --git a/src/asio_webserver/webserver.cpp b/src/asio_webserver/webserver.cpp new file mode 100644 index 0000000..cb45303 --- /dev/null +++ b/src/asio_webserver/webserver.cpp @@ -0,0 +1,38 @@ +#include "webserver.h" + +#include + +#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(*this, std::move(socket))->start(); + + doAccept(); +} diff --git a/src/asio_webserver/webserver.h b/src/asio_webserver/webserver.h new file mode 100644 index 0000000..b07044c --- /dev/null +++ b/src/asio_webserver/webserver.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +class ResponseHandler; +class ClientConnection; + +class Webserver +{ +public: + Webserver(asio::io_context& io_context, short port); + virtual ~Webserver() = default; + + virtual std::unique_ptr 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; +}; diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..51732bd --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,6 @@ +*.pro.user* +cpputils/ +date/ +espchrono/ +expected/ +fmt/ diff --git a/test/asio_webserver.pro b/test/asio_webserver.pro new file mode 100644 index 0000000..28602eb --- /dev/null +++ b/test/asio_webserver.pro @@ -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 diff --git a/test/asio_webserver_tests.pro b/test/asio_webserver_tests.pro new file mode 100644 index 0000000..a6713c5 --- /dev/null +++ b/test/asio_webserver_tests.pro @@ -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 diff --git a/test/dependencies.pri b/test/dependencies.pri new file mode 100644 index 0000000..ced7ba6 --- /dev/null +++ b/test/dependencies.pri @@ -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 diff --git a/test/esp_log.h b/test/esp_log.h new file mode 100644 index 0000000..1322c92 --- /dev/null +++ b/test/esp_log.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#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__) diff --git a/test/paths.pri b/test/paths.pri new file mode 100644 index 0000000..12efb63 --- /dev/null +++ b/test/paths.pri @@ -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") +} diff --git a/test/webserver_example/debugresponsehandler.cpp b/test/webserver_example/debugresponsehandler.cpp new file mode 100644 index 0000000..239d3b7 --- /dev/null +++ b/test/webserver_example/debugresponsehandler.cpp @@ -0,0 +1,114 @@ +#include "debugresponsehandler.h" + +// esp-idf includes +#include +#include + +// 3rdparty lib includes +#include +#include + +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("" + "" + "Test" + "" + "" + "

Request details:

" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
Method:{}
Path:{}
Protocol:{}
Request headers:" + "", + m_method, m_path, m_protocol); + + for (const auto &pair : m_requestHeaders) + m_response += fmt::format( "", + pair.first, pair.second); + + m_response += fmt::format( "
{}{}
" + "
" + "
" + "
" + "GET form with line edit" + "" + "" + "
" + "
" + "
" + "
" + "POST form with line edit" + "" + "" + "
" + "
" + "
" + "
" + "POST form with multiline edit" + "" + "" + "
" + "
" + "
" + "
" + "POST form with multipart form-data and line edit" + "" + "" + "
" + "
" + "" + ""); + + 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); +} diff --git a/test/webserver_example/debugresponsehandler.h b/test/webserver_example/debugresponsehandler.h new file mode 100644 index 0000000..d512605 --- /dev/null +++ b/test/webserver_example/debugresponsehandler.h @@ -0,0 +1,36 @@ +#pragma once + +// system includes +#include +#include +#include +#include +#include + +// 3rdparty lib includes +#include + +// 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> m_requestHeaders; + + std::string m_response; +}; diff --git a/test/webserver_example/errorresponsehandler.cpp b/test/webserver_example/errorresponsehandler.cpp new file mode 100644 index 0000000..7c715cf --- /dev/null +++ b/test/webserver_example/errorresponsehandler.cpp @@ -0,0 +1,58 @@ +#include "errorresponsehandler.h" + +// esp-idf includes +#include +#include + +// 3rdparty lib includes +#include +#include + +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); +} diff --git a/test/webserver_example/errorresponsehandler.h b/test/webserver_example/errorresponsehandler.h new file mode 100644 index 0000000..eef1b50 --- /dev/null +++ b/test/webserver_example/errorresponsehandler.h @@ -0,0 +1,30 @@ +#pragma once + +// system includes +#include +#include +#include + +// 3rdparty lib includes +#include + +// 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; +}; diff --git a/test/webserver_example/examplewebserver.cpp b/test/webserver_example/examplewebserver.cpp new file mode 100644 index 0000000..6166968 --- /dev/null +++ b/test/webserver_example/examplewebserver.cpp @@ -0,0 +1,21 @@ +#include "examplewebserver.h" + +#include "rootresponsehandler.h" +#include "debugresponsehandler.h" +#include "errorresponsehandler.h" + +std::unique_ptr 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(clientConnection); + else if (processedPath == "/debug" || processedPath.starts_with("/debug/") ) + return std::make_unique(clientConnection, method, path, protocol); + else + return std::make_unique(clientConnection, path); +} diff --git a/test/webserver_example/examplewebserver.h b/test/webserver_example/examplewebserver.h new file mode 100644 index 0000000..191eddd --- /dev/null +++ b/test/webserver_example/examplewebserver.h @@ -0,0 +1,11 @@ +#pragma once + +#include "asio_webserver/webserver.h" + +class ExampleWebserver : public Webserver +{ +public: + using Webserver::Webserver; + + std::unique_ptr makeResponseHandler(ClientConnection &clientConnection, std::string_view method, std::string_view path, std::string_view protocol) final; +}; diff --git a/test/webserver_example/main.cpp b/test/webserver_example/main.cpp new file mode 100644 index 0000000..79eb8ef --- /dev/null +++ b/test/webserver_example/main.cpp @@ -0,0 +1,27 @@ +#include +#include + +#include + +#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(); +} diff --git a/test/webserver_example/rootresponsehandler.cpp b/test/webserver_example/rootresponsehandler.cpp new file mode 100644 index 0000000..2821b6a --- /dev/null +++ b/test/webserver_example/rootresponsehandler.cpp @@ -0,0 +1,65 @@ +#include "rootresponsehandler.h" + +// esp-idf includes +#include +#include + +// 3rdparty lib includes +#include +#include + +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("" + "" + "asio test webserver" + "" + "" + "

asio test webserver

" + "Debug" + "" + ""); + + 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); +} diff --git a/test/webserver_example/rootresponsehandler.h b/test/webserver_example/rootresponsehandler.h new file mode 100644 index 0000000..249484b --- /dev/null +++ b/test/webserver_example/rootresponsehandler.h @@ -0,0 +1,29 @@ +#pragma once + +// system includes +#include +#include +#include + +// 3rdparty lib includes +#include + +// 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; +}; diff --git a/test/webserver_example/webserver_example.pro b/test/webserver_example/webserver_example.pro new file mode 100644 index 0000000..db17956 --- /dev/null +++ b/test/webserver_example/webserver_example.pro @@ -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