From d747193e76baf689211d9f1e42335360288d43c0 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Mon, 9 Jan 2023 10:38:29 +0100 Subject: First websockets test via https --- TODO | 1 + debian/changelog | 6 +++ https.cpp | 116 +++++++++++++++++++++++++++++++++++++++++++++++ plugin_interface.h | 1 - tests/test-webserver.cpp | 85 ++++++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index b5106c3..53c7c5b 100644 --- a/TODO +++ b/TODO @@ -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 Sun, 08 Jan 2023 15:26:48 +0100 + webserver (1.17) unstable; urgency=medium * Automated date handling (year) diff --git a/https.cpp b/https.cpp index 523acb5..ccf14d7 100644 --- a/https.cpp +++ b/https.cpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include #include #ifdef BOOST_LATEST @@ -41,6 +44,7 @@ namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from +namespace websocket = beast::websocket; using tcp = boost::asio::ip::tcp; // from 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::stream> ws_; + beast::flat_buffer buffer_; + +public: + explicit websocket_session(beast::ssl_stream&& stream) : + ws_(std::move(stream)) + { + } + + // Start the asynchronous accept operation + template + void + do_accept(http::request> 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 { @@ -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(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 #include #include +#include +#include #ifdef BOOST_LATEST #include #endif #include +#include +#include #include #include #include @@ -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> 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(::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()); +} + -- cgit v1.2.3