https #6
4 changed files with 34 additions and 23 deletions
test(https): give the TLS tests ports of their own
The new tests reused ports the existing suite already binds — 8095 with ShouldSurviveAbuseHTTP1, and 8097/8098 with ShouldFallbackUnknownRoutes' plaintext and HTTP/3 listeners. SO_REUSEADDR does not let two live listeners share a port, so under the parallel runner whichever bound second failed, and which test that was came down to scheduling. Move the TLS tests to 8110-8114, which nothing else uses. That collision also showed up as a SIGABRT rather than a reported error, so harden the path it took: ~ListenerHTTP1 calls Stop(), which joins threads and touches sockets and can therefore throw. A second listener failing to bind unwinds past a live first one, and a throw out of its destructor mid-unwind terminates the process — turning a diagnosable bind failure into a crash. Swallow it there, where there is nothing left to report it to. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit
14e0a6bab1
|
|
@ -288,7 +288,15 @@ ListenerHTTP1::ListenerHTTP1(ListenerHTTP1&& other) noexcept
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenerHTTP1::~ListenerHTTP1() {
|
ListenerHTTP1::~ListenerHTTP1() {
|
||||||
if (impl) Stop();
|
// Stop() joins threads and touches sockets, so it can throw. Letting that
|
||||||
|
// out of a destructor ends the process — and the case where it matters is
|
||||||
|
// exactly the unhappy one: a second listener failing to bind unwinds past
|
||||||
|
// a live first listener, so a throw here replaces a reportable error with
|
||||||
|
// a terminate.
|
||||||
|
try {
|
||||||
|
if (impl) Stop();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenerHTTP1::Listen() {
|
void ListenerHTTP1::Listen() {
|
||||||
|
|
@ -372,7 +380,10 @@ ListenerAsyncHTTP1::ListenerAsyncHTTP1(std::uint16_t port,
|
||||||
{}
|
{}
|
||||||
|
|
||||||
ListenerAsyncHTTP1::~ListenerAsyncHTTP1() {
|
ListenerAsyncHTTP1::~ListenerAsyncHTTP1() {
|
||||||
Stop();
|
try {
|
||||||
|
Stop();
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ListenerAsyncHTTP1::Stop() {
|
void ListenerAsyncHTTP1::Stop() {
|
||||||
|
|
|
||||||
|
|
@ -132,13 +132,13 @@ namespace {
|
||||||
return CreateResponseHTTP("200", request.scheme);
|
return CreateResponseHTTP("200", request.scheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
ListenerAsyncHTTP1 listener(8097, std::move(routes),
|
ListenerAsyncHTTP1 listener(8111, std::move(routes),
|
||||||
TLSServerCredentials{ .selfSigned = true });
|
TLSServerCredentials{ .selfSigned = true });
|
||||||
Check(WaitForPort(8097, std::chrono::seconds(2)), "the HTTPS listener came up");
|
Check(WaitForPort(8111, std::chrono::seconds(2)), "the HTTPS listener came up");
|
||||||
|
|
||||||
// --cacert, not --insecure: curl does the full chain and hostname
|
// --cacert, not --insecure: curl does the full chain and hostname
|
||||||
// check, so this is a real verification of what we present.
|
// check, so this is a real verification of what we present.
|
||||||
const std::string base = "https://localhost:8097";
|
const std::string base = "https://localhost:8111";
|
||||||
const std::string curl = "curl -sS --http1.1 --cacert '" + files.certificate.string() + "' ";
|
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 + "/hello") == "Hello curl!", "curl GET over TLS");
|
||||||
|
|
@ -203,13 +203,13 @@ namespace {
|
||||||
+ files.privateKey.string() + "')\n"
|
+ files.privateKey.string() + "')\n"
|
||||||
"handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory='"
|
"handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory='"
|
||||||
+ root.string() + "')\n"
|
+ root.string() + "')\n"
|
||||||
"server = http.server.HTTPServer(('127.0.0.1', 8098), handler)\n"
|
"server = http.server.HTTPServer(('127.0.0.1', 8112), handler)\n"
|
||||||
"server.socket = context.wrap_socket(server.socket, server_side=True)\n"
|
"server.socket = context.wrap_socket(server.socket, server_side=True)\n"
|
||||||
"server.serve_forever()\n";
|
"server.serve_forever()\n";
|
||||||
|
|
||||||
Child server({"python3", "-c", script});
|
Child server({"python3", "-c", script});
|
||||||
Check(server.Started(), "python3 TLS http.server was spawned");
|
Check(server.Started(), "python3 TLS http.server was spawned");
|
||||||
if (!WaitForPort(8098, std::chrono::seconds(10))) {
|
if (!WaitForPort(8112, std::chrono::seconds(10))) {
|
||||||
std::println("skipping the python half: the TLS http.server never came up");
|
std::println("skipping the python half: the TLS http.server never came up");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -218,22 +218,22 @@ namespace {
|
||||||
// rather than the caPem blob the self-contained test uses.
|
// rather than the caPem blob the self-contained test uses.
|
||||||
TLSClientCredentials credentials;
|
TLSClientCredentials credentials;
|
||||||
credentials.caPath = files.certificate.string();
|
credentials.caPath = files.certificate.string();
|
||||||
ClientHTTP1 client("localhost", 8098, credentials);
|
ClientHTTP1 client("localhost", 8112, credentials);
|
||||||
|
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/hello.txt", "localhost:8098"));
|
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/hello.txt", "localhost:8112"));
|
||||||
Check(response.status == "200", "python TLS GET status");
|
Check(response.status == "200", "python TLS GET status");
|
||||||
Check(response.body == content, "python TLS GET body");
|
Check(response.body == content, "python TLS GET body");
|
||||||
Check(!client.Connected(), "an HTTP/1.0 response closes the TLS connection");
|
Check(!client.Connected(), "an HTTP/1.0 response closes the TLS connection");
|
||||||
|
|
||||||
HTTPResponse listing = client.Send(CreateRequestHTTP("GET", "/", "localhost:8098"));
|
HTTPResponse listing = client.Send(CreateRequestHTTP("GET", "/", "localhost:8112"));
|
||||||
Check(listing.status == "200", "python TLS directory listing status");
|
Check(listing.status == "200", "python TLS directory listing status");
|
||||||
Check(listing.body.find("hello.txt") != std::string::npos,
|
Check(listing.body.find("hello.txt") != std::string::npos,
|
||||||
"python TLS directory listing body");
|
"python TLS directory listing body");
|
||||||
|
|
||||||
HTTPResponse missing = client.Send(CreateRequestHTTP("GET", "/nothing-here", "localhost:8098"));
|
HTTPResponse missing = client.Send(CreateRequestHTTP("GET", "/nothing-here", "localhost:8112"));
|
||||||
Check(missing.status == "404", "python TLS 404");
|
Check(missing.status == "404", "python TLS 404");
|
||||||
|
|
||||||
HTTPResponse head = client.Send(CreateRequestHTTP("HEAD", "/hello.txt", "localhost:8098"));
|
HTTPResponse head = client.Send(CreateRequestHTTP("HEAD", "/hello.txt", "localhost:8112"));
|
||||||
Check(head.status == "200", "python TLS HEAD status");
|
Check(head.status == "200", "python TLS HEAD status");
|
||||||
Check(head.body.empty(), "python TLS HEAD has no body");
|
Check(head.body.empty(), "python TLS HEAD has no body");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,13 +84,13 @@ namespace {
|
||||||
// The :TLS layer without any HTTP on top — the case for anything else that
|
// The :TLS layer without any HTTP on top — the case for anything else that
|
||||||
// owns a connected socket and wants a record layer over it.
|
// owns a connected socket and wants a record layer over it.
|
||||||
void RawStreamAgainstListener() {
|
void RawStreamAgainstListener() {
|
||||||
ListenerAsyncHTTP1 listener(8099, Routes(), TLSServerCredentials{ .selfSigned = true });
|
ListenerAsyncHTTP1 listener(8113, Routes(), TLSServerCredentials{ .selfSigned = true });
|
||||||
|
|
||||||
TLSClientCredentials credentials;
|
TLSClientCredentials credentials;
|
||||||
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
||||||
auto context = TLSContext::Client(credentials);
|
auto context = TLSContext::Client(credentials);
|
||||||
|
|
||||||
ClientTCP socket("localhost", 8099);
|
ClientTCP socket("localhost", 8113);
|
||||||
std::unique_ptr<TLSStream> stream =
|
std::unique_ptr<TLSStream> stream =
|
||||||
TLSStream::Connect(socket.socketid, context, "localhost",
|
TLSStream::Connect(socket.socketid, context, "localhost",
|
||||||
std::chrono::seconds(5));
|
std::chrono::seconds(5));
|
||||||
|
|
@ -143,7 +143,7 @@ namespace {
|
||||||
server.selfSigned = true;
|
server.selfSigned = true;
|
||||||
server.requireClientCertificate = true;
|
server.requireClientCertificate = true;
|
||||||
server.clientCaPath = identity.authority.string();
|
server.clientCaPath = identity.authority.string();
|
||||||
ListenerAsyncHTTP1 listener(8100, Routes(), server);
|
ListenerAsyncHTTP1 listener(8114, Routes(), server);
|
||||||
|
|
||||||
// A client with a certificate the listener's CA vouches for.
|
// A client with a certificate the listener's CA vouches for.
|
||||||
{
|
{
|
||||||
|
|
@ -151,7 +151,7 @@ namespace {
|
||||||
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
||||||
credentials.certPath = identity.certificate.string();
|
credentials.certPath = identity.certificate.string();
|
||||||
credentials.keyPath = identity.privateKey.string();
|
credentials.keyPath = identity.privateKey.string();
|
||||||
ClientHTTP1 client("localhost", 8100, credentials);
|
ClientHTTP1 client("localhost", 8114, credentials);
|
||||||
|
|
||||||
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
Check(response.status == "200", "a client with a trusted certificate is served");
|
Check(response.status == "200", "a client with a trusted certificate is served");
|
||||||
|
|
@ -167,7 +167,7 @@ namespace {
|
||||||
{
|
{
|
||||||
TLSClientCredentials credentials;
|
TLSClientCredentials credentials;
|
||||||
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
||||||
ClientHTTP1 anonymous("localhost", 8100, credentials);
|
ClientHTTP1 anonymous("localhost", 8114, credentials);
|
||||||
bool refused = false;
|
bool refused = false;
|
||||||
try {
|
try {
|
||||||
anonymous.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
anonymous.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@ int main() {
|
||||||
// The listener mints an ephemeral certificate; the client is handed
|
// The listener mints an ephemeral certificate; the client is handed
|
||||||
// that same certificate as a trust anchor, so this exercises real
|
// that same certificate as a trust anchor, so this exercises real
|
||||||
// chain *and* hostname verification rather than skipping both.
|
// chain *and* hostname verification rather than skipping both.
|
||||||
ListenerAsyncHTTP1 listener(8095, Routes(), TLSServerCredentials{ .selfSigned = true });
|
ListenerAsyncHTTP1 listener(8110, Routes(), TLSServerCredentials{ .selfSigned = true });
|
||||||
Check(listener.listener.Secure(), "the listener reports itself as https");
|
Check(listener.listener.Secure(), "the listener reports itself as https");
|
||||||
|
|
||||||
TLSClientCredentials credentials;
|
TLSClientCredentials credentials;
|
||||||
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
credentials.caPem = GetSelfSignedCertificatePem().certificate;
|
||||||
ClientHTTP1 client("localhost", 8095, credentials);
|
ClientHTTP1 client("localhost", 8110, credentials);
|
||||||
Check(client.Secure(), "the client reports itself as https");
|
Check(client.Secure(), "the client reports itself as https");
|
||||||
|
|
||||||
HTTPResponse hello = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
HTTPResponse hello = client.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
|
|
@ -110,7 +110,7 @@ int main() {
|
||||||
// Default credentials: system trust store only, so a self-signed
|
// Default credentials: system trust store only, so a self-signed
|
||||||
// certificate must be rejected rather than quietly accepted.
|
// certificate must be rejected rather than quietly accepted.
|
||||||
{
|
{
|
||||||
ClientHTTP1 strict("localhost", 8095, TLSClientCredentials{});
|
ClientHTTP1 strict("localhost", 8110, TLSClientCredentials{});
|
||||||
bool rejected = false;
|
bool rejected = false;
|
||||||
try {
|
try {
|
||||||
strict.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
strict.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
|
|
@ -128,7 +128,7 @@ int main() {
|
||||||
TLSClientCredentials mismatched;
|
TLSClientCredentials mismatched;
|
||||||
mismatched.caPem = GetSelfSignedCertificatePem().certificate;
|
mismatched.caPem = GetSelfSignedCertificatePem().certificate;
|
||||||
mismatched.serverName = "not-localhost.invalid";
|
mismatched.serverName = "not-localhost.invalid";
|
||||||
ClientHTTP1 wrongName("localhost", 8095, mismatched);
|
ClientHTTP1 wrongName("localhost", 8110, mismatched);
|
||||||
bool rejected = false;
|
bool rejected = false;
|
||||||
try {
|
try {
|
||||||
wrongName.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
wrongName.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
|
|
@ -141,7 +141,7 @@ int main() {
|
||||||
// insecureNoServerValidation is the dev escape hatch; it has to work,
|
// insecureNoServerValidation is the dev escape hatch; it has to work,
|
||||||
// because the alternative is people shipping their own worse one.
|
// because the alternative is people shipping their own worse one.
|
||||||
{
|
{
|
||||||
ClientHTTP1 insecure("localhost", 8095,
|
ClientHTTP1 insecure("localhost", 8110,
|
||||||
TLSClientCredentials{ .insecureNoServerValidation = true });
|
TLSClientCredentials{ .insecureNoServerValidation = true });
|
||||||
HTTPResponse response = insecure.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
HTTPResponse response = insecure.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
Check(response.body == "Hello World!", "insecureNoServerValidation talks to the same server");
|
Check(response.body == "Hello World!", "insecureNoServerValidation talks to the same server");
|
||||||
|
|
@ -154,7 +154,7 @@ int main() {
|
||||||
{
|
{
|
||||||
const std::uint64_t before = listener.listener.HandshakeFailureCount();
|
const std::uint64_t before = listener.listener.HandshakeFailureCount();
|
||||||
try {
|
try {
|
||||||
ClientHTTP1 plaintext("localhost", 8095);
|
ClientHTTP1 plaintext("localhost", 8110);
|
||||||
plaintext.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
plaintext.Send(CreateRequestHTTP("GET", "/", "localhost"));
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception&) {
|
||||||
// Expected: the listener drops it without answering.
|
// Expected: the listener drops it without answering.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue