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:
128
src/ayla/httpc.cpp
Normal file
128
src/ayla/httpc.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "ayla/httpc.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "ayla/log.hpp"
|
||||
#include "ayla/platform/platform.hpp"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
namespace {
|
||||
|
||||
// Читает статус-строку "HTTP/1.x NNN ..."; возвращает код или -1.
|
||||
int read_status(int fd) {
|
||||
char line[64];
|
||||
size_t len = 0;
|
||||
for (;;) {
|
||||
if (len + 1 >= sizeof(line)) return -1;
|
||||
uint8_t ch;
|
||||
long n = fgl::plat::tcp_recv(fd, &ch, 1);
|
||||
if (n <= 0) return -1;
|
||||
if (ch == '\n') break;
|
||||
line[len++] = static_cast<char>(ch);
|
||||
}
|
||||
// "HTTP/1.1 202 Accepted"
|
||||
if (len < 12 || strncmp(line, "HTTP/", 5) != 0) return -1;
|
||||
const char* sp = strchr(line, ' ');
|
||||
if (sp == nullptr) return -1;
|
||||
int code = atoi(sp + 1);
|
||||
return (code >= 200 && code <= 599) ? code : -1;
|
||||
}
|
||||
|
||||
// Дочитывает заголовки до пустой строки и тело по Content-Length (до лимита).
|
||||
bool drain_response(int fd) {
|
||||
char line[256];
|
||||
long content_length = 0;
|
||||
for (;;) {
|
||||
size_t i = 0;
|
||||
for (;;) {
|
||||
if (i + 1 >= sizeof(line)) return false;
|
||||
uint8_t ch;
|
||||
long n = fgl::plat::tcp_recv(fd, &ch, 1);
|
||||
if (n <= 0) return false;
|
||||
if (ch == '\n') break;
|
||||
line[i++] = static_cast<char>(ch);
|
||||
}
|
||||
if (i > 0 && line[i - 1] == '\r') i--;
|
||||
line[i] = '\0';
|
||||
if (i == 0) break; // конец заголовков
|
||||
if ((line[0] | 0x20) == 'c' && strncasecmp(line, "Content-Length:", 15) == 0) {
|
||||
content_length = strtol(line + 15, nullptr, 10);
|
||||
}
|
||||
}
|
||||
if (content_length > 0) {
|
||||
uint8_t sink[256];
|
||||
long remaining = content_length;
|
||||
if (remaining > 4096) remaining = 4096; // тела local_reg нет — ограничим
|
||||
while (remaining > 0) {
|
||||
size_t chunk = remaining < static_cast<long>(sizeof(sink))
|
||||
? static_cast<size_t>(remaining)
|
||||
: sizeof(sink);
|
||||
long n = fgl::plat::tcp_recv(fd, sink, chunk);
|
||||
if (n <= 0) break;
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool httpc_perform(const HttpcRequest& req, HttpcResponse* resp) {
|
||||
if (resp == nullptr || req.host == nullptr) return false;
|
||||
resp->status = 0;
|
||||
resp->transport_ok = false;
|
||||
|
||||
int fd = fgl::plat::tcp_connect(req.host, req.port, req.timeout_ms);
|
||||
if (fd < 0) {
|
||||
FGL_LOGD("httpc: connect %s:%u failed", req.host,
|
||||
static_cast<unsigned>(req.port));
|
||||
return false;
|
||||
}
|
||||
fgl::plat::tcp_set_timeout(fd, req.timeout_ms, req.timeout_ms);
|
||||
|
||||
char head[256];
|
||||
int n = snprintf(head, sizeof(head),
|
||||
"%s %s%s%s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Accept: application/json\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %u\r\n"
|
||||
"\r\n",
|
||||
req.method, req.path, req.query != nullptr ? "?" : "",
|
||||
req.query != nullptr ? req.query : "", req.host,
|
||||
req.content_type, static_cast<unsigned>(req.body_len));
|
||||
if (n <= 0 || static_cast<size_t>(n) >= sizeof(head)) {
|
||||
fgl::plat::tcp_close(fd);
|
||||
return false;
|
||||
}
|
||||
bool sent = fgl::plat::tcp_send(fd, head, static_cast<size_t>(n)) == n;
|
||||
if (sent && req.body != nullptr && req.body_len > 0) {
|
||||
sent = fgl::plat::tcp_send(fd, req.body, req.body_len) ==
|
||||
static_cast<long>(req.body_len);
|
||||
}
|
||||
if (!sent) {
|
||||
FGL_LOGD("httpc: send failed");
|
||||
fgl::plat::tcp_close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = read_status(fd);
|
||||
if (status < 0) {
|
||||
fgl::plat::tcp_close(fd);
|
||||
return false;
|
||||
}
|
||||
drain_response(fd);
|
||||
fgl::plat::tcp_shutdown(fd);
|
||||
fgl::plat::tcp_close(fd);
|
||||
|
||||
resp->status = status;
|
||||
resp->transport_ok = true;
|
||||
FGL_LOGD("httpc: %s %s -> %d", req.method, req.path, status);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla
|
||||
Reference in New Issue
Block a user