// Main unit test compilation unit // boost mandates that exactly one compilation unit contains the following two lines: #define BOOST_TEST_MODULE webserver_test #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "webserver.h" #include "response.h" using namespace std::string_literals; namespace fs = std::filesystem; namespace pt = boost::property_tree; using namespace boost::unit_test; using namespace Reichwein; const fs::path testConfigFilename{"./webserver.conf"}; const fs::path testCertFilename{"./testchain.pem"}; const fs::path testKeyFilename{"./testkey.pem"}; // tcp: tcp or tcp6 bool tcp_is_pid_listening_on(const std::string& tcp, pid_t pid, int port) { std::string filename{fmt::format("/proc/{}/net/{}", pid, tcp)}; std::ifstream f{filename, std::ios::in}; // e.g.: // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode // 0: 00000000:C799 00000000:0000 0A 00000000:00000000 00:00000000 00000000 107 0 21869 1 00000000335416a4 100 0 0 10 0 std::string s; std::getline(f, s); // skip head line while (std::getline(f, s)) { boost::algorithm::trim_left(s); size_t pos_space1{s.find(' ')}; if (pos_space1 == std::string::npos) throw std::runtime_error("Expected first space in " + filename); size_t pos_colon1{s.find(':', pos_space1 + 1)}; if (pos_colon1 == std::string::npos) throw std::runtime_error("Expected first colon in " + filename); size_t pos_space2{s.find(' ', pos_colon1 + 1)}; if (pos_space2 == std::string::npos) throw std::runtime_error("Expected second space in " + filename); std::string port_s{s.substr(pos_colon1 + 1, pos_space2 - (pos_colon1 + 1))}; auto current_port{std::stoul(port_s, nullptr, 16)}; if (current_port != port) continue; // now, we are in a line related to matching local port size_t pos_space3{s.find(' ', pos_space2 + 1)}; if (pos_space3 == std::string::npos) throw std::runtime_error("Expected third space in " + filename); size_t pos_space4{s.find(' ', pos_space3 + 1)}; if (pos_space4 == std::string::npos) throw std::runtime_error("Expected fourth space in " + filename); std::string state_s{s.substr(pos_space3 + 1, pos_space4 - (pos_space3 + 1))}; if (state_s == "0A") // listening state TCP_LISTEN, from net/tcp_states.h return true; } return false; // not found } bool is_pid_listening_on(pid_t pid, int port) { return tcp_is_pid_listening_on("tcp", pid, port) || tcp_is_pid_listening_on("tcp6", pid, port); } void wait_for_pid_listening_on(pid_t pid, int port) { while (!is_pid_listening_on(pid, port)) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } // returns -1 if no port found in config int port_from_config(const std::string& config) { pt::ptree tree; std::istringstream stream{config}; pt::read_xml(stream, tree); try { return tree.get("webserver.sockets.socket.port"); } catch(...) { return -1; } } class WebserverProcess { void init(const std::string& config) { m_config = config; File::setFile(testConfigFilename, config); // test self signed certificate File::setFile(testCertFilename, R"(-----BEGIN CERTIFICATE----- MIIC4zCCAcugAwIBAgIUeS9y+EsFWxf+foEx6SJ/R56rmX8wDQYJKoZIhvcNAQEL BQAwADAgFw0yMzAxMDYxNzIwNTFaGA8yMDUwMDUyNDE3MjA1MVowADCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALiZSICAcXng9j7zAb873U4TpuzvRVfh xS3gEhxqNPs6+ZQ43nAxDSdafzfGxpTkElTt/REj4oEOLw+QWI/jfbe4gDRDzf6V ij0fVuzp02JtJSS+dNrLv17NufBydOyD8oDrPehVrPlrZQhhkYMvLHAim+wikT2O s0es2R+avixxAZvx5EYgHba9T7R/pC/lA4BI3lEbVKjDA83hZvjPH1YdK+RYQS2g Jygdhe8qOSswXIwFAF3MMBpwRD3mz+vAJZP3lpBGsn+asO6Xd/5cjC8msgomS8Ji c9DMMNlrE1WU73wVG9n0OJcke2XEtzARVKJLlBPsug4oxDev6O4GakkCAwEAAaNT MFEwHQYDVR0OBBYEFE4i7Gtyn30qpIkH6f0/wuFA45pjMB8GA1UdIwQYMBaAFE4i 7Gtyn30qpIkH6f0/wuFA45pjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL BQADggEBAIlGv4b2yLmTOrXOPNst2y3J+GiRvuMKoAfDt5KLxUhbCmPgJzGDWn0l 60xXBX/t2uo3dQa9yAIW64RqhEQX7uja/7B3PmJZlgF7+owvT8OZA4+UN1lLUvY4 V7mUzuKuqo5jcX8EmZnHrJ4TGZ0dXbT1hAUgqIjnDChjWyvs4B9zZL5FTisPUic7 MU+FcpKJ5M6iJ150d9hzLiwmJyPLkW5Grq0Jh22njUQwWW2vIMn4cA3CyS64+oi2 DNnDgde3mYxXL8Oki7CbeCTpmUXcBHmQtWOvKZPCsOzMF4moTLC4DdElvOpwKCAK ABd6rubkarwvDV7wEo1eSuAHPZ/KhGo= -----END CERTIFICATE----- )"); File::setFile(testKeyFilename, R"(-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4mUiAgHF54PY+ 8wG/O91OE6bs70VX4cUt4BIcajT7OvmUON5wMQ0nWn83xsaU5BJU7f0RI+KBDi8P kFiP4323uIA0Q83+lYo9H1bs6dNibSUkvnTay79ezbnwcnTsg/KA6z3oVaz5a2UI YZGDLyxwIpvsIpE9jrNHrNkfmr4scQGb8eRGIB22vU+0f6Qv5QOASN5RG1SowwPN 4Wb4zx9WHSvkWEEtoCcoHYXvKjkrMFyMBQBdzDAacEQ95s/rwCWT95aQRrJ/mrDu l3f+XIwvJrIKJkvCYnPQzDDZaxNVlO98FRvZ9DiXJHtlxLcwEVSiS5QT7LoOKMQ3 r+juBmpJAgMBAAECggEACjs5suCbmYAb2d2VZlRitdP+Q6HX37D0YTBrBI7o6JdK U7oqrwBy/JBGHpDqewBgmTs3FGr/H/zJpDTRickXs9X6qhrreQ2wA6b/5gHoPMt0 nHKfbqyOCuq/YGmxnBXMnDNdoynTfGAE0af5vRIBZiYu6vG4B9fHzURR5O/qVDNn WqJ+2Y8AAf4mJDCBEvJz2RaZwSq788i/d8oTSeCk93TDF+GHhUq6ymkORACj76ws 8ohfoNQIG1VhdcTK2GOQjqctEFUm54t9N1nxD6VavMu5DlSSVsPTDbYuE5U4cy3T ThDtoYJgwz5KRflklwl3xoDJVx3B5wMUaqviRp6l0QKBgQC6MPG+EV2drsHwG0Wg gnP4uCSuFAfWBlHAQyZv5PMQBNfM8YjyMyL+O7cggGNJSOZr/X30EqoBe+LXrL3X Gtix8F1Ed0fbAarAgxIwq8MktzmastDq4XS+zwYPZ7UTbmbqvT3VYPga4Sh90fyY nPJpqZvhvGzQX22yeHS7vTSQOQKBgQD9z36EIYMuLl0HJK6gfjGHsy/Rx7bw1TmP aHmuF8Ra7rpDSOym0ImKWTOLEoLlQUsMz/FuVLCGP/ACjMFKsqh3Zy/0hVJOMDMR Z+ODT28Hcz4AMcTYDvcTYd70HhhZL+/eFCVk8Nk164saMuhifAkOgvwfaYs0m3ue S9jxlZKKkQKBgHgBzf6k8MMOfaAF4/XVv2wDPFkbPgW74vtaDK84UVX02ScWUx9Q yHA3Cwye09/LZgEazREA6qS0NfyvMVkwy5S9CVB01VKam3UjxhiqzMegdTd5o+CQ WpAVnaFWRcb1dM4+FVmv+5pPn6qhKv8uwaxLDtcLfNM9ftX2f77176g5AoGADWtQ DBpdfi6TWpJU7UVexwbxS00c3gTYAz4J2OuGxSwECxSq9nLmIrtunza+VvKpziac ZDH0F1UAEpJwkct6Xr3E6k+2N04TFSOCAupLO4CbUZVQDABWjd7J0+xXaze+neZA x+J4CYLHmv4ADVzzeaHxRJPm+UQTOB5YfQVkdxECgYBv3QuUMiBGKWgeheP4nAFU SVgqGBQwAtqb5DR1YVJ4LFPt+jyrQMby6mqSlzENYcidSP3Ogn22CvST+bAjbf6D D/ae1zeOHBls00ILHANv1Z/hXcEkiKnZdeP6O43xBfCS+Lps5daXgUbC0kw2R09S VZTqPHmb+db0rFA3XlAg2A== -----END PRIVATE KEY----- )"); start(); } public: WebserverProcess(const std::string& config): m_pid{} { init(config); } WebserverProcess(): m_pid{} { std::string config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost ip6-localhost localhost 127.0.0.1 [::1] static-files . testchain.pem testkey.pem
127.0.0.1
8080 http localhost
::1
8080 http localhost
127.0.0.1
8081 https localhost
::1
8081 https localhost
)CONFIG"}; init(config); } ~WebserverProcess() { stop(); fs::remove(testConfigFilename); fs::remove(testCertFilename); fs::remove(testKeyFilename); } 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 char* argv[] = {(char*)"webserver", (char*)"-c", (char*)"./webserver.conf"}; webserver(sizeof(argv) / sizeof(char*), argv); exit(0); } // wait for server to start up if (int port{port_from_config(m_config)}; port >= 0) wait_for_pid_listening_on(m_pid, port); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } void stop() { if (m_pid == 0) throw std::runtime_error("Process not running, so it can't be stopped"); if (kill(m_pid, SIGTERM) != 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; m_is = 0; m_filebuf = 0; } bool is_running() { if (m_pid == 0) return false; return Reichwein::Process::is_running(m_pid); } std::string output() { if (!is_running()) throw std::runtime_error("No output/stdout available from webserver since it is not running"); if (!m_is) throw std::runtime_error("Webserver stdout stream not initialized."); std::stringstream result; std::string buffer(static_cast(1024), '\0'); int size{}; while ((size = m_is->readsome(buffer.data(), buffer.size())) > 0) result << buffer.substr(0, size); return result.str(); } private: pid_t m_pid; std::string m_config; // 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) { auto const host = ipv6 ? "::1" : "127.0.0.1"; auto const port = "8080"; int version = HTTP11 ? 11 : 10; // The io_context is required for all I/O boost::asio::io_context ioc; // These objects perform our I/O boost::asio::ip::tcp::resolver resolver(ioc); boost::beast::tcp_stream stream(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 stream.connect(results); // Set up an HTTP GET request message boost::beast::http::request req; req.method(method); req.target(target); req.version(version); req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host); req.set(boost::beast::http::field::user_agent, "Webserver Testsuite"); // Send the HTTP request to the remote host boost::beast::http::write(stream, req); // This buffer is used for reading and must be persisted boost::beast::flat_buffer buffer; // Declare a container to hold the response boost::beast::http::response res; // Receive the HTTP response boost::beast::http::read(stream, buffer, res); // Return value std::ostringstream header_stream; header_stream << res.base(); std::ostringstream body_stream; body_stream << boost::beast::buffers_to_string(res.body().data()); // Gracefully close the socket boost::beast::error_code ec; stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); // not_connected happens sometimes // so don't bother reporting it. // if (ec && ec != boost::beast::errc::not_connected) throw boost::beast::system_error{ec}; return {header_stream.str(), body_stream.str()}; } void load_root_certificates(boost::asio::ssl::context& ctx) { std::string cert_chain{File::getFile(testCertFilename)}; ctx.add_certificate_authority(boost::asio::buffer(cert_chain.data(), cert_chain.size())); } std::pair HTTPS(const std::string& target, bool ipv6 = true, bool HTTP11 = true, boost::beast::http::verb method = boost::beast::http::verb::get) { auto const host = ipv6 ? "::1" : "127.0.0.1"; auto const port = "8081"; int version = HTTP11 ? 11 : 10; // 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); // Verify the remote server's certificate ctx.set_verify_mode(boost::asio::ssl::verify_peer); // These objects perform our I/O boost::asio::ip::tcp::resolver resolver(ioc); boost::beast::ssl_stream stream(ioc, ctx); // Set SNI Hostname (many hosts need this to handshake successfully) if (!SSL_set_tlsext_host_name(stream.native_handle(), host)) { boost::beast::error_code ec{static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()}; throw boost::beast::system_error{ec}; } // Look up the domain name auto const results = resolver.resolve(host, port); // Make the connection on the IP address we get from a lookup boost::beast::get_lowest_layer(stream).connect(results); // Perform the SSL handshake stream.handshake(boost::asio::ssl::stream_base::client); // Set up an HTTP GET request message boost::beast::http::request req; req.method(method); req.target(target); req.version(version); req.set(boost::beast::http::field::host, host == "::1"s ? "["s + host + "]"s : host); req.set(boost::beast::http::field::user_agent, "Webserver Testsuite"); // Send the HTTP request to the remote host boost::beast::http::write(stream, req); // This buffer is used for reading and must be persisted boost::beast::flat_buffer buffer; // Declare a container to hold the response boost::beast::http::response res; // Receive the HTTP response boost::beast::http::read(stream, buffer, res); // Return value std::ostringstream header_stream; header_stream << res.base(); std::ostringstream body_stream; body_stream << boost::beast::buffers_to_string(res.body().data()); // Gracefully close the stream boost::beast::error_code ec; stream.shutdown(ec); if (ec == boost::asio::error::eof) { // Rationale: // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error ec = {}; } if (ec) throw boost::beast::system_error{ec}; return {header_stream.str(), body_stream.str()}; } class Fixture { public: Fixture() { std::error_code ec; fs::remove_all("testdir", ec); fs::create_directory("testdir"); } ~Fixture() { std::error_code ec; fs::remove("stats.db", ec); fs::remove_all("testdir", ec); } }; 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.is_running()); std::pair response{https ? HTTPS("/webserver.conf", ipv6, http11, method) : HTTP("/webserver.conf", ipv6, http11, method)}; 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)); BOOST_REQUIRE_EQUAL(response.second, method == boost::beast::http::verb::head ? ""s : File::getFile(testConfigFilename)); 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.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)); } } BOOST_DATA_TEST_CASE_F(Fixture, http_get_file_not_found, 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.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.is_running()); BOOST_REQUIRE_EQUAL(response.first, fmt::format("HTTP/{} 404 Not Found\r\nServer: Reichwein.IT Webserver " VERSION "\r\nContent-Type: text/plain\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"); } // Test server class WebsocketServerProcess { // shared data between Unix processes struct shared_data_t { std::mutex mutex; // for synchronization between processes (!) char subprotocol[1024]{}; // instead of std::string since std::string allocates data on heap char target[1024]{}; }; public: WebsocketServerProcess() { // RAII pattern for shared memory allocation/deallocation m_shared = std::unique_ptr>( (shared_data_t*)mmap(NULL, sizeof(shared_data_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0), [this](shared_data_t*){munmap(m_shared.get(), sizeof(shared_data_t));}); start(); } ~WebsocketServerProcess() { stop(); } 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")); })); boost::beast::http::request_parser parser; request_type req; boost::beast::flat_buffer buffer; boost::beast::http::read(ws.next_layer(), buffer, parser); req = parser.get(); { std::lock_guard lock{m_shared->mutex}; strncpy(m_shared->subprotocol, std::string{req[http::field::sec_websocket_protocol]}.data(), sizeof(m_shared->subprotocol)); strncpy(m_shared->target, std::string{req.target()}.data(), sizeof(m_shared->target)); } ws.accept(req); for(;;) { boost::beast::flat_buffer buffer; ws.read(buffer); // Reply with : 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 try { auto const address = boost::asio::ip::make_address("::1"); auto const port = static_cast(8765); // 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); } wait_for_pid_listening_on(m_pid, 8765); } void stop() { if (!is_running()) throw std::runtime_error("Process not running, so it can't be stopped"); if (kill(m_pid, SIGTERM) != 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; } std::string subprotocol() { std::lock_guard lock{m_shared->mutex}; return m_shared->subprotocol; } std::string target() { std::lock_guard lock{m_shared->mutex}; return m_shared->target; } private: int m_pid{}; int m_count{}; std::unique_ptr> m_shared; }; // class WebsocketServerProcess BOOST_FIXTURE_TEST_CASE(websocket_ssl, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost ip6-localhost localhost 127.0.0.1 [::1] websocket ::1:8765 testchain.pem testkey.pem
127.0.0.1
8080 http localhost
::1
8080 http localhost
127.0.0.1
8081 https localhost
::1
8081 https localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); WebsocketServerProcess websocketProcess; BOOST_REQUIRE(websocketProcess.is_running()); 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> 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(::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 if (host == "::1") 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) { req.set(boost::beast::http::field::user_agent, std::string("Reichwein.IT Test Websocket Client")); })); // 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"); 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: 2"); // Close the WebSocket connection ws.close(boost::beast::websocket::close_code::normal); BOOST_REQUIRE(serverProcess.is_running()); BOOST_REQUIRE(websocketProcess.is_running()); } BOOST_FIXTURE_TEST_CASE(websocket_plain_subprotocol, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost ip6-localhost localhost 127.0.0.1 [::1] websocket ::1:8765 testchain.pem testkey.pem
127.0.0.1
8080 http localhost
::1
8080 http localhost
127.0.0.1
8081 https localhost
::1
8081 https localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); WebsocketServerProcess websocketProcess; BOOST_REQUIRE(websocketProcess.is_running()); std::string host = "::1"; auto const port = "8080" ; auto const text = "request1"; // The io_context is required for all I/O boost::asio::io_context ioc; // These objects perform our I/O boost::asio::ip::tcp::resolver resolver{ioc}; 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(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. // See https://tools.ietf.org/html/rfc7230#section-5.4 if (host == "::1") host = "[" + host + "]"; host += ':' + std::to_string(ep.port()); // 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")); })); ws.set_option(boost::beast::websocket::stream_base::decorator( [](boost::beast::websocket::request_type& req) { req.set(boost::beast::http::field::sec_websocket_protocol, "protocol1"); })); // Perform the websocket handshake ws.handshake(host, "/path1/target1"); // 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"); 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: 2"); // Close the WebSocket connection ws.close(boost::beast::websocket::close_code::normal); BOOST_CHECK_EQUAL(websocketProcess.subprotocol(), "protocol1"); BOOST_CHECK_EQUAL(websocketProcess.target(), "/path1/target1"); BOOST_REQUIRE(websocketProcess.is_running()); BOOST_REQUIRE(serverProcess.is_running()); } BOOST_FIXTURE_TEST_CASE(plugin_cgi, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] cgi testdir
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); File::setFile("testdir/test1.sh", R"(#!/bin/bash echo -ne "Content-Type: text/plain\r\n" echo -ne "\r\n" echo -ne "Test 1:\r\n" echo -ne "HTTP_CONNECTION: $HTTP_CONNECTION\r\n" echo -ne "HTTP_HOST: $HTTP_HOST\r\n" echo -ne "HTTP_USER_AGENT: $HTTP_USER_AGENT\r\n" echo -ne "SERVER_PORT: $SERVER_PORT\r\n" echo -ne "QUERY_STRING: $QUERY_STRING\r\n" echo -ne "SCRIPT_NAME: $SCRIPT_NAME\r\n" echo -ne "PATH_INFO: $PATH_INFO\r\n" echo -ne "REQUEST_METHOD: $REQUEST_METHOD\r\n" echo -ne "SERVER_NAME: $SERVER_NAME\r\n" echo -ne "HTTP_HOST: $HTTP_HOST\r\n" )"); fs::permissions("testdir/test1.sh", fs::perms::owner_all | fs::perms::group_all, fs::perm_options::add); auto result {HTTP("/cgi-test/test1.sh/path1?q=2")}; BOOST_CHECK_EQUAL(result.first, fmt::format( "HTTP/1.1 200 OK\r\n" "Server: Reichwein.IT Webserver {}\r\n" "Content-Type: text/plain\r\n" "Content-Length: {}\r\n" "\r\n" , VERSION, result.second.size())); BOOST_CHECK_EQUAL(result.second, "Test 1:\r\n" "HTTP_CONNECTION: \r\n" "HTTP_HOST: [::1]\r\n" "HTTP_USER_AGENT: Webserver Testsuite\r\n" "SERVER_PORT: 8080\r\n" "QUERY_STRING: q=2\r\n" "SCRIPT_NAME: /cgi-test/test1.sh\r\n" "PATH_INFO: /path1\r\n" "REQUEST_METHOD: GET\r\n" "SERVER_NAME: [::1]\r\n" "HTTP_HOST: [::1]\r\n" ); } BOOST_FIXTURE_TEST_CASE(plugin_cgi_missing_exe, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] cgi testdir
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); File::setFile("testdir/test1.sh", R"(#!/bin/bash echo -ne "Content-Type: text/plain\r\n" echo -ne "\r\n" echo -ne "Test 1:\r\n" echo -ne "HTTP_CONNECTION: $HTTP_CONNECTION\r\n" echo -ne "HTTP_HOST: $HTTP_HOST\r\n" echo -ne "HTTP_USER_AGENT: $HTTP_USER_AGENT\r\n" echo -ne "SERVER_PORT: $SERVER_PORT\r\n" echo -ne "QUERY_STRING: $QUERY_STRING\r\n" echo -ne "SCRIPT_NAME: $SCRIPT_NAME\r\n" echo -ne "PATH_INFO: $PATH_INFO\r\n" echo -ne "REQUEST_METHOD: $REQUEST_METHOD\r\n" echo -ne "SERVER_NAME: $SERVER_NAME\r\n" echo -ne "HTTP_HOST: $HTTP_HOST\r\n" )"); fs::permissions("testdir/test1.sh", fs::perms::owner_all | fs::perms::group_all, fs::perm_options::add); auto result {HTTP("/cgi-test/test2.sh")}; BOOST_CHECK_EQUAL(result.first, fmt::format( "HTTP/1.1 500 Internal Server Error\r\n" "Server: Reichwein.IT Webserver {}\r\n" "Content-Type: text/plain\r\n" "Content-Length: {}\r\n" "\r\n" , VERSION, result.second.size())); BOOST_CHECK_EQUAL(result.second, "500 Bad Script: test2.sh"); result = HTTP("/cgi-test/test2.sh/path1?q=2"); BOOST_CHECK_EQUAL(result.first, fmt::format( "HTTP/1.1 500 Internal Server Error\r\n" "Server: Reichwein.IT Webserver {}\r\n" "Content-Type: text/plain\r\n" "Content-Length: {}\r\n" "\r\n" , VERSION, result.second.size())); BOOST_CHECK_EQUAL(result.second, "500 Bad Script: test2.sh/path1"); } BOOST_FIXTURE_TEST_CASE(empty_config, Fixture) { WebserverProcess serverProcess{""}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false); } BOOST_FIXTURE_TEST_CASE(incomplete_config, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data )CONFIG"}; WebserverProcess serverProcess{webserver_config}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false); }