Add initial webserver sources
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.qmake.stash
|
||||
Makefile
|
||||
build-*/
|
||||
*.o
|
||||
moc_predefs.h
|
||||
target_wrapper.sh
|
||||
*.moc
|
43
CMakeLists.txt
Normal file
43
CMakeLists.txt
Normal 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
1
asio_webserver.pri
Normal file
@ -0,0 +1 @@
|
||||
INCLUDEPATH += $$PWD/src
|
9
asio_webserver_src.pri
Normal file
9
asio_webserver_src.pri
Normal 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
|
220
src/asio_webserver/clientconnection.cpp
Normal file
220
src/asio_webserver/clientconnection.cpp
Normal 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;
|
||||
}
|
||||
}
|
51
src/asio_webserver/clientconnection.h
Normal file
51
src/asio_webserver/clientconnection.h
Normal 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;
|
||||
};
|
1
src/asio_webserver/responsehandler.cpp
Normal file
1
src/asio_webserver/responsehandler.cpp
Normal file
@ -0,0 +1 @@
|
||||
#include "responsehandler.h"
|
12
src/asio_webserver/responsehandler.h
Normal file
12
src/asio_webserver/responsehandler.h
Normal 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;
|
||||
};
|
38
src/asio_webserver/webserver.cpp
Normal file
38
src/asio_webserver/webserver.cpp
Normal 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();
|
||||
}
|
24
src/asio_webserver/webserver.h
Normal file
24
src/asio_webserver/webserver.h
Normal 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
6
test/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.pro.user*
|
||||
cpputils/
|
||||
date/
|
||||
espchrono/
|
||||
expected/
|
||||
fmt/
|
24
test/asio_webserver.pro
Normal file
24
test/asio_webserver.pro
Normal 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
|
77
test/asio_webserver_tests.pro
Normal file
77
test/asio_webserver_tests.pro
Normal 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
17
test/dependencies.pri
Normal 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
9
test/esp_log.h
Normal 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
56
test/paths.pri
Normal 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")
|
||||
}
|
114
test/webserver_example/debugresponsehandler.cpp
Normal file
114
test/webserver_example/debugresponsehandler.cpp
Normal 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);
|
||||
}
|
36
test/webserver_example/debugresponsehandler.h
Normal file
36
test/webserver_example/debugresponsehandler.h
Normal 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;
|
||||
};
|
58
test/webserver_example/errorresponsehandler.cpp
Normal file
58
test/webserver_example/errorresponsehandler.cpp
Normal 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);
|
||||
}
|
30
test/webserver_example/errorresponsehandler.h
Normal file
30
test/webserver_example/errorresponsehandler.h
Normal 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;
|
||||
};
|
21
test/webserver_example/examplewebserver.cpp
Normal file
21
test/webserver_example/examplewebserver.cpp
Normal 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);
|
||||
}
|
11
test/webserver_example/examplewebserver.h
Normal file
11
test/webserver_example/examplewebserver.h
Normal 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;
|
||||
};
|
27
test/webserver_example/main.cpp
Normal file
27
test/webserver_example/main.cpp
Normal 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();
|
||||
}
|
65
test/webserver_example/rootresponsehandler.cpp
Normal file
65
test/webserver_example/rootresponsehandler.cpp
Normal 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);
|
||||
}
|
29
test/webserver_example/rootresponsehandler.h
Normal file
29
test/webserver_example/rootresponsehandler.h
Normal 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;
|
||||
};
|
32
test/webserver_example/webserver_example.pro
Normal file
32
test/webserver_example/webserver_example.pro
Normal 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
|
Reference in New Issue
Block a user