From dc2e2b3e293a8374a2627982b521cc6865129c49 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Mon, 9 Jan 2023 13:15:18 +0100 Subject: Separated out websocket --- tests/Makefile | 8 +- tests/test-config.cpp | 1 + tests/test-webserver.cpp | 304 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 216 insertions(+), 97 deletions(-) (limited to 'tests') diff --git a/tests/Makefile b/tests/Makefile index 5f162de..14af291 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -36,6 +36,7 @@ LDFLAGS+=-pie UNITS=\ auth.cpp \ config.cpp \ + error.cpp \ http.cpp \ https.cpp \ plugin.cpp \ @@ -43,7 +44,8 @@ UNITS=\ response.cpp \ statistics.cpp \ server.cpp \ - webserver.cpp + webserver.cpp \ + websocket.cpp TESTSRC=\ test-auth.cpp \ @@ -85,6 +87,8 @@ auth.o: ../auth.cpp $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ config.o: ../config.cpp $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ +error.o: ../error.cpp + $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ http.o: ../http.cpp $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ https.o: ../https.cpp @@ -101,6 +105,8 @@ server.o: ../server.cpp $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ webserver.o: ../webserver.cpp $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ +websocket.o: ../websocket.cpp + $(CXX) $(CXXFLAGS) $(CXXTESTFLAGS) -c $< -o $@ ADD_DEP=Makefile diff --git a/tests/test-config.cpp b/tests/test-config.cpp index fe482f8..eb7e9c7 100644 --- a/tests/test-config.cpp +++ b/tests/test-config.cpp @@ -31,6 +31,7 @@ public: { std::error_code ec; fs::remove(testConfigFilename); + fs::remove("stats.db"); } }; diff --git a/tests/test-webserver.cpp b/tests/test-webserver.cpp index 7059bc6..6bbf302 100644 --- a/tests/test-webserver.cpp +++ b/tests/test-webserver.cpp @@ -3,11 +3,6 @@ #define BOOST_TEST_MODULE webserver_test #include -// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67) -#if BOOST_VERSION >= 107100 -#define BOOST_LATEST -#endif - #include #include #include @@ -17,9 +12,7 @@ #include #include #include -#ifdef BOOST_LATEST #include -#endif #include #include #include @@ -47,6 +40,7 @@ #include #include +#include #include "webserver.h" @@ -211,31 +205,17 @@ VZTqPHmb+db0rFA3XlAg2A== m_filebuf = 0; } - bool isRunning() + bool is_running() { if (m_pid == 0) return false; - fs::path pid_file{fmt::format("/proc/{}/stat", m_pid)}; - if (!fs::exists(pid_file)) - return false; - - std::string s{File::getFile(pid_file)}; - - auto pos0{s.find(' ', 0)}; - pos0 = s.find(' ', pos0 + 1); - pos0++; - - auto pos1{s.find(' ', pos0 + 1)}; - - std::string state{s.substr(pos0, pos1 - pos0)}; - - return state == "R" || state == "S"; + return Reichwein::Process::is_running(m_pid); } std::string output() { - if (!isRunning()) + if (!is_running()) throw std::runtime_error("No output/stdout available from webserver since it is not running"); if (!m_is) @@ -255,7 +235,7 @@ private: // child stdout std::shared_ptr<__gnu_cxx::stdio_filebuf> m_filebuf; std::shared_ptr m_is; -}; +}; // class WebserverProcess std::pair HTTP(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get) { @@ -331,13 +311,7 @@ std::pair HTTPS(const std::string& target, bool ipv6 = boost::asio::io_context ioc; // The SSL context is required, and holds certificates - boost::asio::ssl::context ctx( -#ifdef BOOST_LATEST - boost::asio::ssl::context::tlsv13_client -#else - boost::asio::ssl::context::tlsv12_client -#endif - ); + boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client); // This holds the root certificate used for verification load_root_certificates(ctx); @@ -410,16 +384,19 @@ class Fixture { public: Fixture(){} - ~Fixture(){} + ~Fixture() + { + fs::remove("stats.db"); + } }; BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make({false, true}) * data::make({false, true}) * data::make({boost::beast::http::verb::head, boost::beast::http::verb::get}), ipv6, http11, https, method) { WebserverProcess serverProcess; - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); std::pair response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)}; - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); std::string::size_type size{File::getFile(testConfigFilename).size()}; BOOST_CHECK_GT(size, 0); BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size)); @@ -427,7 +404,7 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get, data::make({false, true}) * data::make for (int i = 0; i < 10; i++) { std::pair response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)}; - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 200 OK\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: application/text\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : size)); BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? ""s : File::getFile(testConfigFilename)); } @@ -437,92 +414,227 @@ BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, data::make({false, true { WebserverProcess serverProcess; - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); BOOST_REQUIRE(!fs::exists("./webserver.confSUFFIX")); auto response{(https ? HTTPS("/webserver.confSUFFIX", ipv6, http11, method) : HTTP("/webserver.confSUFFIX", ipv6, http11, method))}; - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 404 Not Found\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n", http11 ? "1.1" : "1.0", method == boost::beast::http::verb::head ? 0 : 36)); BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? "" : "404 Not found: /webserver.confSUFFIX"); } -BOOST_FIXTURE_TEST_CASE(websocket, Fixture) +// Test server +class WebsocketServerProcess { - WebserverProcess serverProcess; - BOOST_REQUIRE(serverProcess.isRunning()); +public: + WebsocketServerProcess() + { + start(); + } + + ~WebsocketServerProcess() + { + stop(); + } + + // Echoes back all received WebSocket messages + void do_session(boost::asio::ip::tcp::socket socket) + { + try + { + // Construct the stream by moving in the socket + boost::beast::websocket::stream ws{std::move(socket)}; + + // Set a decorator to change the Server of the handshake + ws.set_option(boost::beast::websocket::stream_base::decorator( + [](boost::beast::websocket::response_type& res) + { + res.set(boost::beast::http::field::server, + std::string("Reichwein.IT Test Websocket Server")); + })); + + // Accept the websocket handshake + ws.accept(); + + for(;;) + { + // This buffer will hold the incoming message + boost::beast::flat_buffer buffer; + + // Read a message + ws.read(buffer); + + // Echo the message back + ws.text(ws.got_text()); + std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data())); + data += ": " + std::to_string(m_count++); + buffer.consume(buffer.size()); + boost::beast::ostream(buffer) << data; + ws.write(buffer.data()); + } + } + catch(boost::beast::system_error const& se) + { + // This indicates that the session was closed + if(se.code() != boost::beast::websocket::error::closed) + std::cerr << "Error: " << se.code().message() << std::endl; + } + catch(std::exception const& e) + { + std::cerr << "Error: " << e.what() << std::endl; + } + } + + bool is_running() + { + if (m_pid == 0) + return false; + + return Reichwein::Process::is_running(m_pid); + } + + void start() + { + if (m_pid != 0) + throw std::runtime_error("Process already running, so it can't be started"); + + // connect stdout of new child process to stream of parent, via pipe + m_pid = fork(); + if (m_pid < 0) + throw std::runtime_error("Fork unsuccessful."); + + if (m_pid == 0) { // child process branch + while (true) { + try + { + auto const address = boost::asio::ip::make_address("localhost"); + auto const port = static_cast(9876); + + // The io_context is required for all I/O + boost::asio::io_context ioc{1}; + + // The acceptor receives incoming connections + boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}}; + for(;;) + { + // This will receive the new connection + boost::asio::ip::tcp::socket socket{ioc}; + + // Block until we get a connection + acceptor.accept(socket); + + // Launch the session, transferring ownership of the socket + std::thread( + &WebsocketServerProcess::do_session, this, + std::move(socket)).detach(); + } + } + catch (const std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + } + } + exit(0); + } + } + + void stop() + { + if (!is_running()) + throw std::runtime_error("Process not running, so it can't be stopped"); + + if (kill(m_pid, SIGKILL) != 0) + throw std::runtime_error("Unable to kill process"); + + if (int result = waitpid(m_pid, NULL, 0); result != m_pid) + throw std::runtime_error("waitpid returned "s + std::to_string(result)); + + m_pid = 0; + } +private: + int m_pid{}; + int m_count{}; +}; // class WebsocketServerProcess - std::string host = "::1"; - auto const port = "8081" ; - auto const text = "request1"; +BOOST_FIXTURE_TEST_CASE(websocket, Fixture) +{ + WebserverProcess serverProcess; + BOOST_REQUIRE(serverProcess.is_running()); + + WebsocketServerProcess websocketProcess; + BOOST_REQUIRE(websocketProcess.is_running()); - // The io_context is required for all I/O - boost::asio::io_context ioc; + std::string host = "::1"; + auto const port = "8081" ; + auto const text = "request1"; - // The SSL context is required, and holds certificates - boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client}; + // The io_context is required for all I/O + boost::asio::io_context ioc; - // This holds the root certificate used for verification - load_root_certificates(ctx); + // The SSL context is required, and holds certificates + boost::asio::ssl::context ctx{boost::asio::ssl::context::tlsv13_client}; - // These objects perform our I/O - boost::asio::ip::tcp::resolver resolver{ioc}; - boost::beast::websocket::stream> ws{ioc, ctx}; + // This holds the root certificate used for verification + load_root_certificates(ctx); - // Look up the domain name - auto const results = resolver.resolve(host, port); + // These objects perform our I/O + boost::asio::ip::tcp::resolver resolver{ioc}; + boost::beast::websocket::stream> ws{ioc, ctx}; - // Make the connection on the IP address we get from a lookup - auto ep = boost::asio::connect(get_lowest_layer(ws), results); + // Look up the domain name + auto const results = resolver.resolve(host, port); - // 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"); + // Make the connection on the IP address we get from a lookup + auto ep = boost::asio::connect(get_lowest_layer(ws), results); - // 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()); + // 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); + // 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"); - })); + // 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("Reichwein.IT Test Websocket Client")); + })); - // Perform the websocket handshake - ws.handshake(host, "/"); + // Perform the websocket handshake + ws.handshake(host, "/"); - // Send the message - ws.write(boost::asio::buffer(std::string(text))); + // Send the message + ws.write(boost::asio::buffer(std::string(text))); - // This buffer will hold the incoming message - boost::beast::flat_buffer buffer; + // 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"); + // 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()); + 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"); + 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); + // Close the WebSocket connection + ws.close(boost::beast::websocket::close_code::normal); - BOOST_REQUIRE(serverProcess.isRunning()); + BOOST_REQUIRE(serverProcess.is_running()); } -- cgit v1.2.3