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:
137
src/ayla/crypto.cpp
Normal file
137
src/ayla/crypto.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "ayla/crypto.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "mbedtls/platform_util.h"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
namespace {
|
||||
|
||||
void hmac_sha256(const uint8_t* key, size_t key_len, const uint8_t* data,
|
||||
size_t data_len, uint8_t out[kSha256Len]) {
|
||||
const mbedtls_md_info_t* md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||
mbedtls_md_hmac(md, key, key_len, data, data_len, out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DirectionCrypto::derive(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* msg, size_t msg_len,
|
||||
uint8_t out[kSha256Len]) {
|
||||
// key = HMAC_k( HMAC_k(msg) || msg )
|
||||
if (msg_len > sizeof(uint8_t) * (kSha256Len + 192) - kSha256Len) {
|
||||
return false; // невозможно по протоколу (<=73 байт)
|
||||
}
|
||||
uint8_t inner[kSha256Len];
|
||||
hmac_sha256(key, key_len, msg, msg_len, inner);
|
||||
uint8_t buf[kSha256Len + 192];
|
||||
memcpy(buf, inner, kSha256Len);
|
||||
memcpy(buf + kSha256Len, msg, msg_len);
|
||||
hmac_sha256(key, key_len, buf, kSha256Len + msg_len, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DirectionCrypto::init(const char* lanip_key, const char* rnd_a,
|
||||
const char* rnd_b, int64_t t_a, int64_t t_b) {
|
||||
if (lanip_key == nullptr || rnd_a == nullptr || rnd_b == nullptr) return false;
|
||||
initialized_ = false;
|
||||
mode_ = 0;
|
||||
// затираем предыдущий материал
|
||||
mbedtls_platform_zeroize(sign_key_, sizeof(sign_key_));
|
||||
mbedtls_platform_zeroize(aes_key_, sizeof(aes_key_));
|
||||
mbedtls_platform_zeroize(chain_iv_, sizeof(chain_iv_));
|
||||
|
||||
uint8_t msg[128];
|
||||
size_t off = 0;
|
||||
auto append = [&](const char* s) {
|
||||
size_t n = strlen(s);
|
||||
if (off + n > sizeof(msg)) return false;
|
||||
memcpy(msg + off, s, n);
|
||||
off += n;
|
||||
return true;
|
||||
};
|
||||
char t_a_buf[24], t_b_buf[24];
|
||||
snprintf(t_a_buf, sizeof(t_a_buf), "%lld", static_cast<long long>(t_a));
|
||||
snprintf(t_b_buf, sizeof(t_b_buf), "%lld", static_cast<long long>(t_b));
|
||||
if (!append(rnd_a) || !append(rnd_b) || !append(t_a_buf) || !append(t_b_buf) ||
|
||||
off + 1 > sizeof(msg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* k = reinterpret_cast<const uint8_t*>(lanip_key);
|
||||
size_t klen = strlen(lanip_key);
|
||||
|
||||
uint8_t block[kSha256Len];
|
||||
msg[off] = 0x30; // sign key
|
||||
if (!derive(k, klen, msg, off + 1, block)) return false;
|
||||
memcpy(sign_key_, block, kHmacKeyLen);
|
||||
|
||||
msg[off] = 0x31; // aes key
|
||||
if (!derive(k, klen, msg, off + 1, block)) return false;
|
||||
memcpy(aes_key_, block, kAesKeyLen);
|
||||
|
||||
msg[off] = 0x32; // iv seed (первые 16 байт)
|
||||
if (!derive(k, klen, msg, off + 1, block)) return false;
|
||||
memcpy(chain_iv_, block, kAesBlockLen);
|
||||
|
||||
mbedtls_aes_init(&aes_enc_);
|
||||
mbedtls_aes_init(&aes_dec_);
|
||||
if (mbedtls_aes_setkey_enc(&aes_enc_, aes_key_, 256) != 0) return false;
|
||||
if (mbedtls_aes_setkey_dec(&aes_dec_, aes_key_, 256) != 0) return false;
|
||||
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
long DirectionCrypto::encrypt(uint8_t* out, size_t out_cap, const uint8_t* in,
|
||||
size_t in_len) {
|
||||
if (!initialized_) return -1;
|
||||
if (mode_ == 0) mode_ = 1;
|
||||
if (mode_ != 1) return -1; // направление уже в режиме расшифровки
|
||||
// Java-паддинг: >=1 NUL, кратно 16.
|
||||
size_t padded = ((in_len + 1 + kAesBlockLen - 1) / kAesBlockLen) * kAesBlockLen;
|
||||
if (padded > out_cap) return -1;
|
||||
uint8_t buf[kAesBlockLen * 128]; // до 2048 байт (kHttpdMaxBody)
|
||||
if (padded > sizeof(buf)) return -1;
|
||||
memcpy(buf, in, in_len);
|
||||
memset(buf + in_len, 0, padded - in_len);
|
||||
// mbedtls_aes_crypt_cbc использует и обновляет chain_iv_ на месте —
|
||||
// это и есть состояние цепочки.
|
||||
if (mbedtls_aes_crypt_cbc(&aes_enc_, MBEDTLS_AES_ENCRYPT, padded, chain_iv_,
|
||||
buf, out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<long>(padded);
|
||||
}
|
||||
|
||||
long DirectionCrypto::decrypt(uint8_t* out, size_t out_cap, const uint8_t* in,
|
||||
size_t in_len) {
|
||||
if (!initialized_) return -1;
|
||||
if (mode_ == 0) mode_ = 2;
|
||||
if (mode_ != 2) return -1; // направление уже в режиме шифрования
|
||||
if (in_len == 0 || in_len % kAesBlockLen != 0 || in_len > out_cap) return -1;
|
||||
if (mbedtls_aes_crypt_cbc(&aes_dec_, MBEDTLS_AES_DECRYPT, in_len, chain_iv_,
|
||||
in, out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
// Снять завершающие NUL (по крайней мере один в Java-паддинге, но
|
||||
// принимаем и строго-кратные длины без NUL).
|
||||
size_t len = in_len;
|
||||
while (len > 0 && out[len - 1] == 0) len--;
|
||||
return static_cast<long>(len);
|
||||
}
|
||||
|
||||
void DirectionCrypto::sign(uint8_t out[kSha256Len], const uint8_t* data,
|
||||
size_t len) const {
|
||||
hmac_sha256(sign_key_, kHmacKeyLen, data, len, out);
|
||||
}
|
||||
|
||||
bool SessionCrypto::init(const char* lanip_key, const char* random_1,
|
||||
const char* random_2, int64_t time_1, int64_t time_2) {
|
||||
return app.init(lanip_key, random_1, random_2, time_1, time_2) &&
|
||||
dev.init(lanip_key, random_2, random_1, time_2, time_1);
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla
|
||||
70
src/ayla/crypto.hpp
Normal file
70
src/ayla/crypto.hpp
Normal file
@@ -0,0 +1,70 @@
|
||||
// Сессионная криптография Ayla LAN (docs/PROTOCOL.md §3).
|
||||
// KDF: key = HMAC-SHA256_k(HMAC-SHA256_k(msg) || msg), msg = rnd_a|rnd_b|t_a|t_b|X,
|
||||
// X: 0x30 sign / 0x31 crypto / 0x32 iv(первые 16 байт).
|
||||
// Направления: app (rnd1,rnd2,t1,t2) — исходящие; dev (rnd2,rnd1,t2,t1) — входящие.
|
||||
// ВАЖНО: AES-CBC-цепочка непрерывна в рамках сессии (каждое сообщение
|
||||
// продолжает цепочку с последнего шифроблока предыдущего).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "mbedtls/aes.h"
|
||||
#include "mbedtls/md.h"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
constexpr size_t kSha256Len = 32;
|
||||
constexpr size_t kAesKeyLen = 32; // AES-256
|
||||
constexpr size_t kAesBlockLen = 16;
|
||||
constexpr size_t kHmacKeyLen = 32;
|
||||
|
||||
class DirectionCrypto {
|
||||
public:
|
||||
// Выводит ключи направления. rnd_a/rnd_b — 16-символьные токены,
|
||||
// t_a/t_b — 64-битные «времена» (материал KDF, значение произвольно).
|
||||
// app: (random_1, random_2, time_1, time_2); dev: (random_2, random_1, time_2, time_1).
|
||||
bool init(const char* lanip_key, const char* rnd_a, const char* rnd_b,
|
||||
int64_t t_a, int64_t t_b);
|
||||
|
||||
// Шифрует in (любая длина) с zero-паддингом (>=1 NUL, кратно 16).
|
||||
// out_cap должен быть >= padded_len = ((in_len+1)/16+1)*16.
|
||||
// Возвращает длину шифротекста или -1. Продолжает CBC-цепочку.
|
||||
// ВАЖНО: у направления ОДНА цепочка; первый вызов фиксирует режим
|
||||
// (шифрование либо расшифровка) — смешение режимов возвращает -1.
|
||||
long encrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
|
||||
|
||||
// Расшифровывает in (длина кратна 16), снимает завершающие NUL.
|
||||
// Возвращает длину открытого текста или -1. Продолжает CBC-цепочку
|
||||
// (даже если далее подпись не сойдётся — состояние обязано двигаться).
|
||||
long decrypt(uint8_t* out, size_t out_cap, const uint8_t* in, size_t in_len);
|
||||
|
||||
// HMAC-SHA256(sign_key, data) — подпись/проверка сообщений направления.
|
||||
void sign(uint8_t out[kSha256Len], const uint8_t* data, size_t len) const;
|
||||
|
||||
bool initialized() const { return initialized_; }
|
||||
|
||||
private:
|
||||
static bool derive(const uint8_t* key, size_t key_len, const uint8_t* msg,
|
||||
size_t msg_len, uint8_t out[kSha256Len]);
|
||||
|
||||
bool initialized_ = false;
|
||||
uint8_t mode_ = 0; // 0 не зафиксирован, 1 шифрование, 2 расшифровка
|
||||
uint8_t sign_key_[kHmacKeyLen];
|
||||
uint8_t aes_key_[kAesKeyLen];
|
||||
uint8_t chain_iv_[kAesBlockLen]; // текущее состояние CBC-цепочки
|
||||
mbedtls_aes_context aes_enc_;
|
||||
mbedtls_aes_context aes_dec_;
|
||||
};
|
||||
|
||||
// Пара направлений одной сессии.
|
||||
struct SessionCrypto {
|
||||
DirectionCrypto app; // приложение -> модуль
|
||||
DirectionCrypto dev; // модуль -> приложение
|
||||
|
||||
// lanip_key — строка base64 КАК ЕСТЬ (utf-8 байты, не декодировать).
|
||||
bool init(const char* lanip_key, const char* random_1, const char* random_2,
|
||||
int64_t time_1, int64_t time_2);
|
||||
};
|
||||
|
||||
} // namespace fgl::ayla
|
||||
180
src/ayla/envelope.cpp
Normal file
180
src/ayla/envelope.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "ayla/envelope.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
namespace {
|
||||
|
||||
bool b64_encode(const uint8_t* in, size_t in_len, char* out, size_t out_cap) {
|
||||
size_t olen = 0;
|
||||
if (mbedtls_base64_encode(reinterpret_cast<unsigned char*>(out), out_cap,
|
||||
&olen, in, in_len) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true; // mbedtls пишет NUL-терминатор
|
||||
}
|
||||
|
||||
bool b64_decode(const char* in, uint8_t* out, size_t out_cap, size_t* out_len) {
|
||||
size_t olen = 0;
|
||||
if (mbedtls_base64_decode(out, out_cap, &olen,
|
||||
reinterpret_cast<const unsigned char*>(in),
|
||||
strlen(in)) != 0) {
|
||||
return false;
|
||||
}
|
||||
*out_len = olen;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Извлекает seq_no верхнего уровня из '{"seq_no":N,"data":...}'.
|
||||
// Ручной depth-1 сканер (без ограничений на размер data; вход уже
|
||||
// проверен подписью). Понимает пробелы между лексемами.
|
||||
bool extract_seq_no(const char* json, int64_t* seq_no) {
|
||||
const char* p = json;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p != '{') return false;
|
||||
p++;
|
||||
for (;;) {
|
||||
while (*p == ' ' || *p == ',' || *p == '\t') p++;
|
||||
if (*p == '}' || *p == '\0') return false;
|
||||
// ключ
|
||||
if (*p != '"') return false;
|
||||
const char* k = ++p;
|
||||
while (*p != '"' && *p != '\0') p++;
|
||||
if (*p != '"') return false;
|
||||
size_t klen = static_cast<size_t>(p - k);
|
||||
p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p != ':') return false;
|
||||
p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (klen == 6 && strncmp(k, "seq_no", 6) == 0) {
|
||||
if (*p == '-' || (*p >= '0' && *p <= '9')) {
|
||||
*seq_no = strtoll(p, nullptr, 10);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// пропустить значение другого ключа
|
||||
if (*p == '{' || *p == '[') {
|
||||
int depth = 0;
|
||||
bool in_str = false;
|
||||
for (; *p != '\0'; p++) {
|
||||
if (in_str) {
|
||||
if (*p == '\\') {
|
||||
p++;
|
||||
if (*p == '\0') return false; // обрыв после escape
|
||||
} else if (*p == '"') {
|
||||
in_str = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (*p == '"') {
|
||||
in_str = true;
|
||||
} else if (*p == '{' || *p == '[') {
|
||||
depth++;
|
||||
} else if (*p == '}' || *p == ']') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
p++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (*p == '\0') return false;
|
||||
} else if (*p == '"') {
|
||||
p++;
|
||||
while (*p != '"' && *p != '\0') {
|
||||
if (*p == '\\') {
|
||||
p++;
|
||||
if (*p == '\0') return false; // обрыв после escape
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (*p != '"') return false;
|
||||
p++;
|
||||
} else {
|
||||
while (*p != ',' && *p != '}' && *p != '\0') p++;
|
||||
if (*p == '\0') return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сравнение за константное время (аналог mbedtls_ct_memcmp, без зависимости
|
||||
// от версии библиотеки).
|
||||
int ct_memcmp(const uint8_t* a, const uint8_t* b, size_t n) {
|
||||
uint8_t diff = 0;
|
||||
for (size_t i = 0; i < n; i++) diff |= static_cast<uint8_t>(a[i] ^ b[i]);
|
||||
return diff != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool envelope_pack(DirectionCrypto& dir, int64_t seq_no,
|
||||
const char* data_json, char* out, size_t out_cap) {
|
||||
if (data_json == nullptr || out == nullptr) return false;
|
||||
|
||||
// Открытый текст: {"seq_no":N,"data":<data_json>}
|
||||
char plain[kEnvelopeMaxPlain];
|
||||
int n = snprintf(plain, sizeof(plain), "{\"seq_no\":%lld,\"data\":%s}",
|
||||
static_cast<long long>(seq_no), data_json);
|
||||
if (n <= 0 || static_cast<size_t>(n) >= sizeof(plain)) return false;
|
||||
size_t plain_len = static_cast<size_t>(n);
|
||||
|
||||
// Подпись — по байтам открытого текста без паддинга.
|
||||
uint8_t mac[kSha256Len];
|
||||
dir.sign(mac, reinterpret_cast<const uint8_t*>(plain), plain_len);
|
||||
char sign_b64[64];
|
||||
if (!b64_encode(mac, sizeof(mac), sign_b64, sizeof(sign_b64))) return false;
|
||||
|
||||
// Шифрование с zero-паддингом (>=1 NUL, кратно 16).
|
||||
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
long ct_len = dir.encrypt(ct, sizeof(ct),
|
||||
reinterpret_cast<const uint8_t*>(plain), plain_len);
|
||||
if (ct_len < 0) return false;
|
||||
char enc_b64[kEnvelopeMaxB64];
|
||||
if (!b64_encode(ct, static_cast<size_t>(ct_len), enc_b64, sizeof(enc_b64))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int m = snprintf(out, out_cap, "{\"enc\":\"%s\",\"sign\":\"%s\"}", enc_b64,
|
||||
sign_b64);
|
||||
return m > 0 && static_cast<size_t>(m) < out_cap;
|
||||
}
|
||||
|
||||
bool envelope_unpack(DirectionCrypto& dir, const char* enc_b64,
|
||||
const char* sign_b64, char* json_out, size_t out_cap,
|
||||
int64_t* seq_no) {
|
||||
if (enc_b64 == nullptr || sign_b64 == nullptr || json_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t ct[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
size_t ct_len = 0;
|
||||
if (!b64_decode(enc_b64, ct, sizeof(ct), &ct_len)) return false;
|
||||
|
||||
// Расшифровка: CBC-цепочка двигается независимо от исхода проверки подписи.
|
||||
uint8_t plain[kEnvelopeMaxPlain + kAesBlockLen];
|
||||
long plain_len = dir.decrypt(plain, sizeof(plain), ct, ct_len);
|
||||
if (plain_len < 0 || static_cast<size_t>(plain_len) >= out_cap) return false;
|
||||
|
||||
// Проверка подписи по расшифрованным байтам (без паддинга);
|
||||
// сравнение декодированных байт — константное время.
|
||||
uint8_t mac[kSha256Len];
|
||||
dir.sign(mac, plain, static_cast<size_t>(plain_len));
|
||||
uint8_t given[kSha256Len];
|
||||
size_t given_len = 0;
|
||||
if (!b64_decode(sign_b64, given, sizeof(given), &given_len)) return false;
|
||||
if (given_len != kSha256Len) return false;
|
||||
if (ct_memcmp(mac, given, kSha256Len) != 0) return false;
|
||||
|
||||
memcpy(json_out, plain, static_cast<size_t>(plain_len));
|
||||
json_out[plain_len] = '\0';
|
||||
return extract_seq_no(json_out, seq_no);
|
||||
}
|
||||
|
||||
} // namespace fgl::ayla
|
||||
31
src/ayla/envelope.hpp
Normal file
31
src/ayla/envelope.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
// Защищённый конверт Ayla LAN: {"enc":"<b64 AES-CBC>","sign":"<b64 HMAC>"},
|
||||
// открытый текст: {"seq_no":N,"data":<json-объект>} (docs/PROTOCOL.md §3.3).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "ayla/crypto.hpp"
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
constexpr size_t kEnvelopeMaxPlain = 1024; // открытый текст конверта
|
||||
constexpr size_t kEnvelopeMaxB64 = 1800; // base64 шифротекста с запасом
|
||||
|
||||
// Собирает открытый текст '{"seq_no":<seq>,"data":<data_json>}' и упаковывает
|
||||
// в конверт (envelope). dir — направление ОТПРАВИТЕЛЯ (app); состояние
|
||||
// CBC-цепочки направления ДВИГАЕТСЯ. out — строка NUL-terminated.
|
||||
// data_json — готовый JSON-объект (обычно построен json::Writer).
|
||||
bool envelope_pack(DirectionCrypto& dir, int64_t seq_no,
|
||||
const char* data_json, char* out, size_t out_cap);
|
||||
|
||||
// Распаковывает конверт (enc_b64/sign_b64 — NUL-terminated) направлением dev:
|
||||
// расшифровывает (цепочка движется даже при последующей ошибке подписи!),
|
||||
// проверяет подпись. При успехе json_out содержит ОТКРЫТЫЙ ТЕКСТ
|
||||
// ('{"seq_no":..,"data":..}') NUL-terminated, seq_no записан в *seq_no.
|
||||
// Возвращает false при ошибке base64/расшифровки/подписи.
|
||||
bool envelope_unpack(DirectionCrypto& dir, const char* enc_b64,
|
||||
const char* sign_b64, char* json_out, size_t out_cap,
|
||||
int64_t* seq_no);
|
||||
|
||||
} // namespace fgl::ayla
|
||||
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
|
||||
32
src/ayla/httpc.hpp
Normal file
32
src/ayla/httpc.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Минимальный HTTP-клиент для исходящих запросов к модулю (local_reg).
|
||||
// Один запрос — одно соединение; достаточно для keep-alive-цикла протокола
|
||||
// (проверено на приборе: модуль принимает local_reg на новых соединениях).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace fgl::ayla {
|
||||
|
||||
struct HttpcRequest {
|
||||
const char* method = "PUT"; // "POST" | "PUT"
|
||||
const char* host = nullptr; // dotted-quad или DNS-имя
|
||||
uint16_t port = 80;
|
||||
const char* path = "/local_reg.json";
|
||||
const char* query = nullptr; // без '?', может быть nullptr
|
||||
const char* content_type = "application/json";
|
||||
const uint8_t* body = nullptr;
|
||||
size_t body_len = 0;
|
||||
uint32_t timeout_ms = 10000;
|
||||
};
|
||||
|
||||
struct HttpcResponse {
|
||||
int status = 0; // HTTP-код или 0 при ошибке транспорта
|
||||
bool transport_ok = false;
|
||||
};
|
||||
|
||||
// Выполняет запрос блокирующе. Тело ответа не читается (у local_reg оно
|
||||
// пусто), но соединение закрывается корректно (shutdown перед close).
|
||||
bool httpc_perform(const HttpcRequest& req, HttpcResponse* resp);
|
||||
|
||||
} // namespace fgl::ayla
|
||||
253
src/ayla/json.hpp
Normal file
253
src/ayla/json.hpp
Normal file
@@ -0,0 +1,253 @@
|
||||
// Минимальный JSON: writer с фиксированным буфером + парсер (jsmn).
|
||||
// Никаких аллокаций; экранирование строк — достаточное для протокола.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#define JSMN_STATIC
|
||||
#include "third_party/jsmn/jsmn.h"
|
||||
|
||||
namespace fgl::ayla::json {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Writer. Стек глубин <= 8; состояние контейнеров (были ли элементы).
|
||||
// ---------------------------------------------------------------------------
|
||||
class Writer {
|
||||
public:
|
||||
Writer(char* buf, size_t cap) : buf_(buf), cap_(cap) {
|
||||
buf_[0] = '\0';
|
||||
}
|
||||
|
||||
void begin_object() { value_comma(); put('{'); push('{'); }
|
||||
void end_object() { pop(); put('}'); }
|
||||
void begin_array() { value_comma(); put('['); push('['); }
|
||||
void end_array() { pop(); put(']'); }
|
||||
|
||||
// Ключ объекта; за ним должно следовать значение.
|
||||
void key(const char* name) {
|
||||
if (items_[depth_]) put(',');
|
||||
items_[depth_] = true;
|
||||
put('"');
|
||||
str_escape(name);
|
||||
put_raw("\":", 2);
|
||||
await_value_ = true;
|
||||
}
|
||||
|
||||
void string(const char* v) {
|
||||
value_comma();
|
||||
put('"');
|
||||
str_escape(v);
|
||||
put('"');
|
||||
}
|
||||
void integer(int64_t v) {
|
||||
value_comma();
|
||||
char tmp[24];
|
||||
int n = snprintf(tmp, sizeof(tmp), "%lld", static_cast<long long>(v));
|
||||
put_raw(tmp, static_cast<size_t>(n));
|
||||
}
|
||||
void boolean(bool v) { value_comma(); put_raw(v ? "true" : "false", v ? 4 : 5); }
|
||||
void literal(const char* v) { value_comma(); put_raw(v, strlen(v)); }
|
||||
void raw_json(const char* v) { value_comma(); put_raw(v, strlen(v)); }
|
||||
|
||||
const char* c_str() const { return buf_; }
|
||||
size_t size() const { return pos_; }
|
||||
bool ok() const { return ok_; }
|
||||
|
||||
private:
|
||||
static constexpr int kMaxDepth = 8;
|
||||
|
||||
// Запятая/маркер перед ЗНАЧЕНИЕМ (не ключом, не закрытием).
|
||||
void value_comma() {
|
||||
if (await_value_) {
|
||||
await_value_ = false;
|
||||
return; // ключ уже поставил ':' — запятая не нужна
|
||||
}
|
||||
if (items_[depth_]) put(',');
|
||||
items_[depth_] = true;
|
||||
}
|
||||
void push(char c) {
|
||||
if (depth_ + 1 >= kMaxDepth) {
|
||||
ok_ = false;
|
||||
return;
|
||||
}
|
||||
depth_++;
|
||||
items_[depth_] = false;
|
||||
(void)c;
|
||||
}
|
||||
void pop() {
|
||||
if (depth_ > 0) depth_--;
|
||||
await_value_ = false;
|
||||
}
|
||||
void put(char c) {
|
||||
if (!ok_ || pos_ + 1 >= cap_) {
|
||||
ok_ = false;
|
||||
return;
|
||||
}
|
||||
buf_[pos_++] = c;
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
void put_raw(const char* s, size_t n) {
|
||||
if (!ok_ || pos_ + n + 1 > cap_) {
|
||||
ok_ = false;
|
||||
return;
|
||||
}
|
||||
memcpy(buf_ + pos_, s, n);
|
||||
pos_ += n;
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
void str_escape(const char* s) {
|
||||
for (const char* p = s; *p != '\0'; p++) {
|
||||
char c = *p;
|
||||
if (c == '"' || c == '\\') {
|
||||
put('\\');
|
||||
put(c);
|
||||
} else if (c == '\n') {
|
||||
put_raw("\\n", 2);
|
||||
} else if (c == '\t') {
|
||||
put_raw("\\t", 2);
|
||||
} else if (c == '\r') {
|
||||
put_raw("\\r", 2);
|
||||
} else if (c == '\b') {
|
||||
put_raw("\\b", 2);
|
||||
} else if (c == '\f') {
|
||||
put_raw("\\f", 2);
|
||||
} else if (static_cast<unsigned char>(c) < 0x20) {
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||
put_raw(tmp, 6);
|
||||
} else {
|
||||
put(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char* buf_;
|
||||
size_t cap_;
|
||||
size_t pos_ = 0;
|
||||
bool ok_ = true;
|
||||
int depth_ = 0;
|
||||
bool items_[kMaxDepth + 1] = {};
|
||||
bool await_value_ = false;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parser (jsmn): токены поверх исходного буфера, без копирований.
|
||||
// ---------------------------------------------------------------------------
|
||||
constexpr int kMaxTokens = 64;
|
||||
|
||||
class Doc {
|
||||
public:
|
||||
// Разбирает buf (NUL-terminated, будет модифицироваться при unescape).
|
||||
// После вызова buf должен жить, пока используется Doc.
|
||||
bool parse(char* buf) {
|
||||
buf_ = buf;
|
||||
jsmn_init(&p_);
|
||||
count_ = jsmn_parse(&p_, buf, strlen(buf), tokens_, kMaxTokens);
|
||||
return count_ >= 1 && tokens_[0].type == JSMN_OBJECT;
|
||||
}
|
||||
|
||||
// Значение ключа по имени (первое вхождение в порядке документа —
|
||||
// глубина не различается; для форматов протокора ключи уникальны).
|
||||
// Для строки возвращает указатель внутрь buf_ (БЕЗ кавычек,
|
||||
// НЕ NUL-terminated) и длину.
|
||||
bool find(const char* name, const char** out_start, size_t* out_len,
|
||||
jsmntype_t* out_type = nullptr) const {
|
||||
for (int i = 1; i + 1 < count_; i++) {
|
||||
if (tokens_[i].type == JSMN_STRING && tokens_[i].size == 1) {
|
||||
int len = tokens_[i].end - tokens_[i].start;
|
||||
if (len == static_cast<int>(strlen(name)) &&
|
||||
strncmp(buf_ + tokens_[i].start, name, len) == 0) {
|
||||
const jsmntok_t& v = tokens_[i + 1];
|
||||
*out_start = buf_ + v.start;
|
||||
*out_len = static_cast<size_t>(v.end - v.start);
|
||||
if (out_type != nullptr) *out_type = v.type;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Строка в out (снятие кавычек; поддержка \" \\ \/ \n \t \r \b \f).
|
||||
bool get_string(const char* name, char* out, size_t out_cap) const {
|
||||
const char* s;
|
||||
size_t len;
|
||||
jsmntype_t type;
|
||||
if (!find(name, &s, &len, &type) || type != JSMN_STRING) return false;
|
||||
size_t o = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (o + 1 >= out_cap) return false;
|
||||
char c = s[i];
|
||||
if (c == '\\' && i + 1 < len) {
|
||||
char e = s[++i];
|
||||
switch (e) {
|
||||
case '"': out[o++] = '"'; break;
|
||||
case '\\': out[o++] = '\\'; break;
|
||||
case '/': out[o++] = '/'; break;
|
||||
case 'n': out[o++] = '\n'; break;
|
||||
case 't': out[o++] = '\t'; break;
|
||||
case 'r': out[o++] = '\r'; break;
|
||||
case 'b': out[o++] = '\b'; break;
|
||||
case 'f': out[o++] = '\f'; break;
|
||||
default: return false; // \uXXXX не требуется протоколом
|
||||
}
|
||||
} else {
|
||||
out[o++] = c;
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_int(const char* name, int64_t* out) const {
|
||||
const char* s;
|
||||
size_t len;
|
||||
jsmntype_t type;
|
||||
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
|
||||
int64_t v = 0;
|
||||
bool neg = false;
|
||||
size_t i = 0;
|
||||
if (i < len && (s[i] == '-' || s[i] == '+')) {
|
||||
neg = s[i] == '-';
|
||||
i++;
|
||||
}
|
||||
if (i >= len) return false;
|
||||
for (; i < len; i++) {
|
||||
if (s[i] < '0' || s[i] > '9') return false;
|
||||
int64_t digit = s[i] - '0';
|
||||
if (v > (INT64_MAX - digit) / 10) return false; // переполнение
|
||||
v = v * 10 + digit;
|
||||
}
|
||||
*out = neg ? -v : v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_bool(const char* name, bool* out) const {
|
||||
const char* s;
|
||||
size_t len;
|
||||
jsmntype_t type;
|
||||
if (!find(name, &s, &len, &type) || type != JSMN_PRIMITIVE) return false;
|
||||
if (len == 4 && strncmp(s, "true", 4) == 0) {
|
||||
*out = true;
|
||||
return true;
|
||||
}
|
||||
if (len == 5 && strncmp(s, "false", 5) == 0) {
|
||||
*out = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int tokens_used() const { return count_; }
|
||||
|
||||
private:
|
||||
char* buf_ = nullptr;
|
||||
jsmn_parser p_;
|
||||
jsmntok_t tokens_[kMaxTokens];
|
||||
int count_ = 0;
|
||||
};
|
||||
|
||||
} // namespace fgl::ayla::json
|
||||
Reference in New Issue
Block a user