From deb28a9ce73ed7e38aaa53659027b61976fdca6b Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Wed, 11 Jan 2023 18:36:43 +0100 Subject: Websocket for both http and https --- TODO | 4 +-- http.cpp | 24 +++++++++++-- https.cpp | 2 +- tests/test-webserver.cpp | 29 ++++----------- websocket.cpp | 10 ++++++ websocket.h | 94 +++++++++++++++++++++++++++++++++++++----------- 6 files changed, 113 insertions(+), 50 deletions(-) diff --git a/TODO b/TODO index f1ff320..fc4e4a8 100644 --- a/TODO +++ b/TODO @@ -5,9 +5,7 @@ test: - Redirect Big file bug - dynamic plugin interface (file buffer, ...) -Websockets -- http+https -http+https=CRTP +CRTP: http+https FastCGI from command line stats.png diff --git a/http.cpp b/http.cpp index 4738ad8..469f1aa 100644 --- a/http.cpp +++ b/http.cpp @@ -4,6 +4,7 @@ #include "server.h" #include "response.h" +#include "websocket.h" #include #include @@ -27,6 +28,7 @@ namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from +namespace websocket = beast::websocket; namespace { @@ -41,6 +43,7 @@ void fail(beast::error_code ec, char const* what) // Handles an HTTP server connection class session : public std::enable_shared_from_this { + boost::asio::io_context& ioc_; beast::tcp_stream stream_; beast::flat_buffer buffer_; Server& m_server; @@ -65,13 +68,21 @@ class session : public std::enable_shared_from_this sp->need_eof())); } + void handle_websocket() + { + beast::get_lowest_layer(stream_).expires_never(); + make_websocket_session(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server), parser_->release()); + } + public: // Take ownership of the stream session( + boost::asio::io_context& ioc, tcp::socket&& socket, - Server& server) - : stream_(std::move(socket)) - , m_server(server) + Server& server): + ioc_(ioc), + stream_(std::move(socket)), + m_server(server) { } @@ -127,6 +138,12 @@ public: req_ = parser_->get(); + if (websocket::is_upgrade(req_)) + { + handle_websocket(); + return; + } + // Send the response handle_request(m_server, std::move(req_)); } @@ -251,6 +268,7 @@ private: { // Create the session and run it std::make_shared( + ioc_, std::move(socket), m_server)->run(); } diff --git a/https.cpp b/https.cpp index 5d19d5b..6b32195 100644 --- a/https.cpp +++ b/https.cpp @@ -79,7 +79,7 @@ class session : public std::enable_shared_from_this void handle_websocket() { beast::get_lowest_layer(stream_).expires_never(); - std::make_shared(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server))->do_accept_in(parser_->release()); + make_websocket_session(ioc_, std::move(stream_), response::get_websocket_address(req_, m_server), parser_->release()); } public: diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp index e9e6df5..077c27e 100644 --- a/tests/test-webserver.cpp +++ b/tests/test-webserver.cpp @@ -202,7 +202,7 @@ public: } // wait for server to start up - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); } void stop() @@ -611,7 +611,7 @@ private: std::unique_ptr> m_shared; }; // class WebsocketServerProcess -BOOST_FIXTURE_TEST_CASE(websocket, Fixture) +BOOST_FIXTURE_TEST_CASE(websocket_ssl, Fixture) { std::string webserver_config{R"CONFIG( www-data @@ -752,7 +752,7 @@ BOOST_FIXTURE_TEST_CASE(websocket, Fixture) BOOST_REQUIRE(websocketProcess.is_running()); } -BOOST_FIXTURE_TEST_CASE(websocket_subprotocol, Fixture) +BOOST_FIXTURE_TEST_CASE(websocket_plain_subprotocol, Fixture) { std::string webserver_config{R"CONFIG( www-data @@ -810,35 +810,21 @@ BOOST_FIXTURE_TEST_CASE(websocket_subprotocol, Fixture) BOOST_REQUIRE(websocketProcess.is_running()); std::string host = "::1"; - auto const port = "8081" ; + auto const port = "8080" ; 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}; + boost::beast::websocket::stream ws{ioc}; // 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"); + auto ep = boost::asio::connect(boost::beast::get_lowest_layer(ws), results); // Update the host_ string. This will provide the value of the // Host HTTP header during the WebSocket handshake. @@ -847,9 +833,6 @@ BOOST_FIXTURE_TEST_CASE(websocket_subprotocol, Fixture) host = "[" + host + "]"; 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) diff --git a/websocket.cpp b/websocket.cpp index a4c8dfb..37d4b59 100644 --- a/websocket.cpp +++ b/websocket.cpp @@ -1,2 +1,12 @@ #include "websocket.h" + +void make_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string websocket_address, request_type&& req) +{ + std::make_shared(ioc, std::move(stream), std::move(websocket_address))->do_accept_in(std::move(req)); +} + +void make_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream&& stream, std::string websocket_address, request_type&& req) +{ + std::make_shared(ioc, std::move(stream), std::move(websocket_address))->do_accept_in(std::move(req)); +} diff --git a/websocket.h b/websocket.h index 951155e..b941433 100644 --- a/websocket.h +++ b/websocket.h @@ -1,6 +1,10 @@ +// +// Websocket, implemented via CRTP for both plain and ssl websockets +// #pragma once #include "error.h" +#include "response.h" #include #include @@ -36,12 +40,18 @@ namespace websocket = beast::websocket; using tcp = boost::asio::ip::tcp; // from using namespace std::placeholders; -// Server session, asynchronous, proxying -class websocket_session: public std::enable_shared_from_this +// Server session, asynchronous, proxying, implemented w/ CRTP for plain+ssl variants +template +class websocket_session { +private: + Derived& derived() + { + return static_cast(*this); + } + boost::asio::io_context& ioc_; boost::asio::ip::tcp::resolver resolver_; - boost::beast::websocket::stream> ws_in_; boost::beast::flat_buffer buffer_in_; boost::beast::websocket::stream ws_app_; boost::beast::flat_buffer buffer_out_; @@ -50,11 +60,10 @@ class websocket_session: public std::enable_shared_from_this std::string subprotocol_; std::string relative_target_; -public: - explicit websocket_session(boost::asio::io_context& ioc, beast::ssl_stream&& stream, const std::string& websocket_address): +public: + explicit websocket_session(boost::asio::io_context& ioc, std::string&& websocket_address): ioc_(ioc), resolver_(boost::asio::make_strand(ioc_)), - ws_in_(std::move(stream)), ws_app_(boost::asio::make_strand(ioc_)), host_{}, port_{}, @@ -90,16 +99,17 @@ public: // // Start the asynchronous accept operation + // TODO: why template here? template void do_accept_in(http::request> req) { // Set suggested timeout settings for the websocket - ws_in_.set_option( + derived().ws_in().set_option( websocket::stream_base::timeout::suggested( beast::role_type::server)); // Set a decorator to change the Server of the handshake - ws_in_.set_option(websocket::stream_base::decorator( + derived().ws_in().set_option(websocket::stream_base::decorator( [](websocket::response_type& res) { res.set(http::field::server, @@ -110,11 +120,11 @@ public: subprotocol_ = std::string{req[http::field::sec_websocket_protocol]}; // Accept the websocket handshake - ws_in_.async_accept( + derived().ws_in().async_accept( req, beast::bind_front_handler( &websocket_session::on_accept_in, - shared_from_this())); + derived().shared_from_this())); } private: @@ -124,7 +134,7 @@ private: return fail(ec, "accept in"); resolver_.async_resolve(host_, port_, - beast::bind_front_handler(&websocket_session::on_resolve_app, shared_from_this())); + beast::bind_front_handler(&websocket_session::on_resolve_app, derived().shared_from_this())); } void on_resolve_app(beast::error_code ec, tcp::resolver::results_type results) @@ -133,7 +143,7 @@ private: return fail(ec, "resolve app"); beast::get_lowest_layer(ws_app_).async_connect(results, - beast::bind_front_handler(&websocket_session::on_connect_app, shared_from_this())); + beast::bind_front_handler(&websocket_session::on_connect_app, derived().shared_from_this())); } void on_connect_app(beast::error_code ec, tcp::resolver::results_type::endpoint_type endpoint) @@ -163,7 +173,7 @@ private: })); ws_app_.async_handshake(host_, relative_target_, - beast::bind_front_handler(&websocket_session::on_handshake_app, shared_from_this())); + beast::bind_front_handler(&websocket_session::on_handshake_app, derived().shared_from_this())); } void on_handshake_app(beast::error_code ec) @@ -184,11 +194,11 @@ private: do_read_in() { // Read a message into our buffer - ws_in_.async_read( + derived().ws_in().async_read( buffer_in_, beast::bind_front_handler( &websocket_session::on_read_in, - shared_from_this())); + derived().shared_from_this())); } void @@ -205,7 +215,7 @@ private: if (ec) fail(ec, "read in"); - ws_app_.text(ws_in_.got_text()); + ws_app_.text(derived().ws_in().got_text()); do_write_app(); } @@ -215,7 +225,7 @@ private: ws_app_.async_write(buffer_in_.data(), beast::bind_front_handler( &websocket_session::on_write_app, - shared_from_this())); + derived().shared_from_this())); } void on_write_app(beast::error_code ec, std::size_t bytes_transferred) @@ -242,7 +252,7 @@ private: buffer_out_, beast::bind_front_handler( &websocket_session::on_read_app, - shared_from_this())); + derived().shared_from_this())); } void on_read_app(beast::error_code ec, std::size_t bytes_transferred) @@ -260,10 +270,10 @@ private: void do_write_out() { - ws_in_.async_write(buffer_out_.data(), + derived().ws_in().async_write(buffer_out_.data(), beast::bind_front_handler( &websocket_session::on_write_out, - shared_from_this())); + derived().shared_from_this())); } void on_write_out( @@ -283,3 +293,47 @@ private: } }; // class + +class plain_websocket_session: + public websocket_session, + public std::enable_shared_from_this +{ + boost::beast::websocket::stream ws_in_; + +public: + + explicit plain_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string&& websocket_address): + websocket_session(ioc, std::move(websocket_address)), + ws_in_(std::move(stream)) + { + } + + boost::beast::websocket::stream& ws_in() + { + return ws_in_; + } +}; // class + +class ssl_websocket_session: + public websocket_session, + public std::enable_shared_from_this +{ + boost::beast::websocket::stream> ws_in_; + +public: + + explicit ssl_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream&& stream, std::string&& websocket_address): + websocket_session(ioc, std::move(websocket_address)), + ws_in_(std::move(stream)) + { + } + + boost::beast::websocket::stream>& ws_in() + { + return ws_in_; + } +}; // class + +void make_websocket_session(boost::asio::io_context& ioc, beast::tcp_stream&& stream, std::string websocket_address, request_type&& req); +void make_websocket_session(boost::asio::io_context& ioc, beast::ssl_stream&& stream, std::string websocket_address, request_type&& req); + -- cgit v1.2.3