//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // TLS interop against implementations that are not this library. Speaking // HTTP/1.1 correctly to ourselves proves very little about the record layer — // an OpenSSL client and an OpenSSL server can agree on a mistake. // // * curl drives our TLS listener, verifying our certificate properly with // --cacert: keep-alive reuse, POST, HEAD, ALPN, and a status line. // * ClientHTTP1 drives python3's http.server behind ssl.wrap_socket, which // answers HTTP/1.0 with `Connection: close` — so the response is framed by // close_notify rather than by content-length. // // Both peers are optional: a missing curl or python3 skips its half rather // than failing, so the suite still runs on a bare machine. #include #include #include #include import Crafter.Network; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } bool HaveCommand(std::string_view name) { const std::string probe = "command -v " + std::string(name) + " >/dev/null 2>&1"; return std::system(probe.c_str()) == 0; } std::string Run(const std::string& command) { std::string output; FILE* pipe = popen((command + " 2>&1").c_str(), "r"); if (pipe == nullptr) return output; char buffer[4096]; while (std::size_t read = std::fread(buffer, 1, sizeof(buffer), pipe)) { output.append(buffer, read); } pclose(pipe); return output; } // A child process, killed when this goes out of scope. class Child { public: explicit Child(std::vector argv) { std::vector raw; for (auto& argument : argv) raw.push_back(argument.data()); raw.push_back(nullptr); pid = fork(); if (pid == 0) { freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); execvp(raw[0], raw.data()); _exit(127); } } ~Child() { if (pid > 0) { kill(pid, SIGTERM); int status = 0; waitpid(pid, &status, 0); } } Child(const Child&) = delete; bool Started() const { return pid > 0; } private: pid_t pid = -1; }; bool WaitForPort(std::uint16_t port, std::chrono::milliseconds budget) { const auto deadline = std::chrono::steady_clock::now() + budget; while (std::chrono::steady_clock::now() < deadline) { try { ClientTCP probe("localhost", port); return true; } catch (const std::exception&) { std::this_thread::sleep_for(std::chrono::milliseconds(25)); } } return false; } // The development certificate on disk, so peer processes (curl, python) // can be pointed at it. struct CertificateFiles { std::filesystem::path directory; std::filesystem::path certificate; std::filesystem::path privateKey; CertificateFiles() { directory = std::filesystem::temp_directory_path() / "crafter-network-https1-interop"; std::filesystem::create_directories(directory); certificate = directory / "cert.pem"; privateKey = directory / "key.pem"; const TLSCertificatePem& pem = GetSelfSignedCertificatePem(); std::ofstream(certificate, std::ios::binary) << pem.certificate; std::ofstream(privateKey, std::ios::binary) << pem.privateKey; } ~CertificateFiles() { std::error_code error; std::filesystem::remove_all(directory, error); } CertificateFiles(const CertificateFiles&) = delete; }; void CurlAgainstListener(const CertificateFiles& files) { if (!HaveCommand("curl")) { std::println("skipping the curl half: curl is not installed"); return; } std::unordered_map> routes; routes["/hello"] = [](const HTTPRequest&) { return CreateResponseHTTP("200", {{"content-type", "text/plain"}}, "Hello curl!"); }; routes["/echo"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", request.method + ":" + request.body); }; routes["/scheme"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", request.scheme); }; ListenerAsyncHTTP1 listener(8097, std::move(routes), TLSServerCredentials{ .selfSigned = true }); Check(WaitForPort(8097, std::chrono::seconds(2)), "the HTTPS listener came up"); // --cacert, not --insecure: curl does the full chain and hostname // check, so this is a real verification of what we present. const std::string base = "https://localhost:8097"; const std::string curl = "curl -sS --http1.1 --cacert '" + files.certificate.string() + "' "; Check(Run(curl + base + "/hello") == "Hello curl!", "curl GET over TLS"); Check(Run(curl + base + "/scheme") == "https", "the handler sees scheme=https"); // Two URLs in one invocation: curl reuses the TLS session, which only // works if our framing let it know the first response ended. const std::uint64_t before = listener.listener.AcceptedCount(); Check(Run(curl + base + "/hello " + base + "/hello") == "Hello curl!Hello curl!", "curl got both responses"); Check(listener.listener.AcceptedCount() == before + 1, "curl reused one TLS connection for both"); Check(Run(curl + "-d 'body text' " + base + "/echo") == "POST:body text", "curl POST over TLS"); const std::string head = Run(curl + "-I " + base + "/hello"); Check(head.find("HTTP/1.1 200 OK") != std::string::npos, "curl HEAD status line"); Check(head.find("content-length: 11") != std::string::npos, "curl HEAD keeps content-length"); Check(head.find("Hello curl!") == std::string::npos, "curl HEAD carries no body"); Check(Run(curl + "-o /dev/null -w '%{http_code}' " + base + "/missing") == "404", "curl reads the 404 status"); // curl offers h2 and http/1.1 by default; ours advertises only // http/1.1, so ALPN has to land there. `-w %{...}` reports what the // handshake actually agreed on rather than what we hoped for. Check(Run(curl + "-o /dev/null -w '%{http_version}' " + base + "/hello") == "1.1", "ALPN settled on HTTP/1.1"); // A client offering only h2 shares no protocol with us. RFC 7301 says // that is a fatal no_application_protocol alert, not a downgrade. if (Run("curl -sS --http2-prior-knowledge -o /dev/null -w '%{http_code}' --cacert '" + files.certificate.string() + "' " + base + "/hello").find("200") == std::string::npos) { Check(true, "an h2-only client is refused rather than mis-served"); } else { Check(false, "an h2-only client is refused rather than mis-served"); } listener.Stop(); } void ClientAgainstPythonServer(const CertificateFiles& files) { if (!HaveCommand("python3")) { std::println("skipping the python half: python3 is not installed"); return; } const std::filesystem::path root = files.directory / "www"; std::filesystem::create_directories(root); const std::string content = "served by python over TLS\n"; std::ofstream(root / "hello.txt", std::ios::binary) << content; // http.server answers HTTP/1.0 with `Connection: close`, so over TLS // the response body is framed by close_notify — the path our reader // has to treat as an orderly end rather than a truncation. const std::string script = "import functools, http.server, ssl\n" "context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n" "context.load_cert_chain('" + files.certificate.string() + "', '" + files.privateKey.string() + "')\n" "handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory='" + root.string() + "')\n" "server = http.server.HTTPServer(('127.0.0.1', 8098), handler)\n" "server.socket = context.wrap_socket(server.socket, server_side=True)\n" "server.serve_forever()\n"; Child server({"python3", "-c", script}); Check(server.Started(), "python3 TLS http.server was spawned"); if (!WaitForPort(8098, std::chrono::seconds(10))) { std::println("skipping the python half: the TLS http.server never came up"); return; } // Verified against the certificate on disk, exercising caPath (a file) // rather than the caPem blob the self-contained test uses. TLSClientCredentials credentials; credentials.caPath = files.certificate.string(); ClientHTTP1 client("localhost", 8098, credentials); HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/hello.txt", "localhost:8098")); Check(response.status == "200", "python TLS GET status"); Check(response.body == content, "python TLS GET body"); Check(!client.Connected(), "an HTTP/1.0 response closes the TLS connection"); HTTPResponse listing = client.Send(CreateRequestHTTP("GET", "/", "localhost:8098")); Check(listing.status == "200", "python TLS directory listing status"); Check(listing.body.find("hello.txt") != std::string::npos, "python TLS directory listing body"); HTTPResponse missing = client.Send(CreateRequestHTTP("GET", "/nothing-here", "localhost:8098")); Check(missing.status == "404", "python TLS 404"); HTTPResponse head = client.Send(CreateRequestHTTP("HEAD", "/hello.txt", "localhost:8098")); Check(head.status == "200", "python TLS HEAD status"); Check(head.body.empty(), "python TLS HEAD has no body"); } } int main() { try { CertificateFiles files; CurlAgainstListener(files); ClientAgainstPythonServer(files); } catch (const std::exception& error) { std::println("threw: {}", error.what()); return 1; } if (failures != 0) { std::println("{} check(s) failed", failures); return 1; } return 0; }