diff options
| author | Roland Reichwein <mail@reichwein.it> | 2023-01-09 10:38:29 +0100 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2023-01-09 10:38:29 +0100 | 
| commit | d747193e76baf689211d9f1e42335360288d43c0 (patch) | |
| tree | 91d3911b94f5e9c5091d9ab978bf4f9b4046d626 | |
| parent | 7b3fe2cc608928df3b885168a0676a771f7bc7be (diff) | |
First websockets test via https
| -rw-r--r-- | TODO | 1 | ||||
| -rw-r--r-- | debian/changelog | 6 | ||||
| -rw-r--r-- | https.cpp | 116 | ||||
| -rw-r--r-- | plugin_interface.h | 1 | ||||
| -rw-r--r-- | tests/test-webserver.cpp | 85 | 
5 files changed, 208 insertions, 1 deletions
@@ -1,6 +1,7 @@  Big file bug  Websockets +FastCGI from command line  stats.png  cgi unhandled headers  git via smart http / cgi diff --git a/debian/changelog b/debian/changelog index 9fb309e..fdaa32c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +webserver (1.18) UNRELEASED; urgency=medium + +  *  + + -- Roland Reichwein <mail@reichwein.it>  Sun, 08 Jan 2023 15:26:48 +0100 +  webserver (1.17) unstable; urgency=medium    * Automated date handling (year) @@ -12,6 +12,9 @@  #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/asio/buffers_iterator.hpp>  #include <boost/asio/dispatch.hpp>  #include <boost/asio/ssl/context.hpp>  #ifdef BOOST_LATEST @@ -41,6 +44,7 @@ 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>  using namespace Reichwein; @@ -82,6 +86,111 @@ void fail(      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>  { @@ -265,6 +374,13 @@ public:              return fail(ec, "https read");          req_ = parser_->get(); + +        if (websocket::is_upgrade(req_)) +        { +         beast::get_lowest_layer(stream_).expires_never(); +         std::make_shared<websocket_session>(std::move(stream_))->do_accept(parser_->release()); +         return; +        }          // Send the response          handle_request(m_server, std::move(req_)); diff --git a/plugin_interface.h b/plugin_interface.h index 830c44c..13e5f53 100644 --- a/plugin_interface.h +++ b/plugin_interface.h @@ -16,7 +16,6 @@ public:   //   // The Interface to be implemented by plugins   // - //   virtual std::string name() = 0; diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp index ef3b15f..7059bc6 100644 --- a/tests/test-webserver.cpp +++ b/tests/test-webserver.cpp @@ -15,10 +15,14 @@  #include <boost/algorithm/string.hpp>  #include <boost/beast/core.hpp>  #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>  #include <boost/asio/connect.hpp>  #include <boost/asio/ip/tcp.hpp>  #include <boost/asio/ssl/error.hpp> @@ -441,3 +445,84 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, data::make({false, true   BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? "" : "404 Not found: /webserver.confSUFFIX");  } +BOOST_FIXTURE_TEST_CASE(websocket, Fixture) +{ + WebserverProcess serverProcess; + BOOST_REQUIRE(serverProcess.isRunning()); + + +        std::string host = "::1"; +        auto const  port = "8081" ; +        auto const  text = "request1"; + +        // The io_context is required for all I/O +        boost::asio::io_context ioc; + +        // The SSL context is required, and holds certificates +        boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client}; + +        // This holds the root certificate used for verification +        load_root_certificates(ctx); + +        // 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}; + +        // Look up the domain name +        auto const results = resolver.resolve(host, port); + +        // Make the connection on the IP address we get from a lookup +        auto ep = boost::asio::connect(get_lowest_layer(ws), results); + +        // 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); + +        // 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"); +            })); + +        // Perform the websocket handshake +        ws.handshake(host, "/"); + +        // Send the message +        ws.write(boost::asio::buffer(std::string(text))); + +        // 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"); + +        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"); + +        // Close the WebSocket connection +        ws.close(boost::beast::websocket::close_code::normal); + + BOOST_REQUIRE(serverProcess.isRunning()); +} +  | 
