// Тесты мини-httpd: парсинг запросов, keep-alive, 404 по умолчанию, // корректный stop при живом keep-alive соединении. #include "doctest/doctest.h" #include #include #include "ayla/httpd.hpp" #include "ayla/platform/platform.hpp" namespace { // Простой блокирующий HTTP-клиент для тестов (ephemeral-порт сервера). std::string build_request(const char* method, const char* target, const char* body, bool keep_alive) { char head[512]; int n = snprintf(head, sizeof(head), "%s %s HTTP/1.1\r\n" "Content-Length: %u\r\n" "Connection: %s\r\n" "\r\n", method, target, body != nullptr ? static_cast(strlen(body)) : 0u, keep_alive ? "keep-alive" : "close"); std::string req(head, head + n); if (body != nullptr) req += body; return req; } struct RawResponse { int status = 0; std::string body; }; // Один запрос на новом соединении. RawResponse http_request(uint16_t port, const char* method, const char* target, const char* body = nullptr, bool keep_alive = true) { int fd = fgl::plat::tcp_connect("127.0.0.1", port, 2000); REQUIRE(fd >= 0); fgl::plat::tcp_set_timeout(fd, 2000, 2000); std::string req = build_request(method, target, body, keep_alive); REQUIRE(fgl::plat::tcp_send(fd, req.data(), req.size()) == static_cast(req.size())); std::string raw; char buf[1024]; for (;;) { long r = fgl::plat::tcp_recv(fd, buf, sizeof(buf)); if (r <= 0) break; raw.append(buf, buf + r); size_t hdr_end = raw.find("\r\n\r\n"); if (hdr_end != std::string::npos) { unsigned clen = 0; size_t cl = raw.find("Content-Length:"); if (cl != std::string::npos) { clen = static_cast(atoi(raw.c_str() + cl + 15)); } if (raw.size() >= hdr_end + 4 + clen) break; } } fgl::plat::tcp_close(fd); RawResponse out; out.status = atoi(raw.c_str() + 9); // "HTTP/1.1 NNN" size_t hdr_end = raw.find("\r\n\r\n"); if (hdr_end != std::string::npos) out.body = raw.substr(hdr_end + 4); return out; } } // namespace TEST_CASE("httpd: ephemeral порт и 404 по умолчанию") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); const uint16_t port = srv.port(); REQUIRE(port != 0); auto resp = http_request(port, "GET", "/local_lan/commands.json"); CHECK(resp.status == 404); CHECK(resp.body.empty()); srv.stop(); CHECK_FALSE(srv.is_running()); } TEST_CASE("httpd: keep-alive — два запроса на одном соединении") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); const uint16_t port = srv.port(); int fd = fgl::plat::tcp_connect("127.0.0.1", port, 2000); REQUIRE(fd >= 0); fgl::plat::tcp_set_timeout(fd, 2000, 2000); for (int i = 0; i < 2; i++) { std::string req = build_request("GET", "/local_lan/commands.json", nullptr, true); REQUIRE(fgl::plat::tcp_send(fd, req.data(), req.size()) == static_cast(req.size())); std::string raw; char buf[512]; while (raw.find("\r\n\r\n") == std::string::npos) { long r = fgl::plat::tcp_recv(fd, buf, sizeof(buf)); if (r <= 0) break; raw.append(buf, buf + r); } CHECK(atoi(raw.c_str() + 9) == 404); } fgl::plat::tcp_close(fd); srv.stop(); } namespace { struct HandlerCtx { fgl::ayla::HttpRequest last_req; int calls = 0; }; bool capture_handler(const fgl::ayla::HttpRequest& req, fgl::ayla::HttpResponse& resp, void* ctx) { auto* h = static_cast(ctx); h->last_req = req; h->calls++; resp.status = 200; static const char kBody[] = "{\"ok\":true}"; resp.body = reinterpret_cast(kBody); resp.body_len = sizeof(kBody) - 1; return true; } } // namespace TEST_CASE("httpd: обработчик, парсинг метода/пути/query/тела/пира") { fgl::ayla::HttpServer srv; HandlerCtx h; REQUIRE(srv.start(0, capture_handler, &h)); auto resp = http_request(srv.port(), "POST", "/local_lan/property/datapoint.json?cmd_id=5&status=200", "{\"enc\":\"abc\"}"); CHECK(resp.status == 200); CHECK(resp.body == std::string("{\"ok\":true}")); CHECK(h.calls == 1); CHECK(std::string(h.last_req.method) == "POST"); CHECK(std::string(h.last_req.target) == "/local_lan/property/datapoint.json"); CHECK(std::string(h.last_req.query) == "cmd_id=5&status=200"); CHECK(h.last_req.body_len == 13); CHECK(memcmp(h.last_req.body, "{\"enc\":\"abc\"}", 13) == 0); CHECK(h.last_req.peer_ip == 0x7f000001); srv.stop(); } TEST_CASE("httpd: oversized body -> 400 + Connection: close") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); std::string big(fgl::ayla::kHttpdMaxBody + 100, 'x'); int fd = fgl::plat::tcp_connect("127.0.0.1", srv.port(), 2000); REQUIRE(fd >= 0); fgl::plat::tcp_set_timeout(fd, 2000, 2000); std::string req = build_request("POST", "/local_lan/property/datapoint.json", big.c_str(), false); REQUIRE(fgl::plat::tcp_send(fd, req.data(), req.size()) == static_cast(req.size())); std::string raw; char buf[1024]; for (;;) { long r = fgl::plat::tcp_recv(fd, buf, sizeof(buf)); if (r <= 0) break; raw.append(buf, buf + r); } fgl::plat::tcp_close(fd); CHECK(atoi(raw.c_str() + 9) == 400); CHECK(raw.find("Connection: close") != std::string::npos); srv.stop(); } TEST_CASE("httpd: stop() при открытом keep-alive соединении (нет UAF/зависания)") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); const uint16_t port = srv.port(); // Соединение без запроса: поток сервера сидит в recv с 30с таймаутом. int fd = fgl::plat::tcp_connect("127.0.0.1", port, 2000); REQUIRE(fd >= 0); fgl::plat::sleep_ms(200); // даём серверу принять и уйти в ожидание uint64_t t0 = fgl::plat::now_ms(); srv.stop(); // должен прервать соединение shutdown'ом и join'нуть поток uint64_t elapsed = fgl::plat::now_ms() - t0; CHECK(srv.port() == port); CHECK(elapsed < 2000); // не ждём rx-таймаут fgl::plat::tcp_close(fd); } TEST_CASE("httpd: stop() сразу после start()") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); uint64_t t0 = fgl::plat::now_ms(); srv.stop(); // поток мог не дойти до poll — join всё равно быстрый CHECK(fgl::plat::now_ms() - t0 < 2000); // Сервер можно перезапустить после stop. REQUIRE(srv.start(0, nullptr, nullptr)); auto resp = http_request(srv.port(), "GET", "/"); CHECK(resp.status == 404); srv.stop(); } TEST_CASE("httpd: бесконечный стрим заголовков завершается (лимит 1КБ)") { fgl::ayla::HttpServer srv; REQUIRE(srv.start(0, nullptr, nullptr)); int fd = fgl::plat::tcp_connect("127.0.0.1", srv.port(), 2000); REQUIRE(fd >= 0); fgl::plat::tcp_set_timeout(fd, 2000, 2000); // Отправляем request line и бессрочный поток заголовков малыми кусками. std::string req = "GET / HTTP/1.1\r\n"; REQUIRE(fgl::plat::tcp_send(fd, req.data(), req.size()) == static_cast(req.size())); const char* filler = "X-Pad: 0123456789012345678901234567890123456789\r\n"; size_t flen = strlen(filler); for (int i = 0; i < 80; i++) { // ~4КБ — больше лимита if (fgl::plat::tcp_send(fd, filler, flen) != static_cast(flen)) { break; // сервер уже закрыл соединение (лимит превышен) — RST допустим } } // Сервер должен перестать читать и закрыть соединение: recv завершается // (EOF или RST после close с непрочитанными данными), а не висит вечно. fgl::plat::tcp_set_timeout(fd, 3000, 3000); char buf[64]; long r; while ((r = fgl::plat::tcp_recv(fd, buf, sizeof(buf))) > 0) { } CHECK(r <= 0); // соединение закрыто сервером fgl::plat::tcp_close(fd); srv.stop(); }