From d747193e76baf689211d9f1e42335360288d43c0 Mon Sep 17 00:00:00 2001
From: Roland Reichwein <mail@reichwein.it>
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 <mail@reichwein.it>  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 <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());
+}
+
-- 
cgit v1.2.3