// 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" #include "plugins/fcgi/fastcgiprocess.h" #include "helper.h" #include "webserverprocess.h" #include "websocketserverprocess.h" using namespace std::string_literals; namespace fs = std::filesystem; namespace pt = boost::property_tree; using namespace boost::unit_test; using namespace Reichwein; 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"); } 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(config_empty, Fixture) { WebserverProcess serverProcess{std::string{}}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false); } BOOST_FIXTURE_TEST_CASE(config_incomplete, 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); } BOOST_FIXTURE_TEST_CASE(config_bad_syntax, Fixture) { 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"}; WebserverProcess serverProcess{config, false}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false); } BOOST_FIXTURE_TEST_CASE(config_bad_socket, Fixture) { 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
1234:5678::1
8081 https localhost
)CONFIG"}; WebserverProcess serverProcess{config, false}; std::this_thread::sleep_for(std::chrono::milliseconds(50)); BOOST_REQUIRE_EQUAL(serverProcess.is_running(), false); } BOOST_FIXTURE_TEST_CASE(http_redirect, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] redirect https://www.reichwein.it/ 301 Redirecting to reichwein.it ...
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); auto result {HTTP("/redirect1")}; BOOST_CHECK_EQUAL(result.first, fmt::format( "HTTP/1.1 301 Moved Permanently\r\n" "Server: Reichwein.IT Webserver {}\r\n" "Location: https://www.reichwein.it/\r\n" "Content-Type: text/html\r\n" "Content-Length: {}\r\n" "\r\n" , VERSION, result.second.size())); BOOST_CHECK_EQUAL(result.second, "301 Redirecting to reichwein.it ..."); result = HTTP("/redirect1/path1"); BOOST_CHECK_EQUAL(result.first, fmt::format( "HTTP/1.1 301 Moved Permanently\r\n" "Server: Reichwein.IT Webserver {}\r\n" "Location: https://www.reichwein.it/\r\n" "Content-Type: text/html\r\n" "Content-Length: {}\r\n" "\r\n" , VERSION, result.second.size())); BOOST_CHECK_EQUAL(result.second, "301 Redirecting to reichwein.it ..."); } BOOST_FIXTURE_TEST_CASE(http_fcgi_ip4, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] fcgi 127.0.0.1:8765
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); FastCGIProcess fcgiProcess("./fcgi1", "127.0.0.1", 8765); BOOST_REQUIRE(fcgiProcess.is_running()); auto result {HTTP("/fcgi/abc")}; 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, "returning data of : "); } BOOST_FIXTURE_TEST_CASE(http_fcgi_ip6, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] fcgi ::1:8765
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); FastCGIProcess fcgiProcess("./fcgi1", "::1", 8765); BOOST_REQUIRE(fcgiProcess.is_running()); auto result {HTTP("/fcgi/abc")}; 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, "returning data of : "); } BOOST_FIXTURE_TEST_CASE(http_fcgi_unix_socket, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] fcgi fcgi-socket
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); FastCGIProcess fcgiProcess("./fcgi1", "fcgi-socket"); BOOST_REQUIRE(fcgiProcess.is_running()); auto result {HTTP("/fcgi/abc")}; 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, "returning data of : "); } BOOST_FIXTURE_TEST_CASE(http_fcgi_executable, Fixture) { std::string webserver_config{R"CONFIG( www-data www-data 10 stats.db ../plugins localhost localhost [::1] fcgi fcgi1
::1
8080 http localhost
)CONFIG"}; WebserverProcess serverProcess{webserver_config}; BOOST_REQUIRE(serverProcess.is_running()); auto result {HTTP("/fcgi/abc")}; 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, "returning data of : "); }