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.
This commit is contained in:
2026-09-22 15:45:20 +03:00
parent b44004627b
commit e91d4605b1
19 changed files with 2120 additions and 3 deletions

106
tests/ayla/test_httpc.cpp Normal file
View File

@@ -0,0 +1,106 @@
// Тесты HTTP-клиента против in-process httpd.
#include "doctest/doctest.h"
#include <cstring>
#include <string>
#include "ayla/httpc.hpp"
#include "ayla/httpd.hpp"
#include "ayla/platform/platform.hpp"
namespace {
struct Ctx {
fgl::ayla::HttpRequest last;
int calls = 0;
int status_to_return = 200;
};
bool handler(const fgl::ayla::HttpRequest& req, fgl::ayla::HttpResponse& resp,
void* ctx) {
auto* c = static_cast<Ctx*>(ctx);
c->last = req;
c->calls++;
resp.status = c->status_to_return;
static const uint8_t kBody[] = "{\"x\":1}";
resp.body = kBody;
resp.body_len = sizeof(kBody) - 1;
return true;
}
} // namespace
TEST_CASE("httpc: POST с телом и query, статус пробрасывается") {
fgl::ayla::HttpServer srv;
Ctx ctx;
ctx.status_to_return = 202;
REQUIRE(srv.start(0, handler, &ctx));
const uint32_t ip = (127u << 24) | 1u; // для лога, не используется
fgl::ayla::HttpcRequest req;
req.method = "POST";
req.host = "127.0.0.1";
req.port = srv.port();
req.path = "/local_reg.json";
req.query = "dsn=AC000W00REDACTED";
const char body[] = "{\"local_reg\":{\"notify\":1}}";
req.body = reinterpret_cast<const uint8_t*>(body);
req.body_len = strlen(body);
req.timeout_ms = 3000;
fgl::ayla::HttpcResponse resp;
REQUIRE(fgl::ayla::httpc_perform(req, &resp));
CHECK(resp.transport_ok);
CHECK(resp.status == 202);
CHECK(ctx.calls == 1);
CHECK(std::string(ctx.last.method) == "POST");
CHECK(std::string(ctx.last.target) == "/local_reg.json");
CHECK(std::string(ctx.last.query) == "dsn=AC000W00REDACTED");
CHECK(ctx.last.body_len == strlen(body));
CHECK(memcmp(ctx.last.body, body, strlen(body)) == 0);
srv.stop();
(void)ip;
}
TEST_CASE("httpc: ошибки транспорта (соединение отвергнуто)") {
// Занимаем и освобождаем порт — соединение точно отвергнётся.
fgl::ayla::HttpServer tmp;
REQUIRE(tmp.start(0, nullptr, nullptr));
const uint16_t port = tmp.port();
tmp.stop();
fgl::plat::sleep_ms(100);
fgl::ayla::HttpcRequest req;
req.method = "PUT";
req.host = "127.0.0.1";
req.port = port;
req.timeout_ms = 2000;
fgl::ayla::HttpcResponse resp;
bool ok = fgl::ayla::httpc_perform(req, &resp);
// Либо connect сразу отклонён (false), либо таймаут — оба варианта ошибки.
if (ok) {
FAIL("ожидаля отказ соединения");
}
CHECK_FALSE(resp.transport_ok);
}
TEST_CASE("httpc: PUT без тела, ответ 503 c телом (дренаж)") {
fgl::ayla::HttpServer srv;
Ctx ctx;
ctx.status_to_return = 503;
REQUIRE(srv.start(0, handler, &ctx));
fgl::ayla::HttpcRequest req;
req.method = "PUT";
req.host = "127.0.0.1";
req.port = srv.port();
req.path = "/local_reg.json";
req.body = nullptr;
req.body_len = 0;
req.timeout_ms = 3000;
fgl::ayla::HttpcResponse resp;
REQUIRE(fgl::ayla::httpc_perform(req, &resp));
CHECK(resp.status == 503);
CHECK(ctx.calls == 1);
CHECK(ctx.last.body_len == 0);
srv.stop();
}