127 lines
6 KiB
C++
127 lines
6 KiB
C++
|
|
//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;
|
||
|
|
}
|