diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/Makefile | 8 | ||||
| -rw-r--r-- | tests/test-config.cpp | 1 | ||||
| -rw-r--r-- | tests/test-webserver.cpp | 304 | 
3 files changed, 216 insertions, 97 deletions
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 <boost/test/included/unit_test.hpp> -// Support both boost in Debian unstable (BOOST_LATEST) and in stable (boost 1.67) -#if BOOST_VERSION >= 107100 -#define BOOST_LATEST -#endif -  #include <boost/test/data/dataset.hpp>  #include <boost/test/data/monomorphic.hpp>  #include <boost/test/data/test_case.hpp> @@ -17,9 +12,7 @@  #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> @@ -47,6 +40,7 @@  #include <unistd.h>  #include <libreichwein/file.h> +#include <libreichwein/process.h>  #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<char>> m_filebuf;   std::shared_ptr<std::istream> m_is; -}; +}; // class WebserverProcess  std::pair<std::string,std::string> 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<std::string,std::string> 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<std::string,std::string> 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<std::string,std::string> 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<boost::asio::ip::tcp::socket> 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<unsigned short>(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<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> 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<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>> 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<int>(::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<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); + // 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());  }  | 
