diff options
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | debian/changelog | 2 | ||||
-rw-r--r-- | error.cpp | 32 | ||||
-rw-r--r-- | error.h | 6 | ||||
-rw-r--r-- | http.cpp | 105 | ||||
-rw-r--r-- | https.cpp | 270 | ||||
-rw-r--r-- | https.h | 11 | ||||
-rw-r--r-- | plugin.cpp | 9 | ||||
-rw-r--r-- | server.cpp | 10 | ||||
-rw-r--r-- | tests/Makefile | 8 | ||||
-rw-r--r-- | tests/test-config.cpp | 1 | ||||
-rw-r--r-- | tests/test-webserver.cpp | 304 | ||||
-rw-r--r-- | websocket.cpp | 2 | ||||
-rw-r--r-- | websocket.h | 141 |
14 files changed, 410 insertions, 499 deletions
@@ -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"; +} + @@ -0,0 +1,6 @@ +#pragma once + +#include <boost/beast/core/error.hpp> + +void fail(boost::beast::error_code ec, char const* what); + @@ -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(); } @@ -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(); } @@ -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 { @@ -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); @@ -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(); + } +}; + |