Crafter.Network/tests/ShouldSurviveAbuseHTTP1/main.cpp

127 lines
6 KiB
C++
Raw Normal View History

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// A server is only as good as its worst peer. This drives ListenerHTTP1
// with many concurrent clients and then with peers that behave badly —
// vanishing mid-request, sending garbage, opening and dropping connections,
// stalling forever — and requires that it keeps serving correctly
// throughout and still shuts down promptly at the end.
import Crafter.Network;
import std;
using namespace Crafter;
namespace {
std::atomic<int> failures{0};
void Check(bool condition, std::string_view what) {
if (!condition) {
std::println("FAIL: {}", what);
failures.fetch_add(1);
}
}
constexpr std::uint16_t port = 8095;
// Raw socket, no HTTP client in the way, so the test can send things a
// well-behaved client never would.
void SendRaw(std::string_view bytes, bool waitForReply) {
ClientTCP socket("localhost", port);
if (!bytes.empty()) socket.Send(bytes.data(), static_cast<std::uint32_t>(bytes.size()));
if (waitForReply) {
try {
(void)socket.RecieveSync();
} catch (const std::exception&) {
// A dropped connection is an acceptable answer to nonsense.
}
}
}
}
int main() {
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
routes["/work"] = [](const HTTPRequest& request) {
return CreateResponseHTTP("200", request.body.empty() ? std::string("empty") : request.body);
};
try {
ListenerAsyncHTTP1 listener(port, std::move(routes));
// Keep the stalled-peer case quick; the defaults are measured in
// tens of seconds, which is right for a real server and wrong for a
// test.
listener.listener.requestTimeout = std::chrono::milliseconds(300);
listener.listener.keepAliveTimeout = std::chrono::milliseconds(500);
// ── Many clients at once, each reusing its own connection ──────
constexpr int clientCount = 24;
constexpr int requestsPerClient = 8;
std::atomic<int> completed{0};
std::vector<std::thread> clients;
for (int i = 0; i < clientCount; ++i) {
clients.emplace_back([i, &completed] {
try {
ClientHTTP1 client("localhost", port);
for (int request = 0; request < requestsPerClient; ++request) {
const std::string payload = std::format("client-{}-request-{}", i, request);
HTTPResponse response = client.Send(
CreateRequestHTTP("POST", "/work", "localhost", payload));
Check(response.status == "200", "concurrent request status");
Check(response.body == payload, "concurrent request body matches its sender");
completed.fetch_add(1);
}
} catch (const std::exception& error) {
std::println("client threw: {}", error.what());
failures.fetch_add(1);
}
});
}
for (auto& client : clients) client.join();
Check(completed.load() == clientCount * requestsPerClient, "every concurrent request finished");
Check(listener.listener.AcceptedCount() == clientCount,
"each client used exactly one connection");
// ── Peers that misbehave ──────────────────────────────────────
SendRaw("", false); // connect, say nothing, leave
SendRaw("GET /work HTTP/1.1\r\nHost: x\r\n", false); // headers cut short
SendRaw("POST /work HTTP/1.1\r\nHost: x\r\nContent-Length: 100\r\n\r\nshort", false);
SendRaw("not http at all\r\n\r\n", true); // garbage start line
SendRaw("GET / HTTP/1.1\r\nHost : x\r\n\r\n", true); // smuggling-shaped header
SendRaw(std::string("GET /") + std::string(16 * 1024, 'z') + " HTTP/1.1\r\n\r\n", true);
// A peer that opens a request and then just sits there must be cut
// loose by the request timeout rather than holding its thread.
{
ClientTCP stalled("localhost", port);
const std::string partial = "POST /work HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\n\r\nab";
stalled.Send(partial.data(), static_cast<std::uint32_t>(partial.size()));
const auto start = std::chrono::steady_clock::now();
try {
(void)stalled.RecieveUntilCloseSync();
} catch (const std::exception&) {}
const auto elapsed = std::chrono::steady_clock::now() - start;
Check(elapsed < std::chrono::seconds(5), "a stalled peer is timed out, not waited on");
}
// ── Still healthy afterwards ──────────────────────────────────
ClientHTTP1 client("localhost", port);
HTTPResponse response = client.Send(CreateRequestHTTP("POST", "/work", "localhost",
std::string("still here")));
Check(response.status == "200", "the server survived the abuse");
Check(response.body == "still here", "and still answers correctly");
// Shutdown has to finish quickly even with connections open.
const auto start = std::chrono::steady_clock::now();
listener.Stop();
const auto elapsed = std::chrono::steady_clock::now() - start;
Check(elapsed < std::chrono::seconds(5), "Stop() returns promptly with a connection still open");
} catch (const std::exception& error) {
std::println("threw: {}", error.what());
return 1;
}
if (failures.load() != 0) {
std::println("{} check(s) failed", failures.load());
return 1;
}
return 0;
}