From 702d32b41c1c4f496dba046c2017cb5b907e55cd Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Thu, 12 Jan 2023 20:00:40 +0100 Subject: FCGI test --- tests/helper.cpp | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tests/helper.cpp (limited to 'tests/helper.cpp') diff --git a/tests/helper.cpp b/tests/helper.cpp new file mode 100644 index 0000000..644b9ca --- /dev/null +++ b/tests/helper.cpp @@ -0,0 +1,228 @@ +#include "helper.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; + } +} + +std::pair HTTP(const std::string& target, bool ipv6, bool HTTP11, boost::beast::http::verb method) +{ + 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, bool HTTP11, boost::beast::http::verb method) +{ + 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()}; +} + -- cgit v1.2.3