test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
//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 <signal.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <sys/wait.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
|
|
|
|
|
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<std::string> argv) {
|
|
|
|
|
std::vector<char*> 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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> 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);
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-28 20:34:23 +00:00
|
|
|
ListenerAsyncHTTP1 listener(8111, std::move(routes),
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
TLSServerCredentials{ .selfSigned = true });
|
2026-07-28 20:34:23 +00:00
|
|
|
Check(WaitForPort(8111, std::chrono::seconds(2)), "the HTTPS listener came up");
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
|
|
|
|
|
// --cacert, not --insecure: curl does the full chain and hostname
|
|
|
|
|
// check, so this is a real verification of what we present.
|
2026-07-28 20:34:23 +00:00
|
|
|
const std::string base = "https://localhost:8111";
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
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"
|
2026-07-28 20:34:23 +00:00
|
|
|
"server = http.server.HTTPServer(('127.0.0.1', 8112), handler)\n"
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
"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");
|
2026-07-28 20:34:23 +00:00
|
|
|
if (!WaitForPort(8112, std::chrono::seconds(10))) {
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
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();
|
2026-07-28 20:34:23 +00:00
|
|
|
ClientHTTP1 client("localhost", 8112, credentials);
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
|
2026-07-28 20:34:23 +00:00
|
|
|
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/hello.txt", "localhost:8112"));
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
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");
|
|
|
|
|
|
2026-07-28 20:34:23 +00:00
|
|
|
HTTPResponse listing = client.Send(CreateRequestHTTP("GET", "/", "localhost:8112"));
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
Check(listing.status == "200", "python TLS directory listing status");
|
|
|
|
|
Check(listing.body.find("hello.txt") != std::string::npos,
|
|
|
|
|
"python TLS directory listing body");
|
|
|
|
|
|
2026-07-28 20:34:23 +00:00
|
|
|
HTTPResponse missing = client.Send(CreateRequestHTTP("GET", "/nothing-here", "localhost:8112"));
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
Check(missing.status == "404", "python TLS 404");
|
|
|
|
|
|
2026-07-28 20:34:23 +00:00
|
|
|
HTTPResponse head = client.Send(CreateRequestHTTP("HEAD", "/hello.txt", "localhost:8112"));
|
test(https): cover the TLS transport from three angles
ShouldSendRecieveHTTPS1 replays the plaintext round-trip over TLS, so a
regression in the transport shows up as an HTTP failure rather than
nothing at all, and adds what only exists under TLS: ALPN, scheme=https
reaching handlers, a body spanning many records, and the two ways
verification must fail — an untrusted self-signed certificate, and a
trusted certificate presented for the wrong name. A plaintext peer
knocking on the TLS port is asserted to be counted and shrugged off.
ShouldInteropCurlHTTPS1 puts real implementations on the other end, since
two OpenSSL peers can agree on a mistake. curl verifies our certificate
with --cacert rather than --insecure, and an h2-only curl is asserted to
be refused rather than mis-served. python3's http.server behind
ssl.wrap_socket answers HTTP/1.0 with Connection: close, which frames the
body by close_notify — the path a reader is most likely to get wrong.
ShouldRequireClientCertificateHTTPS1 covers mutual TLS both ways, and
drives TLSStream directly with hand-written HTTP/1.1 to keep the :TLS
layer honest as something usable without :ClientHTTP1 on top.
Also fix a delegation that `{}` no longer disambiguates now that a
three-argument TLS constructor exists alongside the fallback one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 20:17:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|