Files
fgl-aircon/tests/ayla/test_httpd.cpp
Petr Polezhaev e91d4605b1 core(M1): ayla-криптография, конверт, JSON (jsmn), HTTP-клиент
- crypto: KDF Ayla (двойной HMAC, suffix 0x30/31/32; app/dev направления),
  AES-256-CBC с непрерывной цепочкой (iv обновляется mbedtls на месте),
  Java-паддинг >=1 NUL; mode-latch против misuse (encrypt|decrypt);
  zeroize ключей при повторном init; векторы из APK (4 сессии × 4 сообщения,
  включая legacy-приём без NUL) — scripts/gen_kdf_vectors.py.
- envelope: pack/unpack {"enc","sign"}; расшифровка (движение цепочки)
  ДО проверки подписи; сравнение подписи в константном времени;
  extract_seq_no — depth-1 сканер без лимита токенов (OOB после escape
  исправлен, регресс-тесты по ASan-репро ревьюера).
- json: Writer (фикс. буфер, стек глубин, escape, ok()=false при
  переполнении) + Doc на jsmn (64 токена, unescape, overflow-guard).
- httpc: блокирующий POST/PUT для local_reg (статус 200-599, дренаж,
  shutdown перед close).
- third_party/jsmn (MIT, JSMN_STATIC).
- CMake: mbedtls системный (/usr/include/mbedtls3) или FetchContent;
  IDF: PRIV_REQUIRES mbedtls.
- CI: 3 конфигурации — gcc-Release, gcc-Debug+ASan/UBSan, clang-Release;
  6/6 тестов стабильно; ESP-IDF esp32 build complete.
Ревью под-агентом: 3 круга (OOB-блокер + тестовые флаки закрыты), APPROVED.
2026-09-22 15:46:18 +03:00

227 lines
8.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Тесты мини-httpd: парсинг запросов, keep-alive, 404 по умолчанию,
// корректный stop при живом keep-alive соединении.
#include "doctest/doctest.h"
#include <cstring>
#include <string>
#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<unsigned>(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<long>(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<unsigned>(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<long>(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<HandlerCtx*>(ctx);
h->last_req = req;
h->calls++;
resp.status = 200;
static const char kBody[] = "{\"ok\":true}";
resp.body = reinterpret_cast<const uint8_t*>(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<long>(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<long>(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<long>(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();
}