summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Makefile8
-rw-r--r--debian/changelog2
-rw-r--r--error.cpp32
-rw-r--r--error.h6
-rw-r--r--http.cpp105
-rw-r--r--https.cpp270
-rw-r--r--https.h11
-rw-r--r--plugin.cpp9
-rw-r--r--server.cpp10
-rw-r--r--tests/Makefile8
-rw-r--r--tests/test-config.cpp1
-rw-r--r--tests/test-webserver.cpp304
-rw-r--r--websocket.cpp2
-rw-r--r--websocket.h141
14 files changed, 410 insertions, 499 deletions
diff --git a/Makefile b/Makefile
index 862fa51..2803d05 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,7 @@ LDFLAGS+=-pie
PROGSRC=\
auth.cpp \
config.cpp \
+ error.cpp \
http.cpp \
https.cpp \
plugin.cpp \
@@ -45,7 +46,8 @@ PROGSRC=\
response.cpp \
statistics.cpp \
server.cpp \
- webserver.cpp
+ webserver.cpp \
+ websocket.cpp
SRC=$(PROGSRC) main.cpp
@@ -148,6 +150,8 @@ DISTFILES= \
debian/webserver.install \
debian/webserver.manpages \
debian/webserver.service \
+ error.cpp \
+ error.h \
http.cpp \
http.h \
https.cpp \
@@ -217,6 +221,8 @@ DISTFILES= \
tests/test-server.cpp \
tests/test-statistics.cpp \
tests/test-webserver.cpp \
+ websocket.cpp \
+ websocket.h \
webserver.1 \
webserver.conf \
webserver.cpp \
diff --git a/debian/changelog b/debian/changelog
index fdaa32c..ea55a13 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-webserver (1.18) UNRELEASED; urgency=medium
+webserver (1.18~pre1) UNRELEASED; urgency=medium
*
diff --git a/error.cpp b/error.cpp
new file mode 100644
index 0000000..d7a26de
--- /dev/null
+++ b/error.cpp
@@ -0,0 +1,32 @@
+#include "error.h"
+
+#include <iostream>
+
+#include <boost/asio/ssl/error.hpp>
+
+// Report a failure
+void fail(boost::beast::error_code ec, char const* what)
+{
+ // ssl::error::stream_truncated, also known as an SSL "short read",
+ // indicates the peer closed the connection without performing the
+ // required closing handshake (for example, Google does this to
+ // improve performance). Generally this can be a security issue,
+ // but if your communication protocol is self-terminated (as
+ // it is with both HTTP and WebSocket) then you may simply
+ // ignore the lack of close_notify.
+ //
+ // https://github.com/boostorg/beast/issues/38
+ //
+ // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
+ //
+ // When a short read would cut off the end of an HTTP message,
+ // Beast returns the error beast::http::error::partial_message.
+ // Therefore, if we see a short read here, it has occurred
+ // after the message has been completed, so it is safe to ignore it.
+
+ if (ec == boost::asio::ssl::error::stream_truncated)
+ return;
+
+ std::cerr << what << ": " << ec.message() << "\n";
+}
+
diff --git a/error.h b/error.h
new file mode 100644
index 0000000..f7a9da3
--- /dev/null
+++ b/error.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <boost/beast/core/error.hpp>
+
+void fail(boost::beast::error_code ec, char const* what);
+
diff --git a/http.cpp b/http.cpp
index b538233..0a9c680 100644
--- a/http.cpp
+++ b/http.cpp
@@ -2,11 +2,6 @@
#include <boost/beast/version.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include "server.h"
#include "response.h"
@@ -14,10 +9,8 @@
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/asio/dispatch.hpp>
-#ifndef BOOST_LATEST
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/ip/tcp.hpp>
-#endif
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
#include <algorithm>
@@ -48,12 +41,7 @@ void fail(beast::error_code ec, char const* what)
// Handles an HTTP server connection
class session : public std::enable_shared_from_this<session>
{
-#ifdef BOOST_LATEST
beast::tcp_stream stream_;
-#else
- tcp::socket socket_;
- boost::asio::strand<boost::asio::io_context::executor_type> strand_;
-#endif
beast::flat_buffer buffer_;
Server& m_server;
std::optional<http::request_parser<http::string_body>> parser_;
@@ -62,17 +50,12 @@ class session : public std::enable_shared_from_this<session>
void handle_request(::Server& server, request_type&& req)
{
-#ifdef BOOST_LATEST
stream_.expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client
-#else
- // socket_.expires_after(std::chrono::seconds(300)); // not supported by old boost
-#endif
auto sp = std::make_shared<response_type>(generate_response(req, server));
res_ = sp;
// Write the response
-#ifdef BOOST_LATEST
http::async_write(
stream_,
*sp,
@@ -80,36 +63,14 @@ class session : public std::enable_shared_from_this<session>
&session::on_write,
shared_from_this(),
sp->need_eof()));
-#else
- http::async_write(
- socket_,
- *sp,
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_write,
- shared_from_this(),
- std::placeholders::_1,
- std::placeholders::_2,
- sp->need_eof())));
-#endif
}
public:
// Take ownership of the stream
session(
-#ifdef BOOST_LATEST
tcp::socket&& socket,
-#else
- tcp::socket socket,
-#endif
Server& server)
-#ifdef BOOST_LATEST
: stream_(std::move(socket))
-#else
- : socket_(std::move(socket))
- , strand_(socket_.get_executor())
-#endif
, m_server(server)
{
}
@@ -119,14 +80,10 @@ public:
{
// We need to be executing within a strand to perform async operations
// on the I/O objects in this session.
-#ifdef BOOST_LATEST
net::dispatch(stream_.get_executor(),
beast::bind_front_handler(
&session::do_read,
shared_from_this()));
-#else
- do_read();
-#endif
}
void do_read()
@@ -140,7 +97,6 @@ public:
parser_.emplace();
parser_->body_limit(1000000000); // 1GB limit
-#ifdef BOOST_LATEST
// Set the timeout.
stream_.expires_after(std::chrono::seconds(30));
@@ -149,28 +105,12 @@ public:
beast::bind_front_handler(
&session::on_read,
shared_from_this()));
-#else
-
- http::async_read(socket_, buffer_, *parser_,
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_read,
- shared_from_this(),
- std::placeholders::_1,
- std::placeholders::_2)));
-#endif
}
void
on_read(
-#ifdef BOOST_LATEST
beast::error_code ec,
std::size_t bytes_transferred
-#else
- boost::system::error_code ec,
- std::size_t bytes_transferred
-#endif
)
{
boost::ignore_unused(bytes_transferred);
@@ -193,15 +133,9 @@ public:
void
on_write(
-#ifdef BOOST_LATEST
bool close,
beast::error_code ec,
std::size_t bytes_transferred
-#else
- boost::system::error_code ec,
- std::size_t bytes_transferred,
- bool close
-#endif
)
{
boost::ignore_unused(bytes_transferred);
@@ -228,11 +162,7 @@ public:
{
// Send a TCP shutdown
beast::error_code ec;
-#ifdef BOOST_LATEST
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
-#else
- socket_.shutdown(tcp::socket::shutdown_send, ec);
-#endif
// At this point the connection is closed gracefully
}
};
@@ -242,13 +172,8 @@ public:
// Accepts incoming connections and launches the sessions
class listener : public std::enable_shared_from_this<listener>
{
-#ifdef BOOST_LATEST
net::io_context& ioc_;
-#endif
tcp::acceptor acceptor_;
-#ifndef BOOST_LATEST
- tcp::socket socket_;
-#endif
Server& m_server;
public:
@@ -256,20 +181,11 @@ public:
net::io_context& ioc,
tcp::endpoint endpoint,
Server& server)
-#ifdef BOOST_LATEST
: ioc_(ioc)
, acceptor_(net::make_strand(ioc))
-#else
- : acceptor_(ioc)
- , socket_(ioc)
-#endif
, m_server(server)
{
-#ifdef BOOST_VERSION
beast::error_code ec;
-#else
- boost::system::error_code ec;
-#endif
// Open the acceptor
acceptor_.open(endpoint.protocol(), ec);
@@ -309,10 +225,6 @@ public:
void
run()
{
-#ifndef BOOST_LATEST
- if (!acceptor_.is_open())
- return;
-#endif
do_accept();
}
@@ -321,28 +233,15 @@ private:
do_accept()
{
// The new connection gets its own strand
-#ifdef BOOST_LATEST
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
-#else
- acceptor_.async_accept(
- socket_,
- std::bind(
- &listener::on_accept,
- shared_from_this(),
- std::placeholders::_1));
-#endif
}
void
-#ifdef BOOST_LATEST
on_accept(beast::error_code ec, tcp::socket socket)
-#else
- on_accept(boost::system::error_code ec)
-#endif
{
if(ec)
{
@@ -352,11 +251,7 @@ private:
{
// Create the session and run it
std::make_shared<session>(
-#ifdef BOOST_LATEST
std::move(socket),
-#else
- std::move(socket_),
-#endif
m_server)->run();
}
diff --git a/https.cpp b/https.cpp
index ccf14d7..3a68b00 100644
--- a/https.cpp
+++ b/https.cpp
@@ -1,8 +1,10 @@
#include "https.h"
#include "config.h"
+#include "error.h"
#include "server.h"
#include "response.h"
+#include "websocket.h"
#include "libreichwein/file.h"
@@ -17,13 +19,7 @@
#include <boost/asio/buffers_iterator.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/ssl/context.hpp>
-#ifdef BOOST_LATEST
#include <boost/beast/ssl.hpp>
-#else
-#include <boost/asio/ip/tcp.hpp>
-#include <boost/asio/ssl/stream.hpp>
-#include <boost/asio/bind_executor.hpp>
-#endif
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
@@ -52,155 +48,10 @@ namespace {
//------------------------------------------------------------------------------
-// Report a failure
-void fail(
-#ifdef BOOST_LATEST
- beast::error_code ec,
-#else
- boost::system::error_code ec,
-#endif
- char const* what)
-{
-#ifdef BOOST_LATEST
- // ssl::error::stream_truncated, also known as an SSL "short read",
- // indicates the peer closed the connection without performing the
- // required closing handshake (for example, Google does this to
- // improve performance). Generally this can be a security issue,
- // but if your communication protocol is self-terminated (as
- // it is with both HTTP and WebSocket) then you may simply
- // ignore the lack of close_notify.
- //
- // https://github.com/boostorg/beast/issues/38
- //
- // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
- //
- // When a short read would cut off the end of an HTTP message,
- // Beast returns the error beast::http::error::partial_message.
- // Therefore, if we see a short read here, it has occurred
- // after the message has been completed, so it is safe to ignore it.
-
- if(ec == net::ssl::error::stream_truncated)
- return;
-#endif
-
- std::cerr << what << ": " << ec.message() << "\n";
-}
-
-class websocket_session: public std::enable_shared_from_this<websocket_session>
-{
- websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_;
- beast::flat_buffer buffer_;
-
-public:
- explicit websocket_session(beast::ssl_stream<beast::tcp_stream>&& stream) :
- ws_(std::move(stream))
- {
- }
-
- // Start the asynchronous accept operation
- template<class Body, class Allocator>
- void
- do_accept(http::request<Body, http::basic_fields<Allocator>> req)
- {
- // Set suggested timeout settings for the websocket
- ws_.set_option(
- websocket::stream_base::timeout::suggested(
- beast::role_type::server));
-
- // Set a decorator to change the Server of the handshake
- ws_.set_option(websocket::stream_base::decorator(
- [](websocket::response_type& res)
- {
- res.set(http::field::server,
- std::string{"Reichwein.IT Webserver"});
- }));
-
- // Accept the websocket handshake
- ws_.async_accept(
- req,
- beast::bind_front_handler(
- &websocket_session::on_accept,
- shared_from_this()));
- }
-
-private:
- void
- on_accept(beast::error_code ec)
- {
- if(ec)
- return fail(ec, "accept");
-
- // Read a message
- do_read();
- }
-
- void
- do_read()
- {
- // Read a message into our buffer
- ws_.async_read(
- buffer_,
- beast::bind_front_handler(
- &websocket_session::on_read,
- shared_from_this()));
- }
-
- void
- on_read(
- beast::error_code ec,
- std::size_t bytes_transferred)
- {
- boost::ignore_unused(bytes_transferred);
-
- // This indicates that the websocket_session was closed
- if(ec == websocket::error::closed)
- return;
-
- if(ec)
- fail(ec, "read");
-
- // Echo the message
- ws_.text(ws_.got_text());
- std::string data(boost::asio::buffers_begin(buffer_.data()), boost::asio::buffers_end(buffer_.data()));
- static int count{};
- data += ": " + std::to_string(count++);
- buffer_.consume(buffer_.size());
- boost::beast::ostream(buffer_) << data;
- ws_.async_write(
- buffer_.data(),
- beast::bind_front_handler(
- &websocket_session::on_write,
- shared_from_this()));
- }
-
- void
- on_write(
- beast::error_code ec,
- std::size_t bytes_transferred)
- {
- boost::ignore_unused(bytes_transferred);
-
- if(ec)
- return fail(ec, "write");
-
- // Clear the buffer
- buffer_.consume(buffer_.size());
-
- // Do another read
- do_read();
- }
-};
-
// Handles an HTTP server connection
class session : public std::enable_shared_from_this<session>
{
-#ifdef BOOST_LATEST
beast::ssl_stream<beast::tcp_stream> stream_;
-#else
- tcp::socket socket_;
- ssl::stream<tcp::socket&> stream_;
- boost::asio::strand<boost::asio::io_context::executor_type> strand_;
-#endif
beast::flat_buffer buffer_;
Server& m_server;
std::optional<http::request_parser<http::string_body>> parser_; // need to reset parser every time, no other mechanism currently
@@ -209,17 +60,12 @@ class session : public std::enable_shared_from_this<session>
void handle_request(::Server& server, request_type&& req)
{
-#ifdef BOOST_LATEST
beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(300)); // timeout on write by server much longer than read timeout from client
-#else
- // beast::get_lowest_layer<tcp::socket>(stream_).expires_after(std::chrono::seconds(300)); // not supported by boost
-#endif
auto sp = std::make_shared<response_type>(generate_response(req, server));
res_ = sp;
// Write the response
-#ifdef BOOST_LATEST
http::async_write(
stream_,
*sp,
@@ -227,38 +73,15 @@ class session : public std::enable_shared_from_this<session>
&session::on_write,
shared_from_this(),
sp->need_eof()));
-#else
- http::async_write(
- stream_,
- *sp,
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_write,
- shared_from_this(),
- std::placeholders::_1,
- std::placeholders::_2,
- sp->need_eof())));
-#endif
}
public:
// Take ownership of the socket
explicit
session(
-#ifdef BOOST_LATEST
tcp::socket&& socket,
-#else
- tcp::socket socket,
-#endif
ssl::context& ctx,
Server& server)
-#ifdef BOOST_LATEST
: stream_(std::move(socket), ctx)
-#else
- : socket_(std::move(socket))
- , stream_(socket_, ctx)
- , strand_(socket_.get_executor())
-#endif
, m_server(server)
{
}
@@ -267,7 +90,6 @@ public:
void
run()
{
-#ifdef BOOST_LATEST
// We need to be executing within a strand to perform async operations
// on the I/O objects in this session.
net::dispatch(
@@ -275,19 +97,8 @@ public:
beast::bind_front_handler(
&session::on_run,
shared_from_this()));
-#else
- stream_.async_handshake(
- ssl::stream_base::server,
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_handshake,
- shared_from_this(),
- std::placeholders::_1)));
-#endif
}
-#ifdef BOOST_LATEST
void
on_run()
{
@@ -302,15 +113,10 @@ public:
&session::on_handshake,
shared_from_this()));
}
-#endif
void
on_handshake(
-#ifdef BOOST_LATEST
beast::error_code ec
-#else
- boost::system::error_code ec
-#endif
)
{
if(ec)
@@ -331,7 +137,6 @@ public:
parser_.emplace();
parser_->body_limit(1000000000); // 1GB limit
-#ifdef BOOST_LATEST
// Set the timeout.
beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
@@ -340,25 +145,11 @@ public:
beast::bind_front_handler(
&session::on_read,
shared_from_this()));
-#else
- http::async_read(stream_, buffer_, *parser_,
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_read,
- shared_from_this(),
- std::placeholders::_1,
- std::placeholders::_2)));
-#endif
}
void
on_read(
-#ifdef BOOST_LATEST
beast::error_code ec,
-#else
- boost::system::error_code ec,
-#endif
std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
@@ -388,15 +179,9 @@ public:
void
on_write(
-#ifdef BOOST_LATEST
bool close,
beast::error_code ec,
std::size_t bytes_transferred
-#else
- boost::system::error_code ec,
- std::size_t bytes_transferred,
- bool close
-#endif
)
{
boost::ignore_unused(bytes_transferred);
@@ -421,7 +206,6 @@ public:
void
do_close()
{
-#ifdef BOOST_LATEST
// Set the timeout.
beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30));
@@ -430,15 +214,6 @@ public:
beast::bind_front_handler(
&session::on_shutdown,
shared_from_this()));
-#else
- stream_.async_shutdown(
- boost::asio::bind_executor(
- strand_,
- std::bind(
- &session::on_shutdown,
- shared_from_this(),
- std::placeholders::_1)));
-#endif
}
void
@@ -456,14 +231,9 @@ public:
// Accepts incoming connections and launches the sessions
class listener : public std::enable_shared_from_this<listener>
{
-#ifdef BOOST_LATEST
net::io_context& ioc_;
-#endif
ssl::context& ctx_;
tcp::acceptor acceptor_;
-#ifndef BOOST_LATEST
- tcp::socket socket_;
-#endif
::Server& m_server;
public:
@@ -472,21 +242,12 @@ public:
ssl::context& ctx,
tcp::endpoint endpoint,
Server& server) :
-#ifdef BOOST_LATEST
ioc_(ioc),
-#endif
- ctx_(ctx)
- , acceptor_(ioc)
-#ifndef BOOST_LATEST
- , socket_(ioc)
-#endif
- , m_server(server)
+ ctx_(ctx),
+ acceptor_(ioc),
+ m_server(server)
{
-#ifdef BOOST_LATEST
beast::error_code ec;
-#else
- boost::system::error_code ec;
-#endif
// Open the acceptor
acceptor_.open(endpoint.protocol(), ec);
@@ -526,10 +287,6 @@ public:
void
run()
{
-#ifndef BOOST_LATEST
- if(! acceptor_.is_open())
- return;
-#endif
do_accept();
}
@@ -538,28 +295,15 @@ private:
do_accept()
{
// The new connection gets its own strand
-#ifdef BOOST_LATEST
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
-#else
- acceptor_.async_accept(
- socket_,
- std::bind(
- &listener::on_accept,
- shared_from_this(),
- std::placeholders::_1));
-#endif
}
void
-#ifdef BOOST_LATEST
on_accept(beast::error_code ec, tcp::socket socket)
-#else
- on_accept(boost::system::error_code ec)
-#endif
{
if(ec)
{
@@ -569,11 +313,7 @@ private:
{
// Create the session and run it
std::make_shared<session>(
-#ifdef BOOST_LATEST
std::move(socket),
-#else
- std::move(socket_),
-#endif
ctx_,
m_server)->run();
}
diff --git a/https.h b/https.h
index 8d4b426..2a1caa8 100644
--- a/https.h
+++ b/https.h
@@ -3,20 +3,13 @@
#include <boost/asio/steady_timer.hpp>
#include <boost/beast/version.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include <memory>
#include <string>
#include <unordered_map>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
-#ifdef BOOST_LATEST
#include <boost/beast/ssl.hpp>
-#endif
#include <boost/asio/ssl.hpp>
#include "config.h"
@@ -26,11 +19,7 @@ namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
namespace HTTPS {
-#ifdef BOOST_LATEST
static const ssl::context_base::method tls_method {ssl::context::tlsv13};
-#else
-static const ssl::context_base::method tls_method {ssl::context::tlsv12};
-#endif
class Server: public ::Server
{
diff --git a/plugin.cpp b/plugin.cpp
index afcbdb3..68861a8 100644
--- a/plugin.cpp
+++ b/plugin.cpp
@@ -2,11 +2,6 @@
#include <boost/beast/version.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include <boost/dll/import.hpp>
#include <boost/filesystem.hpp>
@@ -29,11 +24,7 @@ void PluginLoader::load_plugins()
for (auto& path: fs::recursive_directory_iterator(dir)) {
if (path.is_regular_file() && path.path().extension() == ".so"s) {
-#ifdef BOOST_LATEST
dll::fs::path lib_path{path.path()};
-#else
- boost::filesystem::path lib_path{path.path().generic_string()};
-#endif
try {
boost::shared_ptr<webserver_plugin_interface> plugin = dll::import<webserver_plugin_interface>(lib_path, "webserver_plugin", dll::load_mode::append_decorations);
diff --git a/server.cpp b/server.cpp
index 5d9d7ea..d22d559 100644
--- a/server.cpp
+++ b/server.cpp
@@ -1,19 +1,9 @@
#include <boost/beast/version.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
-#ifdef BOOST_LATEST
#include <boost/beast/ssl.hpp>
-#else
-#include <boost/asio/ip/tcp.hpp>
-#include <boost/asio/ssl/stream.hpp>
-#endif
#include <boost/asio/dispatch.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/strand.hpp>
diff --git a/tests/Makefile b/tests/Makefile
index 5f162de..14af291 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -36,6 +36,7 @@ LDFLAGS+=-pie
UNITS=\
auth.cpp \
config.cpp \
+ error.cpp \
http.cpp \
https.cpp \
plugin.cpp \
@@ -43,7 +44,8 @@ UNITS=\
response.cpp \
statistics.cpp \
server.cpp \
- webserver.cpp
+ webserver.cpp \
+ websocket.cpp
TESTSRC=\
test-auth.cpp \
@@ -85,6 +87,8 @@ auth.o: ../auth.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
config.o: ../config.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
+error.o: ../error.cpp
+ $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
http.o: ../http.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
https.o: ../https.cpp
@@ -101,6 +105,8 @@ server.o: ../server.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
webserver.o: ../webserver.cpp
$(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
+websocket.o: ../websocket.cpp
+ $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@
ADD_DEP=Makefile
diff --git a/tests/test-config.cpp b/tests/test-config.cpp
index fe482f8..eb7e9c7 100644
--- a/tests/test-config.cpp
+++ b/tests/test-config.cpp
@@ -31,6 +31,7 @@ public:
{
std::error_code ec;
fs::remove(testConfigFilename);
+ fs::remove("stats.db");
}
};
diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp
index 7059bc6..6bbf302 100644
--- a/tests/test-webserver.cpp
+++ b/tests/test-webserver.cpp
@@ -3,11 +3,6 @@
#define BOOST_TEST_MODULE webserver_test
#include <boost/test/included/unit_test.hpp>
-// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67)
-#if BOOST_VERSION >= 107100
-#define BOOST_LATEST
-#endif
-
#include <boost/test/data/dataset.hpp>
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
@@ -17,9 +12,7 @@
#include <boost/beast/http.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
-#ifdef BOOST_LATEST
#include <boost/beast/ssl.hpp>
-#endif
#include <boost/beast/version.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/buffers_iterator.hpp>
@@ -47,6 +40,7 @@
#include <unistd.h>
#include <libreichwein/file.h>
+#include <libreichwein/process.h>
#include "webserver.h"
@@ -211,31 +205,17 @@ VZTqPHmb+db0rFA3XlAg2A==
m_filebuf = 0;
}
- bool isRunning()
+ bool is_running()
{
if (m_pid == 0)
return false;
- fs::path pid_file{fmt::format("/proc/{}/stat", m_pid)};
- if (!fs::exists(pid_file))
- return false;
-
- std::string s{File::getFile(pid_file)};
-
- auto pos0{s.find(' ', 0)};
- pos0 = s.find(' ', pos0 + 1);
- pos0++;
-
- auto pos1{s.find(' ', pos0 + 1)};
-
- std::string state{s.substr(pos0, pos1 - pos0)};
-
- return state == "R" || state == "S";
+ return Reichwein::Process::is_running(m_pid);
}
std::string output()
{
- if (!isRunning())
+ if (!is_running())
throw std::runtime_error("No output/stdout available from webserver since it is not running");
if (!m_is)
@@ -255,7 +235,7 @@ private:
// child stdout
std::shared_ptr<__gnu_cxx::stdio_filebuf<char>> m_filebuf;
std::shared_ptr<std::istream> m_is;
-};
+}; // class WebserverProcess
std::pair<std::string,std::string> HTTP(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get)
{
@@ -331,13 +311,7 @@ std::pair<std::string,std::string> HTTPS(const std::string& target, bool ipv6 =
boost::asio::io_context ioc;
// The SSL context is required, and holds certificates
- boost::asio::ssl::context ctx(
-#ifdef BOOST_LATEST
- boost::asio::ssl::context::tlsv13_client
-#else
- boost::asio::ssl::context::tlsv12_client
-#endif
- );
+ boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client);
// This holds the root certificate used for verification
load_root_certificates(ctx);
@@ -410,16 +384,19 @@ class Fixture
{
public:
Fixture(){}
- ~Fixture(){}
+ ~Fixture()
+ {
+ fs::remove("stats.db");
+ }
};
BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make({false, true}) * data::make({false, true}) * data::make({boost::beast::http::verb::head, boost::beast::http::verb::get}), ipv6, http11, https, method)
{
WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
std::pair<std::string,std::string> response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
std::string::size_type size{File::getFile(testConfigFilename).size()};
BOOST_CHECK_GT(size, 0);
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size));
@@ -427,7 +404,7 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make
for (int i = 0; i < 10; i++) {
std::pair<std::string,std::string> response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size));
BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? ""s : File::getFile(testConfigFilename));
}
@@ -437,92 +414,227 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, data::make({false, true
{
WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE(!fs::exists("./webserver.confSUFFIX"));
auto response{(https ? HTTPS("/webserver.confSUFFIX", ipv6, http11, method) : HTTP("/webserver.confSUFFIX", ipv6, http11, method))};
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 404 Not Found\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : 36));
BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? "" : "404 Not found: /webserver.confSUFFIX");
}
-BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
+// Test server
+class WebsocketServerProcess
{
- WebserverProcess serverProcess;
- BOOST_REQUIRE(serverProcess.isRunning());
+public:
+ WebsocketServerProcess()
+ {
+ start();
+ }
+
+ ~WebsocketServerProcess()
+ {
+ stop();
+ }
+
+ // Echoes back all received WebSocket messages
+ void do_session(boost::asio::ip::tcp::socket socket)
+ {
+ try
+ {
+ // Construct the stream by moving in the socket
+ boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws{std::move(socket)};
+
+ // Set a decorator to change the Server of the handshake
+ ws.set_option(boost::beast::websocket::stream_base::decorator(
+ [](boost::beast::websocket::response_type& res)
+ {
+ res.set(boost::beast::http::field::server,
+ std::string("Reichwein.IT Test Websocket Server"));
+ }));
+
+ // Accept the websocket handshake
+ ws.accept();
+
+ for(;;)
+ {
+ // This buffer will hold the incoming message
+ boost::beast::flat_buffer buffer;
+
+ // Read a message
+ ws.read(buffer);
+
+ // Echo the message back
+ ws.text(ws.got_text());
+ std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ data += ": " + std::to_string(m_count++);
+ buffer.consume(buffer.size());
+ boost::beast::ostream(buffer) << data;
+ ws.write(buffer.data());
+ }
+ }
+ catch(boost::beast::system_error const& se)
+ {
+ // This indicates that the session was closed
+ if(se.code() != boost::beast::websocket::error::closed)
+ std::cerr << "Error: " << se.code().message() << std::endl;
+ }
+ catch(std::exception const& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+ }
+
+ bool is_running()
+ {
+ if (m_pid == 0)
+ return false;
+
+ return Reichwein::Process::is_running(m_pid);
+ }
+
+ void start()
+ {
+ if (m_pid != 0)
+ throw std::runtime_error("Process already running, so it can't be started");
+
+ // connect stdout of new child process to stream of parent, via pipe
+ m_pid = fork();
+ if (m_pid < 0)
+ throw std::runtime_error("Fork unsuccessful.");
+
+ if (m_pid == 0) { // child process branch
+ while (true) {
+ try
+ {
+ auto const address = boost::asio::ip::make_address("localhost");
+ auto const port = static_cast<unsigned short>(9876);
+
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc{1};
+
+ // The acceptor receives incoming connections
+ boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}};
+ for(;;)
+ {
+ // This will receive the new connection
+ boost::asio::ip::tcp::socket socket{ioc};
+
+ // Block until we get a connection
+ acceptor.accept(socket);
+
+ // Launch the session, transferring ownership of the socket
+ std::thread(
+ &WebsocketServerProcess::do_session, this,
+ std::move(socket)).detach();
+ }
+ }
+ catch (const std::exception& e)
+ {
+ std::cerr << "Error: " << e.what() << std::endl;
+ }
+ }
+ exit(0);
+ }
+ }
+
+ void stop()
+ {
+ if (!is_running())
+ throw std::runtime_error("Process not running, so it can't be stopped");
+
+ if (kill(m_pid, SIGKILL) != 0)
+ throw std::runtime_error("Unable to kill process");
+
+ if (int result = waitpid(m_pid, NULL, 0); result != m_pid)
+ throw std::runtime_error("waitpid returned "s + std::to_string(result));
+
+ m_pid = 0;
+ }
+private:
+ int m_pid{};
+ int m_count{};
+}; // class WebsocketServerProcess
- std::string host = "::1";
- auto const port = "8081" ;
- auto const text = "request1";
+BOOST_FIXTURE_TEST_CASE(websocket, Fixture)
+{
+ WebserverProcess serverProcess;
+ BOOST_REQUIRE(serverProcess.is_running());
+
+ WebsocketServerProcess websocketProcess;
+ BOOST_REQUIRE(websocketProcess.is_running());
- // The io_context is required for all I/O
- boost::asio::io_context ioc;
+ std::string host = "::1";
+ auto const port = "8081" ;
+ auto const text = "request1";
- // The SSL context is required, and holds certificates
- boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client};
+ // The io_context is required for all I/O
+ boost::asio::io_context ioc;
- // This holds the root certificate used for verification
- load_root_certificates(ctx);
+ // The SSL context is required, and holds certificates
+ boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client};
- // These objects perform our I/O
- boost::asio::ip::tcp::resolver resolver{ioc};
- boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> ws{ioc, ctx};
+ // This holds the root certificate used for verification
+ load_root_certificates(ctx);
- // Look up the domain name
- auto const results = resolver.resolve(host, port);
+ // These objects perform our I/O
+ boost::asio::ip::tcp::resolver resolver{ioc};
+ boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> ws{ioc, ctx};
- // Make the connection on the IP address we get from a lookup
- auto ep = boost::asio::connect(get_lowest_layer(ws), results);
+ // Look up the domain name
+ auto const results = resolver.resolve(host, port);
- // Set SNI Hostname (many hosts need this to handshake successfully)
- if(! SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str()))
- throw boost::beast::system_error(
- boost::beast::error_code(
- static_cast<int>(::ERR_get_error()),
- boost::asio::error::get_ssl_category()),
- "Failed to set SNI Hostname");
+ // Make the connection on the IP address we get from a lookup
+ auto ep = boost::asio::connect(get_lowest_layer(ws), results);
- // Update the host_ string. This will provide the value of the
- // Host HTTP header during the WebSocket handshake.
- // See https://tools.ietf.org/html/rfc7230#section-5.4
- host += ':' + std::to_string(ep.port());
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(! SSL_set_tlsext_host_name(ws.next_layer().native_handle(), host.c_str()))
+ throw boost::beast::system_error(
+ boost::beast::error_code(
+ static_cast<int>(::ERR_get_error()),
+ boost::asio::error::get_ssl_category()),
+ "Failed to set SNI Hostname");
+
+ // Update the host_ string. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ host += ':' + std::to_string(ep.port());
- // Perform the SSL handshake
- ws.next_layer().handshake(boost::asio::ssl::stream_base::client);
+ // Perform the SSL handshake
+ ws.next_layer().handshake(boost::asio::ssl::stream_base::client);
- // Set a decorator to change the User-Agent of the handshake
- ws.set_option(boost::beast::websocket::stream_base::decorator(
- [](boost::beast::websocket::request_type& req)
- {
- req.set(boost::beast::http::field::user_agent,
- std::string(BOOST_BEAST_VERSION_STRING) +
- " websocket-client-coro");
- }));
+ // Set a decorator to change the User-Agent of the handshake
+ ws.set_option(boost::beast::websocket::stream_base::decorator(
+ [](boost::beast::websocket::request_type& req)
+ {
+ req.set(boost::beast::http::field::user_agent,
+ std::string("Reichwein.IT Test Websocket Client"));
+ }));
- // Perform the websocket handshake
- ws.handshake(host, "/");
+ // Perform the websocket handshake
+ ws.handshake(host, "/");
- // Send the message
- ws.write(boost::asio::buffer(std::string(text)));
+ // Send the message
+ ws.write(boost::asio::buffer(std::string(text)));
- // This buffer will hold the incoming message
- boost::beast::flat_buffer buffer;
+ // This buffer will hold the incoming message
+ boost::beast::flat_buffer buffer;
- // Read a message into our buffer
- ws.read(buffer);
- std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
- BOOST_CHECK_EQUAL(data, "request1: 0");
+ // Read a message into our buffer
+ ws.read(buffer);
+ std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ BOOST_CHECK_EQUAL(data, "request1: 0");
- buffer.consume(buffer.size());
+ buffer.consume(buffer.size());
- ws.write(boost::asio::buffer(std::string(text)));
- ws.read(buffer);
- data = std::string(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
- BOOST_CHECK_EQUAL(data, "request1: 1");
+ ws.write(boost::asio::buffer(std::string(text)));
+ ws.read(buffer);
+ data = std::string(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data()));
+ BOOST_CHECK_EQUAL(data, "request1: 1");
- // Close the WebSocket connection
- ws.close(boost::beast::websocket::close_code::normal);
+ // Close the WebSocket connection
+ ws.close(boost::beast::websocket::close_code::normal);
- BOOST_REQUIRE(serverProcess.isRunning());
+ BOOST_REQUIRE(serverProcess.is_running());
}
diff --git a/websocket.cpp b/websocket.cpp
new file mode 100644
index 0000000..a4c8dfb
--- /dev/null
+++ b/websocket.cpp
@@ -0,0 +1,2 @@
+#include "websocket.h"
+
diff --git a/websocket.h b/websocket.h
new file mode 100644
index 0000000..d8d0262
--- /dev/null
+++ b/websocket.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include "error.h"
+
+#include <boost/asio/buffer.hpp>
+#include <boost/beast/core.hpp>
+#include <boost/beast/http.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/beast/ssl/ssl_stream.hpp>
+#include <boost/asio/buffers_iterator.hpp>
+#include <boost/asio/dispatch.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/asio/strand.hpp>
+#include <boost/config.hpp>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdlib>
+#include <filesystem>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace beast = boost::beast; // from <boost/beast.hpp>
+namespace http = beast::http; // from <boost/beast/http.hpp>
+namespace net = boost::asio; // from <boost/asio.hpp>
+namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
+namespace websocket = beast::websocket;
+using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
+
+class websocket_session: public std::enable_shared_from_this<websocket_session>
+{
+ websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws_;
+ beast::flat_buffer buffer_;
+
+public:
+ explicit websocket_session(beast::ssl_stream<beast::tcp_stream>&& stream) :
+ ws_(std::move(stream))
+ {
+ }
+
+ // Start the asynchronous accept operation
+ template<class Body, class Allocator>
+ void
+ do_accept(http::request<Body, http::basic_fields<Allocator>> req)
+ {
+ // Set suggested timeout settings for the websocket
+ ws_.set_option(
+ websocket::stream_base::timeout::suggested(
+ beast::role_type::server));
+
+ // Set a decorator to change the Server of the handshake
+ ws_.set_option(websocket::stream_base::decorator(
+ [](websocket::response_type& res)
+ {
+ res.set(http::field::server,
+ std::string{"Reichwein.IT Webserver"});
+ }));
+
+ // Accept the websocket handshake
+ ws_.async_accept(
+ req,
+ beast::bind_front_handler(
+ &websocket_session::on_accept,
+ shared_from_this()));
+ }
+
+private:
+ void
+ on_accept(beast::error_code ec)
+ {
+ if(ec)
+ return fail(ec, "accept");
+
+ // Read a message
+ do_read();
+ }
+
+ void
+ do_read()
+ {
+ // Read a message into our buffer
+ ws_.async_read(
+ buffer_,
+ beast::bind_front_handler(
+ &websocket_session::on_read,
+ shared_from_this()));
+ }
+
+ void
+ on_read(
+ beast::error_code ec,
+ std::size_t bytes_transferred)
+ {
+ boost::ignore_unused(bytes_transferred);
+
+ // This indicates that the websocket_session was closed
+ if(ec == websocket::error::closed)
+ return;
+
+ if(ec)
+ fail(ec, "read");
+
+ // Echo the message
+ ws_.text(ws_.got_text());
+ std::string data(boost::asio::buffers_begin(buffer_.data()), boost::asio::buffers_end(buffer_.data()));
+ static int count{};
+ data += ": " + std::to_string(count++);
+ buffer_.consume(buffer_.size());
+ boost::beast::ostream(buffer_) << data;
+ ws_.async_write(
+ buffer_.data(),
+ beast::bind_front_handler(
+ &websocket_session::on_write,
+ shared_from_this()));
+ }
+
+ void
+ on_write(
+ beast::error_code ec,
+ std::size_t bytes_transferred)
+ {
+ boost::ignore_unused(bytes_transferred);
+
+ if(ec)
+ return fail(ec, "write");
+
+ // Clear the buffer
+ buffer_.consume(buffer_.size());
+
+ // Do another read
+ do_read();
+ }
+};
+