#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()}; }