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(); +    } +}; +  | 
