From 00ed7df1a09cad8862f2c586347f4f55c99681e5 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Thu, 12 Jan 2023 15:30:07 +0100 Subject: Consolidate HTTP+HTTPS via CRTP --- websocket.cpp | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) (limited to 'websocket.cpp') diff --git a/websocket.cpp b/websocket.cpp index 37d4b59..68ff194 100644 --- a/websocket.cpp +++ b/websocket.cpp @@ -1,5 +1,307 @@ +// +// Websocket, implemented via CRTP for both plain and ssl websockets +// #include "websocket.h" +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 std::placeholders; + +// 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::flat_buffer buffer_in_; + boost::beast::websocket::stream ws_app_; + boost::beast::flat_buffer buffer_out_; + std::string host_; + std::string port_; + std::string subprotocol_; + std::string relative_target_; + +public: + explicit websocket_session(boost::asio::io_context& ioc, std::string&& websocket_address): + ioc_(ioc), + resolver_(boost::asio::make_strand(ioc_)), + ws_app_(boost::asio::make_strand(ioc_)), + host_{}, + port_{}, + subprotocol_{}, + relative_target_{} + { + // Parse websocket address host:port : + + auto colon_pos{websocket_address.find_last_of(':')}; + + if (colon_pos == std::string::npos) { + std::cerr << "Warning: Bad websocket address (colon missing): " << websocket_address << std::endl; + return; + } + + auto slash_pos{websocket_address.find('/')}; + if (slash_pos == std::string::npos) { + std::cerr << "Warning: Bad websocket address (slash missing): " << websocket_address << std::endl; + return; + } + if (slash_pos <= colon_pos) { + std::cerr << "Warning: Bad websocket address: " << websocket_address << std::endl; + return; + } + + host_ = websocket_address.substr(0, colon_pos); + port_ = websocket_address.substr(colon_pos + 1, slash_pos - (colon_pos + 1)); + relative_target_ = websocket_address.substr(slash_pos); + } + + // + // The initial setup path + // + + // Start the asynchronous accept operation + void do_accept_in(request_type req) + { + // Set suggested timeout settings for the websocket + derived().ws_in().set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::server)); + + // Set a decorator to change the Server of the handshake + derived().ws_in().set_option(websocket::stream_base::decorator( + [](websocket::response_type& res) + { + res.set(http::field::server, + std::string{"Reichwein.IT Webserver"}); + })); + + // Forward subprotocol from request to target websocket + subprotocol_ = std::string{req[http::field::sec_websocket_protocol]}; + + // Accept the websocket handshake + derived().ws_in().async_accept( + req, + beast::bind_front_handler( + &websocket_session::on_accept_in, + derived().shared_from_this())); + } + +private: + void on_accept_in(beast::error_code ec) + { + if (ec) + return fail(ec, "accept in"); + + resolver_.async_resolve(host_, port_, + 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) + { + if (ec) + return fail(ec, "resolve app"); + + beast::get_lowest_layer(ws_app_).async_connect(results, + 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) + { + if (ec) + return fail(ec, "connect app"); + + beast::get_lowest_layer(ws_app_).expires_never(); + + host_ += ':' + std::to_string(endpoint.port()); + + // Set suggested timeout settings for the websocket + ws_app_.set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::client)); + + ws_app_.set_option(boost::beast::websocket::stream_base::decorator( + [](boost::beast::websocket::request_type& req) + { + req.set(boost::beast::http::field::user_agent, "Reichwein.IT Webserver Websocket client"); + })); + + ws_app_.set_option(boost::beast::websocket::stream_base::decorator( + [this](boost::beast::websocket::request_type& req) + { + req.set(boost::beast::http::field::sec_websocket_protocol, subprotocol_); + })); + + ws_app_.async_handshake(host_, relative_target_, + beast::bind_front_handler(&websocket_session::on_handshake_app, derived().shared_from_this())); + } + + void on_handshake_app(beast::error_code ec) + { + if (ec) + return fail(ec, "handshake app"); + + // Start reading messages from both sides, asynchronously + do_read_in(); + do_read_app(); + } + + // + // The input path (client,ws_in_ -> app,ws_app_) via + // + + void + do_read_in() + { + // Read a message into our buffer + derived().ws_in().async_read( + buffer_in_, + beast::bind_front_handler( + &websocket_session::on_read_in, + derived().shared_from_this())); + } + + void + on_read_in( + 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 in"); + + ws_app_.text(derived().ws_in().got_text()); + + do_write_app(); + } + + void do_write_app() + { + ws_app_.async_write(buffer_in_.data(), + beast::bind_front_handler( + &websocket_session::on_write_app, + derived().shared_from_this())); + } + + void on_write_app(beast::error_code ec, std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if (ec) + fail(ec, "write app"); + + buffer_in_.consume(buffer_in_.size()); + + // Do another read + do_read_in(); + } + + // + // The output path (app,ws_app_ -> client,ws_in_) + // + + void do_read_app() + { + // Read a message into our buffer + ws_app_.async_read( + buffer_out_, + beast::bind_front_handler( + &websocket_session::on_read_app, + derived().shared_from_this())); + } + + void on_read_app(beast::error_code ec, std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if (ec == websocket::error::closed) + return; + + if (ec) + fail(ec, "read app"); + + do_write_out(); + } + + void do_write_out() + { + derived().ws_in().async_write(buffer_out_.data(), + beast::bind_front_handler( + &websocket_session::on_write_out, + derived().shared_from_this())); + } + + void on_write_out( + beast::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if(ec) + return fail(ec, "write out"); + + // Clear the buffer + buffer_out_.consume(buffer_out_.size()); + + // Do another read + do_read_app(); + } + +}; // 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) { -- cgit v1.2.3