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:
@@ -9,18 +9,23 @@ if(ESP_PLATFORM)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS
|
SRCS
|
||||||
|
"src/ayla/crypto.cpp"
|
||||||
|
"src/ayla/envelope.cpp"
|
||||||
|
"src/ayla/httpc.cpp"
|
||||||
"src/ayla/httpd.cpp"
|
"src/ayla/httpd.cpp"
|
||||||
"src/ayla/log.cpp"
|
"src/ayla/log.cpp"
|
||||||
"src/ayla/platform/esp-idf/platform.cpp"
|
"src/ayla/platform/esp-idf/platform.cpp"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"include"
|
"include"
|
||||||
PRIV_INCLUDE_DIRS
|
PRIV_INCLUDE_DIRS
|
||||||
|
"."
|
||||||
"src"
|
"src"
|
||||||
PRIV_REQUIRES
|
PRIV_REQUIRES
|
||||||
lwip
|
lwip
|
||||||
esp_timer
|
esp_timer
|
||||||
esp_hw_support
|
esp_hw_support
|
||||||
pthread
|
pthread
|
||||||
|
mbedtls
|
||||||
)
|
)
|
||||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||||
-Wall -Wextra -Werror
|
-Wall -Wextra -Werror
|
||||||
@@ -34,16 +39,57 @@ else()
|
|||||||
project(fgl-aircon VERSION 0.1.0 LANGUAGES CXX)
|
project(fgl-aircon VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
option(FGL_BUILD_TESTS "Build tests" ON)
|
option(FGL_BUILD_TESTS "Build tests" ON)
|
||||||
|
option(FGL_BUNDLED_MBEDTLS "Fetch and build Mbed TLS instead of system" OFF)
|
||||||
|
|
||||||
|
# ---- mbedtls ----
|
||||||
|
if(NOT FGL_BUNDLED_MBEDTLS)
|
||||||
|
find_path(MBEDTLS_INCLUDE_DIR mbedtls/aes.h
|
||||||
|
HINTS /usr/include/mbedtls3 /usr/include
|
||||||
|
)
|
||||||
|
find_library(MBEDTLS_CRYPTO_LIB
|
||||||
|
NAMES mbedcrypto-3 mbedcrypto
|
||||||
|
)
|
||||||
|
if(NOT MBEDTLS_INCLUDE_DIR OR NOT MBEDTLS_CRYPTO_LIB)
|
||||||
|
message(STATUS "System Mbed TLS not found - falling back to bundled")
|
||||||
|
set(FGL_BUNDLED_MBEDTLS ON)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(FGL_BUNDLED_MBEDTLS)
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(mbedtls
|
||||||
|
GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git
|
||||||
|
GIT_TAG v3.6.2
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
|
||||||
|
set(USE_SHARED_MBEDTLS_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
|
set(USE_STATIC_MBEDTLS_LIBRARY ON CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(mbedtls)
|
||||||
|
set(FGL_MBEDTLS_TARGET mbedcrypto STATIC)
|
||||||
|
set(FGL_MBEDTLS_INCLUDE_DIR "")
|
||||||
|
else()
|
||||||
|
set(FGL_MBEDTLS_TARGET "${MBEDTLS_CRYPTO_LIB}")
|
||||||
|
set(FGL_MBEDTLS_INCLUDE_DIR "${MBEDTLS_INCLUDE_DIR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_library(fgl-aircon STATIC
|
add_library(fgl-aircon STATIC
|
||||||
|
src/ayla/crypto.cpp
|
||||||
|
src/ayla/envelope.cpp
|
||||||
|
src/ayla/httpc.cpp
|
||||||
src/ayla/httpd.cpp
|
src/ayla/httpd.cpp
|
||||||
src/ayla/log.cpp
|
src/ayla/log.cpp
|
||||||
src/ayla/platform/posix/platform.cpp
|
src/ayla/platform/posix/platform.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(fgl-aircon
|
target_include_directories(fgl-aircon
|
||||||
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include"
|
PUBLIC
|
||||||
PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src"
|
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
"${FGL_MBEDTLS_INCLUDE_DIR}"
|
||||||
|
PRIVATE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||||
)
|
)
|
||||||
|
target_link_libraries(fgl-aircon PUBLIC ${FGL_MBEDTLS_TARGET})
|
||||||
target_compile_features(fgl-aircon PUBLIC cxx_std_20)
|
target_compile_features(fgl-aircon PUBLIC cxx_std_20)
|
||||||
target_compile_options(fgl-aircon PRIVATE
|
target_compile_options(fgl-aircon PRIVATE
|
||||||
-Wall -Wextra -Werror
|
-Wall -Wextra -Werror
|
||||||
|
|||||||
@@ -10,4 +10,21 @@ cmake -S . -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release
|
|||||||
|
|
||||||
cmake --build "$BUILD_DIR"
|
cmake --build "$BUILD_DIR"
|
||||||
ctest --test-dir "$BUILD_DIR" --output-on-failure
|
ctest --test-dir "$BUILD_DIR" --output-on-failure
|
||||||
|
|
||||||
|
# Санитайзеры (gcc): ловим OOB/UB в ядре и тестах.
|
||||||
|
SAN_DIR="${SAN_DIR:-build-san}"
|
||||||
|
cmake -S . -B "$SAN_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
|
||||||
|
cmake --build "$SAN_DIR"
|
||||||
|
ctest --test-dir "$SAN_DIR" --output-on-failure
|
||||||
|
|
||||||
|
# clang (если доступен): отдельный компилятор = отдельные диагностики.
|
||||||
|
if command -v clang++ >/dev/null 2>&1; then
|
||||||
|
CLANG_DIR="${CLANG_DIR:-build-clang}"
|
||||||
|
cmake -S . -B "$CLANG_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_CXX_COMPILER=clang++
|
||||||
|
cmake --build "$CLANG_DIR"
|
||||||
|
ctest --test-dir "$CLANG_DIR" --output-on-failure
|
||||||
|
else
|
||||||
|
echo "clang++ не найден — сборка пропущена"
|
||||||
|
fi
|
||||||
echo "CI OK"
|
echo "CI OK"
|
||||||
|
|||||||
86
scripts/gen_kdf_vectors.py
Normal file
86
scripts/gen_kdf_vectors.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Регенерация tests/ayla/kdf_vectors.hpp. Эталон — APK AylaEncryption
|
||||||
|
# (совпадает с tools/probe_reference.py). Зависимость: pycryptodome.
|
||||||
|
# Запуск: python scripts/gen_kdf_vectors.py > tests/ayla/kdf_vectors.hpp
|
||||||
|
import base64, hmac, json, random, string
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
random.seed(20260922)
|
||||||
|
|
||||||
|
def rand_token(n=16, alpha=string.ascii_letters + string.digits):
|
||||||
|
return "".join(random.choice(alpha) for _ in range(n))
|
||||||
|
|
||||||
|
def cstr(s):
|
||||||
|
return json.dumps(s)
|
||||||
|
|
||||||
|
vectors = []
|
||||||
|
for i in range(4):
|
||||||
|
lanip_key = base64.b64encode(random.randbytes(15 + i)).decode()
|
||||||
|
rnd1 = rand_token(16, string.ascii_letters + string.digits + "+/")
|
||||||
|
rnd2 = rand_token(16)
|
||||||
|
t1 = random.randrange(10**12, 10**15)
|
||||||
|
t2 = random.randrange(10**12, 10**15)
|
||||||
|
k = lanip_key.encode(); b1, b2 = rnd1.encode(), rnd2.encode()
|
||||||
|
s1, s2 = str(t1).encode(), str(t2).encode()
|
||||||
|
|
||||||
|
def m(msg, suf):
|
||||||
|
msg = msg + bytes([suf])
|
||||||
|
return hmac.digest(k, hmac.digest(k, msg, "sha256") + msg, "sha256")
|
||||||
|
|
||||||
|
A, D = b1 + b2 + s1 + s2, b2 + b1 + s2 + s1
|
||||||
|
app_sign, dev_sign = m(A, 0x30), m(D, 0x30)
|
||||||
|
e = AES.new(m(A, 0x31), AES.MODE_CBC, m(A, 0x32)[:16])
|
||||||
|
msgs = [
|
||||||
|
'{"seq_no":0,"data":{}}',
|
||||||
|
'{"seq_no":1,"data":{"cmds":[{"cmd":{"method":"GET","resource":"property.json?name=fan_speed","uri":"/local_lan/property/datapoint.json","data":"","cmd_id":3}}]}}',
|
||||||
|
'{"seq_no":2,"data":{"properties":[{"property":{"base_type":"integer","name":"fan_speed","value":3,"id":"ab12cd34"}}]}}',
|
||||||
|
]
|
||||||
|
encs = []
|
||||||
|
for mtext in msgs:
|
||||||
|
raw = mtext.encode()
|
||||||
|
n = ((len(raw) + 1 + 15) // 16) * 16 # Java-паддинг (>=1 NUL)
|
||||||
|
encs.append(base64.b64encode(e.encrypt(raw.ljust(n, b"\x00"))).decode())
|
||||||
|
legacy_msg = '{"seq_no":3,"data":{"pad":"XX"}}' # 32 байта, без NUL
|
||||||
|
assert len(legacy_msg) % 16 == 0 and legacy_msg[-1] != "\x00"
|
||||||
|
encs.append(base64.b64encode(e.encrypt(legacy_msg.encode())).decode())
|
||||||
|
msgs.append(legacy_msg)
|
||||||
|
|
||||||
|
vectors.append(dict(
|
||||||
|
lanip_key=lanip_key, random_1=rnd1, random_2=rnd2,
|
||||||
|
time_1=t1, time_2=t2, plaintexts=msgs, enc_base64=encs,
|
||||||
|
app_sign_msg0=base64.b64encode(hmac.digest(app_sign, msgs[0].encode(), "sha256")).decode(),
|
||||||
|
dev_sign_msg1=base64.b64encode(hmac.digest(dev_sign, msgs[1].encode(), "sha256")).decode()))
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append("// СГЕНЕРИРОВАНО scripts/gen_kdf_vectors.py (эталон = APK AylaEncryption).")
|
||||||
|
lines.append("// Не редактировать руками; регенерация:")
|
||||||
|
lines.append("// python scripts/gen_kdf_vectors.py > tests/ayla/kdf_vectors.hpp")
|
||||||
|
lines.append("#pragma once")
|
||||||
|
lines.append("#include <cstddef>")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("namespace fgl::ayla::test {")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("struct KdfVector {")
|
||||||
|
lines.append(" const char* lanip_key;")
|
||||||
|
lines.append(" const char* random_1;")
|
||||||
|
lines.append(" const char* random_2;")
|
||||||
|
lines.append(" long long time_1;")
|
||||||
|
lines.append(" long long time_2;")
|
||||||
|
lines.append(" const char* plaintexts[4];")
|
||||||
|
lines.append(" const char* enc_base64[4]; // 0..2: Java-паддинг; 3: legacy без NUL")
|
||||||
|
lines.append(" const char* app_sign_msg0_b64;")
|
||||||
|
lines.append(" const char* dev_sign_msg1_b64;")
|
||||||
|
lines.append("};")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("inline constexpr KdfVector kKdfVectors[] = {")
|
||||||
|
for v in vectors:
|
||||||
|
pt = ", ".join(cstr(m) for m in v["plaintexts"])
|
||||||
|
en = ", ".join(cstr(e) for e in v["enc_base64"])
|
||||||
|
lines.append(f" {{{cstr(v['lanip_key'])}, {cstr(v['random_1'])}, {cstr(v['random_2'])}, "
|
||||||
|
f"{v['time_1']}LL, {v['time_2']}LL, {{{pt}}}, {{{en}}}, "
|
||||||
|
f"{cstr(v['app_sign_msg0'])}, {cstr(v['dev_sign_msg1'])}}},")
|
||||||
|
lines.append("};")
|
||||||
|
lines.append("inline constexpr size_t kKdfVectorCount = sizeof(kKdfVectors) / sizeof(kKdfVectors[0]);")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("} // namespace fgl::ayla::test")
|
||||||
|
print("\n".join(lines))
|
||||||
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
|
||||||
@@ -22,3 +22,7 @@ endfunction()
|
|||||||
|
|
||||||
fgl_add_test(ayla_platform ayla/test_platform.cpp)
|
fgl_add_test(ayla_platform ayla/test_platform.cpp)
|
||||||
fgl_add_test(ayla_httpd ayla/test_httpd.cpp)
|
fgl_add_test(ayla_httpd ayla/test_httpd.cpp)
|
||||||
|
fgl_add_test(ayla_crypto ayla/test_crypto.cpp)
|
||||||
|
fgl_add_test(ayla_envelope ayla/test_envelope.cpp)
|
||||||
|
fgl_add_test(ayla_json ayla/test_json.cpp)
|
||||||
|
fgl_add_test(ayla_httpc ayla/test_httpc.cpp)
|
||||||
|
|||||||
29
tests/ayla/kdf_vectors.hpp
Normal file
29
tests/ayla/kdf_vectors.hpp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// СГЕНЕРИРОВАНО scripts/gen_kdf_vectors.py (эталон = APK AylaEncryption).
|
||||||
|
// Не редактировать руками; регенерация:
|
||||||
|
// python scripts/gen_kdf_vectors.py > tests/ayla/kdf_vectors.hpp
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace fgl::ayla::test {
|
||||||
|
|
||||||
|
struct KdfVector {
|
||||||
|
const char* lanip_key;
|
||||||
|
const char* random_1;
|
||||||
|
const char* random_2;
|
||||||
|
long long time_1;
|
||||||
|
long long time_2;
|
||||||
|
const char* plaintexts[4];
|
||||||
|
const char* enc_base64[4]; // 0..2: Java-паддинг; 3: legacy без NUL
|
||||||
|
const char* app_sign_msg0_b64;
|
||||||
|
const char* dev_sign_msg1_b64;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr KdfVector kKdfVectors[] = {
|
||||||
|
{"nRB152rvLrgpUyysvyS6", "Unajei61hBtnAXvL", "8q8tgzOx60ehhAtF", 274989731965726LL, 847349374141033LL, {"{\"seq_no\":0,\"data\":{}}", "{\"seq_no\":1,\"data\":{\"cmds\":[{\"cmd\":{\"method\":\"GET\",\"resource\":\"property.json?name=fan_speed\",\"uri\":\"/local_lan/property/datapoint.json\",\"data\":\"\",\"cmd_id\":3}}]}}", "{\"seq_no\":2,\"data\":{\"properties\":[{\"property\":{\"base_type\":\"integer\",\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}}", "{\"seq_no\":3,\"data\":{\"pad\":\"XX\"}}"}, {"hk/Iha4XexydQeV7cQ9K4rtNo4hMRPKaCBIINcuU0yo=", "FZnpg9JTsS+56Eh+80V9RlFn7iCVWlGU3xq9Wm8s9GfqLnq49I/V386EqnAMTAT3hZvq46WDGRnymwmlJF+gtOCoTlzvkD/2ozKtXeFMLtidTmqzDddOHP+uPNEXZcuVsfpuN2dLPctqTmM827lrbpwctKvuNTmzd1L6gpdgSQ4e9V4sd+9OA/fI1Eu4E1maJ+Tm4QdpKADrj50oh2qRsOqdjhAaWOjT5lgauLISqno=", "SZa7swUooRnYenDVlVocX8IoLIK9QZm1wybIEVE44g1Q63SDQ4ulpbOvgvTkmTuN4krAGJA6qKt7pm2HYhyIDhMz/fmNgee+FrK/C3Fdy1bFPl8SESR8UL9R7eN2TSxrtK51+uyFvm6DMvft6zmjZnfdyRkjfCN0gI8bGLXN4yU=", "l1DRivEQAazsO+2uWd0qdc00P6qjw5nIK7skXfYn9Bo="}, "IODB6lZl2ClQ/CHa3e0GMbG466jlZ59up4XgVP/00wY=", "sKBzGqhFIPb8DL8KbK0Np2Zag9C80JS8oHLiDXTu2DE="},
|
||||||
|
{"e3YfUELFmkeD4BWjSSnYxA==", "cxtcwrhtsyAcHm8S", "wfdv8OHvq0h03YnY", 761048435275066LL, 11876266770736LL, {"{\"seq_no\":0,\"data\":{}}", "{\"seq_no\":1,\"data\":{\"cmds\":[{\"cmd\":{\"method\":\"GET\",\"resource\":\"property.json?name=fan_speed\",\"uri\":\"/local_lan/property/datapoint.json\",\"data\":\"\",\"cmd_id\":3}}]}}", "{\"seq_no\":2,\"data\":{\"properties\":[{\"property\":{\"base_type\":\"integer\",\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}}", "{\"seq_no\":3,\"data\":{\"pad\":\"XX\"}}"}, {"AhUcfcYews8/ip360hj3RbKoCt8hwgEA3791gG1IQSM=", "S5PS+KqJ9vIu65Fd+hcnvllI5R34enCE7BT6ukZjdRoVTIEOhIQCzusUcu0vlm2ZLqJwSRBYPFlQKkGh5VDXyJZLQLj+tFZDCf2n4d6wPcFBHsH3VEZy8yl+odPXc6ogAEwtyJhBZ9fURrNgQiveWNG42zTQ13dElqU2RD1bmfilJe0FF84cdre8bHJm7HyWAg5XdQ4qOVzZhH+EWE1XfWeegPDoIsi5ch8QJOiK7KY=", "qbEQTeBvTYYDfFUVwf5oK8slPxW3vUk9jbN6z3sAoQ17s1OiaqHJQrYFr0dDQIvuQgqAcj8+lRtc97papq4Fgpz/QAKCDIZujClJR9KWHBUrTqE8j89QjwjaQZ8ze+hzqIvebwt2C7vM/YivEJz9aSuxi1iLcCcEhnR+VlHoeBw=", "+aAezE8EmyfdRzJnIrsFC3NYxsZRswDkhPNc7qypCZ8="}, "4sW8EmpwLVz/pPN7MZ0Art+uz7uZbZI7lDBuI0Lzjtw=", "h8bqQ1umSr3pTReBno61eTSsK5TCA+2xGAP+uzzx6FE="},
|
||||||
|
{"deQG9G4zw74OZBzgL31jxCI=", "AlUvHAXene8hTnpq", "rSvwucWBRIQgcG0i", 299350142996752LL, 501892609552265LL, {"{\"seq_no\":0,\"data\":{}}", "{\"seq_no\":1,\"data\":{\"cmds\":[{\"cmd\":{\"method\":\"GET\",\"resource\":\"property.json?name=fan_speed\",\"uri\":\"/local_lan/property/datapoint.json\",\"data\":\"\",\"cmd_id\":3}}]}}", "{\"seq_no\":2,\"data\":{\"properties\":[{\"property\":{\"base_type\":\"integer\",\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}}", "{\"seq_no\":3,\"data\":{\"pad\":\"XX\"}}"}, {"QSdv/i/vwK6JIni4wmMstgf0PiRc/fAPKkszlQrHeV8=", "q5t4RkNU8bu67S15mi27dT28UwWCcVQLh/PgBshhhQVljm950hqGsKPJMSNy5Bul41Ut6bd6sPGdUuFl82FPecIkloDTAx0e0htDjaESEcdUTao4Uxn4yLILG8PKHGog2bHHfGnG7AIk9XFNKnxXCIRtJetgu57gWTlFSxf4UIG+8zBZmb1Zn0R6csUkrFLCAzjfQzx430Gagcw1+lyfgL4EoZgx6SJhWQQ9mM/uyxE=", "eY/Y17nxXaSGSNKjq0XEDYw+Ks55Sze7bZw5yKGnwI6iS8uODSBila54I630XOp/mlAvdVE09StUi7xPAkrCG0gDJ7DsMLFONZR3HDWf7f63iYpdOOQlXRnH8SnQ7pmu4mqRp2JE/NqdWCWT6AMUzXOWDUbxfl3OShLrZf29uDU=", "8RWdrVA+NRbqkT4aXTgm0djTjfyz5rC2fGoh+RfRqow="}, "Z5Ah10Y/qJngN2DUJhhkEytTZsFCOiV2TaeTh61X5pc=", "TcQdYNIoQXEeDZd9ZJWAn9tayz8Zc6/ZR2MtnvQ6/xc="},
|
||||||
|
{"v8CQFGwiB+/z9UUEGdu7rZ0a", "27XqoPSfZ4553E3K", "rbwC6T3cXdodlhIS", 257649300961690LL, 851173235636538LL, {"{\"seq_no\":0,\"data\":{}}", "{\"seq_no\":1,\"data\":{\"cmds\":[{\"cmd\":{\"method\":\"GET\",\"resource\":\"property.json?name=fan_speed\",\"uri\":\"/local_lan/property/datapoint.json\",\"data\":\"\",\"cmd_id\":3}}]}}", "{\"seq_no\":2,\"data\":{\"properties\":[{\"property\":{\"base_type\":\"integer\",\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}}", "{\"seq_no\":3,\"data\":{\"pad\":\"XX\"}}"}, {"1c7PN6ycazQ1MjiwUmeMQLpp3LuYx3HTuxnS2kFgAw4=", "73cUheq6HOJlHzhuRMf1M5owaQFEw0wXvzsRB1nvGOPpNyLJKilrEVYkyeDH7MzqXRf9thxswnSLOfgK20NHyUozTe41BE/qz7/oVvFquPOPq6bjUWADzugjjOyDAezXyCb0Z+QvgnqruYjQHJJMxe3U2dcypLUXiScDui694bugFVhnIoQTRFyW0LxBff/ZCp3DBrAZUD178MseY9/h/kEx+Rc8qJzLUjvBHuqGqK8=", "7LRiFwjfdrHgksMvV6HwQM/kK8Yeiwgl+QYljJhboya3HzMa1MmGNPxfy0X8nck77ShFeSSpethG1Gk9/jfhFIeIqLoTNSGi/STwfE0S8RKjKx+wIaBSO2xyTbCx8s9g/+Jvn+wORpUdT45wz5sX1c/LjVJkItcjE/gtGPrDICY=", "hyr7N37WMcA/1SOjPceqwnjdCAhn1HWluLPw39EgiL8="}, "fWNCY5DVgVWIKQZoepEUxYE7yaKfxVsR7Bv3zQIkpRo=", "h/9MHTMSW84EFo96BQW2X73cUi0rILDWQoSIqTlEGew="},
|
||||||
|
};
|
||||||
|
inline constexpr size_t kKdfVectorCount = sizeof(kKdfVectors) / sizeof(kKdfVectors[0]);
|
||||||
|
|
||||||
|
} // namespace fgl::ayla::test
|
||||||
171
tests/ayla/test_crypto.cpp
Normal file
171
tests/ayla/test_crypto.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
// Тесты криптографии Ayla LAN: KDF по векторам (эталон = APK), CBC-цепочка,
|
||||||
|
// подписи, оба варианта паддинга.
|
||||||
|
#include "kdf_vectors.hpp"
|
||||||
|
#include "doctest/doctest.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ayla/crypto.hpp"
|
||||||
|
|
||||||
|
using fgl::ayla::test::kKdfVectorCount;
|
||||||
|
using fgl::ayla::test::kKdfVectors;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// base64 для сравнения в тестах.
|
||||||
|
std::string b64(const uint8_t* data, size_t len) {
|
||||||
|
static const char kTable[] =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 0; i < len; i += 3) {
|
||||||
|
uint32_t v = data[i] << 16;
|
||||||
|
if (i + 1 < len) v |= data[i + 1] << 8;
|
||||||
|
if (i + 2 < len) v |= data[i + 2];
|
||||||
|
out += kTable[(v >> 18) & 63];
|
||||||
|
out += kTable[(v >> 12) & 63];
|
||||||
|
out += (i + 1 < len) ? kTable[(v >> 6) & 63] : '=';
|
||||||
|
out += (i + 2 < len) ? kTable[v & 63] : '=';
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("KDF+envelope: векторы APK (шифрование app-направления, цепочка)") {
|
||||||
|
for (size_t v = 0; v < kKdfVectorCount; v++) {
|
||||||
|
CAPTURE(v);
|
||||||
|
const auto& vec = kKdfVectors[v];
|
||||||
|
fgl::ayla::SessionCrypto sc;
|
||||||
|
REQUIRE(sc.init(vec.lanip_key, vec.random_1, vec.random_2, vec.time_1,
|
||||||
|
vec.time_2));
|
||||||
|
|
||||||
|
// Отправитель всегда использует Java-паддинг (>=1 NUL) — сверяем 0..2.
|
||||||
|
// (Вектор 3 — legacy-приём без NUL; проверяется в decrypt-тесте.)
|
||||||
|
for (int m = 0; m < 3; m++) {
|
||||||
|
CAPTURE(m);
|
||||||
|
uint8_t ct[1024];
|
||||||
|
long ct_len = sc.app.encrypt(ct, sizeof(ct),
|
||||||
|
reinterpret_cast<const uint8_t*>(vec.plaintexts[m]),
|
||||||
|
strlen(vec.plaintexts[m]));
|
||||||
|
REQUIRE(ct_len > 0);
|
||||||
|
CHECK(b64(ct, static_cast<size_t>(ct_len)) == std::string(vec.enc_base64[m]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписи направлений.
|
||||||
|
uint8_t mac[32];
|
||||||
|
sc.app.sign(mac, reinterpret_cast<const uint8_t*>(vec.plaintexts[0]),
|
||||||
|
strlen(vec.plaintexts[0]));
|
||||||
|
CHECK(b64(mac, sizeof(mac)) == std::string(vec.app_sign_msg0_b64));
|
||||||
|
sc.dev.sign(mac, reinterpret_cast<const uint8_t*>(vec.plaintexts[1]),
|
||||||
|
strlen(vec.plaintexts[1]));
|
||||||
|
CHECK(b64(mac, sizeof(mac)) == std::string(vec.dev_sign_msg1_b64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Расшифровка векторов: независимый приёмник, цепочка, оба паддинга") {
|
||||||
|
for (size_t v = 0; v < kKdfVectorCount; v++) {
|
||||||
|
const auto& vec = kKdfVectors[v];
|
||||||
|
// «Приёмник» — свежая сессия: цепочка стартует с iv_seed.
|
||||||
|
fgl::ayla::SessionCrypto rx;
|
||||||
|
REQUIRE(rx.init(vec.lanip_key, vec.random_1, vec.random_2, vec.time_1,
|
||||||
|
vec.time_2));
|
||||||
|
|
||||||
|
// base64-декодирование вектора (таблица для теста).
|
||||||
|
auto unb64 = [](const char* in, uint8_t* out) -> size_t {
|
||||||
|
auto val = [](char c) -> int {
|
||||||
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||||
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||||
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||||
|
if (c == '+') return 62;
|
||||||
|
if (c == '/') return 63;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
size_t len = strlen(in), out_len = 0;
|
||||||
|
uint32_t acc = 0;
|
||||||
|
int bits = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (in[i] == '=') break;
|
||||||
|
int d = val(in[i]);
|
||||||
|
if (d < 0) return 0;
|
||||||
|
acc = (acc << 6) | static_cast<uint32_t>(d);
|
||||||
|
bits += 6;
|
||||||
|
if (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
out[out_len++] = static_cast<uint8_t>((acc >> bits) & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out_len;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int m = 0; m < 4; m++) {
|
||||||
|
uint8_t ct[1024];
|
||||||
|
size_t ct_len = unb64(vec.enc_base64[m], ct);
|
||||||
|
REQUIRE(ct_len > 0);
|
||||||
|
REQUIRE(ct_len % 16 == 0);
|
||||||
|
uint8_t pt[1024] = {};
|
||||||
|
long pt_len = rx.app.decrypt(pt, sizeof(pt), ct, ct_len);
|
||||||
|
REQUIRE(pt_len > 0);
|
||||||
|
// Открытый текст совпадает (NUL-паддинг снят; legacy-вариант — точно).
|
||||||
|
CHECK(pt_len == static_cast<long>(strlen(vec.plaintexts[m])));
|
||||||
|
CHECK(memcmp(pt, vec.plaintexts[m], pt_len) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("dev-направление: roundtrip цепочки encrypt→decrypt") {
|
||||||
|
const char* key = "UnitTestKey01234567890==";
|
||||||
|
const char* msgs[] = {
|
||||||
|
"{\"seq_no\":0,\"data\":{\"name\":\"a\",\"value\":1}}",
|
||||||
|
"{\"seq_no\":1,\"data\":{\"name\":\"b\",\"value\":2}}",
|
||||||
|
"{\"seq_no\":2,\"data\":{}}",
|
||||||
|
};
|
||||||
|
fgl::ayla::SessionCrypto tx, rx;
|
||||||
|
REQUIRE(tx.init(key, "aaaa1111aaaa1111", "bbbb2222bbbb2222", 111, 222));
|
||||||
|
REQUIRE(rx.init(key, "aaaa1111aaaa1111", "bbbb2222bbbb2222", 111, 222));
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
uint8_t ct[512];
|
||||||
|
long ct_len = tx.dev.encrypt(ct, sizeof(ct),
|
||||||
|
reinterpret_cast<const uint8_t*>(msgs[i]),
|
||||||
|
strlen(msgs[i]));
|
||||||
|
REQUIRE(ct_len > 0);
|
||||||
|
uint8_t pt[512];
|
||||||
|
long pt_len = rx.dev.decrypt(pt, sizeof(pt), ct, static_cast<size_t>(ct_len));
|
||||||
|
REQUIRE(pt_len == static_cast<long>(strlen(msgs[i])));
|
||||||
|
CHECK(memcmp(pt, msgs[i], pt_len) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("encrypt/decrypt: граничные случаи") {
|
||||||
|
fgl::ayla::SessionCrypto sc;
|
||||||
|
REQUIRE(sc.init("k", "a", "b", 1, 2));
|
||||||
|
|
||||||
|
uint8_t out[2048];
|
||||||
|
// out_cap меньше padded — ошибка.
|
||||||
|
CHECK(sc.app.encrypt(out, 1, reinterpret_cast<const uint8_t*>("xx"), 2) == -1);
|
||||||
|
// Некратная длина шифротекста — ошибка расшифровки.
|
||||||
|
uint8_t junk[17] = {};
|
||||||
|
CHECK(sc.app.decrypt(out, sizeof(out), junk, 17) == -1);
|
||||||
|
// Нулевая длина — ошибка.
|
||||||
|
CHECK(sc.app.decrypt(out, sizeof(out), junk, 0) == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("DirectionCrypto: смешение encrypt/decrypt одного направления запрещено") {
|
||||||
|
fgl::ayla::SessionCrypto sc;
|
||||||
|
REQUIRE(sc.init("k", "a", "b", 1, 2));
|
||||||
|
uint8_t out[64];
|
||||||
|
// Фиксируем режим шифрованием.
|
||||||
|
REQUIRE(sc.app.encrypt(out, sizeof(out),
|
||||||
|
reinterpret_cast<const uint8_t*>("1234567890123456"), 16) == 32);
|
||||||
|
// Расшифровка тем же направлением — отказ (misuse-защита одной цепочки).
|
||||||
|
uint8_t pt[64];
|
||||||
|
CHECK(sc.app.decrypt(pt, sizeof(pt), out, 16) == -1);
|
||||||
|
// Обратный порядок для dev-направления.
|
||||||
|
fgl::ayla::SessionCrypto sc2;
|
||||||
|
REQUIRE(sc2.init("k", "a", "b", 1, 2));
|
||||||
|
uint8_t junk[16] = {};
|
||||||
|
REQUIRE(sc2.dev.decrypt(pt, sizeof(pt), junk, 16) >= 0);
|
||||||
|
CHECK(sc2.dev.encrypt(out, sizeof(out),
|
||||||
|
reinterpret_cast<const uint8_t*>("1234567890123456"), 16) == -1);
|
||||||
|
}
|
||||||
178
tests/ayla/test_envelope.cpp
Normal file
178
tests/ayla/test_envelope.cpp
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// Тесты конверта {"enc","sign"}: roundtrip, цепочка, порча подписи.
|
||||||
|
#include "doctest/doctest.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ayla/crypto.hpp"
|
||||||
|
#include "ayla/envelope.hpp"
|
||||||
|
|
||||||
|
TEST_CASE("envelope: roundtrip и CBC-цепочка (3 сообщения)") {
|
||||||
|
const char* key = "UnitTestKey01234567890==";
|
||||||
|
fgl::ayla::SessionCrypto tx, rx;
|
||||||
|
REQUIRE(tx.init(key, "rnd1111rnd1111aa", "rnd2222rnd2222bb", 42, 43));
|
||||||
|
REQUIRE(rx.init(key, "rnd1111rnd1111aa", "rnd2222rnd2222bb", 42, 43));
|
||||||
|
|
||||||
|
const char* payloads[] = {
|
||||||
|
"{}",
|
||||||
|
"{\"cmds\":[{\"cmd\":{\"cmd_id\":7,\"method\":\"GET\","
|
||||||
|
"\"resource\":\"property.json?name=x\",\"data\":\"\","
|
||||||
|
"\"uri\":\"/local_lan/property/datapoint.json\"}}]}",
|
||||||
|
"{\"properties\":[{\"property\":{\"base_type\":\"integer\","
|
||||||
|
"\"name\":\"n\",\"value\":9,\"id\":\"zz\"}}]}",
|
||||||
|
};
|
||||||
|
char envelopes[3][fgl::ayla::kEnvelopeMaxB64];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
REQUIRE(fgl::ayla::envelope_pack(tx.app, i, payloads[i], envelopes[i],
|
||||||
|
sizeof(envelopes[i])));
|
||||||
|
}
|
||||||
|
// Разные шифротексты (цепочка движется).
|
||||||
|
CHECK(std::string(envelopes[0]) != std::string(envelopes[1]));
|
||||||
|
|
||||||
|
// Приёмник с той же цепочкой расшифровывает по порядку.
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// Разбор конверта на enc/sign (в тесте — вручную, простым поиском).
|
||||||
|
std::string env(envelopes[i]);
|
||||||
|
size_t e1 = env.find("\"enc\":\"") + 7;
|
||||||
|
size_t e2 = env.find('"', e1);
|
||||||
|
size_t s1 = env.find("\"sign\":\"") + 8;
|
||||||
|
size_t s2 = env.find('"', s1);
|
||||||
|
REQUIRE(e2 != std::string::npos);
|
||||||
|
REQUIRE(s2 != std::string::npos);
|
||||||
|
std::string enc = env.substr(e1, e2 - e1);
|
||||||
|
std::string sign = env.substr(s1, s2 - s1);
|
||||||
|
|
||||||
|
char json[fgl::ayla::kEnvelopeMaxPlain];
|
||||||
|
int64_t seq = -1;
|
||||||
|
REQUIRE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(), json,
|
||||||
|
sizeof(json), &seq));
|
||||||
|
CHECK(seq == i);
|
||||||
|
std::string expected = "{\"seq_no\":" + std::to_string(i) + ",\"data\":" +
|
||||||
|
payloads[i] + "}";
|
||||||
|
CHECK(std::string(json) == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("envelope: порча подписи отклоняется, цепочка продолжает движение") {
|
||||||
|
const char* key = "UnitTestKey01234567890==";
|
||||||
|
fgl::ayla::SessionCrypto tx, rx;
|
||||||
|
REQUIRE(tx.init(key, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", 1, 2));
|
||||||
|
REQUIRE(rx.init(key, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", 1, 2));
|
||||||
|
|
||||||
|
char e0[fgl::ayla::kEnvelopeMaxB64], e1[fgl::ayla::kEnvelopeMaxB64];
|
||||||
|
REQUIRE(fgl::ayla::envelope_pack(tx.app, 0, "{}", e0, sizeof(e0)));
|
||||||
|
REQUIRE(fgl::ayla::envelope_pack(tx.app, 1, "{\"a\":1}", e1, sizeof(e1)));
|
||||||
|
|
||||||
|
auto split = [](const char* env, std::string* enc, std::string* sign) {
|
||||||
|
std::string s(env);
|
||||||
|
size_t e1 = s.find("\"enc\":\"") + 7, e2 = s.find('"', e1);
|
||||||
|
size_t g1 = s.find("\"sign\":\"") + 8, g2 = s.find('"', g1);
|
||||||
|
*enc = s.substr(e1, e2 - e1);
|
||||||
|
*sign = s.substr(g1, g2 - g1);
|
||||||
|
};
|
||||||
|
std::string enc0, sign0, enc1, sign1;
|
||||||
|
split(e0, &enc0, &sign0);
|
||||||
|
split(e1, &enc1, &sign1);
|
||||||
|
|
||||||
|
// Портим подпись первого сообщения: распаковка обязана провалиться,
|
||||||
|
// но расшифровка (и движение цепочки) уже произошла.
|
||||||
|
std::string bad = sign0;
|
||||||
|
bad[bad.size() - 2] = (bad[bad.size() - 2] == 'A') ? 'B' : 'A';
|
||||||
|
char json[fgl::ayla::kEnvelopeMaxPlain];
|
||||||
|
int64_t seq = -1;
|
||||||
|
CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc0.c_str(), bad.c_str(),
|
||||||
|
json, sizeof(json), &seq));
|
||||||
|
// Второе сообщение — валидное, цепочка корректна.
|
||||||
|
REQUIRE(fgl::ayla::envelope_unpack(rx.app, enc1.c_str(), sign1.c_str(), json,
|
||||||
|
sizeof(json), &seq));
|
||||||
|
CHECK(seq == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("envelope: некорректный base64 и повреждённый шифротекст") {
|
||||||
|
const char* key = "UnitTestKey01234567890==";
|
||||||
|
fgl::ayla::SessionCrypto tx, rx;
|
||||||
|
REQUIRE(tx.init(key, "cccccccccccccccc", "dddddddddddddddd", 5, 6));
|
||||||
|
REQUIRE(rx.init(key, "cccccccccccccccc", "dddddddddddddddd", 5, 6));
|
||||||
|
|
||||||
|
char env[fgl::ayla::kEnvelopeMaxB64];
|
||||||
|
REQUIRE(fgl::ayla::envelope_pack(tx.app, 0, "{}", env, sizeof(env)));
|
||||||
|
std::string s(env);
|
||||||
|
size_t e1 = s.find("\"enc\":\"") + 7, e2 = s.find('"', e1);
|
||||||
|
size_t g1 = s.find("\"sign\":\"") + 8, g2 = s.find('"', g1);
|
||||||
|
std::string enc = s.substr(e1, e2 - e1);
|
||||||
|
std::string sign = s.substr(g1, g2 - g1);
|
||||||
|
|
||||||
|
char json[fgl::ayla::kEnvelopeMaxPlain];
|
||||||
|
int64_t seq = -1;
|
||||||
|
// Мусорный base64.
|
||||||
|
CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, "!!!not-base64!!!", sign.c_str(),
|
||||||
|
json, sizeof(json), &seq));
|
||||||
|
// Порча шифротекста: подпись не сойдётся (подводного камня нет — просто
|
||||||
|
// проверяем, что это отклоняется).
|
||||||
|
std::string broken = enc;
|
||||||
|
if (broken[10] == 'A') {
|
||||||
|
broken[10] = 'B';
|
||||||
|
} else {
|
||||||
|
broken[10] = 'A';
|
||||||
|
}
|
||||||
|
CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, broken.c_str(), sign.c_str(),
|
||||||
|
json, sizeof(json), &seq));
|
||||||
|
// Оригинал после двух провалов уже не расшифруется (цепочка ушла) —
|
||||||
|
// таково требование протокола; восстановление только re-key.
|
||||||
|
CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(),
|
||||||
|
json, sizeof(json), &seq));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// base64 для ручной сборки конвертов в тестах.
|
||||||
|
std::string tb64(const uint8_t* data, size_t len) {
|
||||||
|
static const char kTable[] =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 0; i < len; i += 3) {
|
||||||
|
uint32_t v = data[i] << 16;
|
||||||
|
if (i + 1 < len) v |= data[i + 1] << 8;
|
||||||
|
if (i + 2 < len) v |= data[i + 2];
|
||||||
|
out += kTable[(v >> 18) & 63];
|
||||||
|
out += kTable[(v >> 12) & 63];
|
||||||
|
out += (i + 1 < len) ? kTable[(v >> 6) & 63] : '=';
|
||||||
|
out += (i + 2 < len) ? kTable[v & 63] : '=';
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("envelope: extract_seq_no устойчив к обрыву после escape (OOB-регрессия)") {
|
||||||
|
// Аутентифицированный plaintext с незакрытой строкой, заканчивающейся '\'
|
||||||
|
// (раньше — чтение за границей буфера, поймано ASan в ревью круга 1).
|
||||||
|
// Конверты собираем вручную: sign + encrypt по битому plaintext.
|
||||||
|
const char* key = "UnitTestKey01234567890==";
|
||||||
|
fgl::ayla::SessionCrypto tx, rx;
|
||||||
|
REQUIRE(tx.init(key, "eeeeeeeeeeeeeeee", "ffffffffffffffff", 7, 8));
|
||||||
|
REQUIRE(rx.init(key, "eeeeeeeeeeeeeeee", "ffffffffffffffff", 7, 8));
|
||||||
|
|
||||||
|
// seq_no отсутствует/после битой части — сканер обязан ПРОПУСКАТЬ
|
||||||
|
// повреждённое значение (именно на пропуске был OOB).
|
||||||
|
const char* cases[] = {
|
||||||
|
"{\"a\":\"x\\", // обрыв в строке-значении верхнего уровня
|
||||||
|
"{\"a\":{\"b\":\"x\\", // обрыв в строке внутри вложенного объекта
|
||||||
|
"{\"a\":[1,2,\"x\\", // обрыв в строке внутри массива
|
||||||
|
};
|
||||||
|
for (const char* plain : cases) {
|
||||||
|
CAPTURE(plain);
|
||||||
|
uint8_t mac[32];
|
||||||
|
tx.app.sign(mac, reinterpret_cast<const uint8_t*>(plain), strlen(plain));
|
||||||
|
uint8_t ct[fgl::ayla::kEnvelopeMaxPlain + 16];
|
||||||
|
long ct_len = tx.app.encrypt(ct, sizeof(ct),
|
||||||
|
reinterpret_cast<const uint8_t*>(plain),
|
||||||
|
strlen(plain));
|
||||||
|
REQUIRE(ct_len > 0);
|
||||||
|
std::string enc = tb64(ct, static_cast<size_t>(ct_len));
|
||||||
|
std::string sign = tb64(mac, sizeof(mac));
|
||||||
|
char json[fgl::ayla::kEnvelopeMaxPlain];
|
||||||
|
int64_t seq = -1;
|
||||||
|
// unpack обязан вернуть false (битый JSON) и не выйти за границы.
|
||||||
|
CHECK_FALSE(fgl::ayla::envelope_unpack(rx.app, enc.c_str(), sign.c_str(),
|
||||||
|
json, sizeof(json), &seq));
|
||||||
|
}
|
||||||
|
}
|
||||||
106
tests/ayla/test_httpc.cpp
Normal file
106
tests/ayla/test_httpc.cpp
Normal 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();
|
||||||
|
}
|
||||||
@@ -209,7 +209,9 @@ TEST_CASE("httpd: бесконечный стрим заголовков зав
|
|||||||
const char* filler = "X-Pad: 0123456789012345678901234567890123456789\r\n";
|
const char* filler = "X-Pad: 0123456789012345678901234567890123456789\r\n";
|
||||||
size_t flen = strlen(filler);
|
size_t flen = strlen(filler);
|
||||||
for (int i = 0; i < 80; i++) { // ~4КБ — больше лимита
|
for (int i = 0; i < 80; i++) { // ~4КБ — больше лимита
|
||||||
REQUIRE(fgl::plat::tcp_send(fd, filler, flen) == static_cast<long>(flen));
|
if (fgl::plat::tcp_send(fd, filler, flen) != static_cast<long>(flen)) {
|
||||||
|
break; // сервер уже закрыл соединение (лимит превышен) — RST допустим
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Сервер должен перестать читать и закрыть соединение: recv завершается
|
// Сервер должен перестать читать и закрыть соединение: recv завершается
|
||||||
// (EOF или RST после close с непрочитанными данными), а не висит вечно.
|
// (EOF или RST после close с непрочитанными данными), а не висит вечно.
|
||||||
|
|||||||
156
tests/ayla/test_json.cpp
Normal file
156
tests/ayla/test_json.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Тесты JSON: writer (фиксированный буфер, вложенность, переполнение)
|
||||||
|
// и парсер (jsmn) — форматы протокола.
|
||||||
|
#include "doctest/doctest.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ayla/json.hpp"
|
||||||
|
|
||||||
|
using fgl::ayla::json::Doc;
|
||||||
|
using fgl::ayla::json::Writer;
|
||||||
|
|
||||||
|
TEST_CASE("json writer: формы протокола") {
|
||||||
|
// datapoint-команда (properties)
|
||||||
|
char buf[512];
|
||||||
|
Writer w(buf, sizeof(buf));
|
||||||
|
w.begin_object(); // {
|
||||||
|
w.key("properties");
|
||||||
|
w.begin_array();
|
||||||
|
w.begin_object();
|
||||||
|
w.key("property");
|
||||||
|
w.begin_object();
|
||||||
|
w.key("base_type"); w.string("integer");
|
||||||
|
w.key("name"); w.string("fan_speed");
|
||||||
|
w.key("value"); w.integer(3);
|
||||||
|
w.key("id"); w.string("ab12cd34");
|
||||||
|
w.end_object();
|
||||||
|
w.end_object();
|
||||||
|
w.end_array();
|
||||||
|
w.end_object();
|
||||||
|
REQUIRE(w.ok());
|
||||||
|
CHECK(std::string(buf) ==
|
||||||
|
"{\"properties\":[{\"property\":{\"base_type\":\"integer\","
|
||||||
|
"\"name\":\"fan_speed\",\"value\":3,\"id\":\"ab12cd34\"}}]}");
|
||||||
|
|
||||||
|
// GET-команда (cmds)
|
||||||
|
Writer w2(buf, sizeof(buf));
|
||||||
|
w2.begin_object();
|
||||||
|
w2.key("cmds");
|
||||||
|
w2.begin_array();
|
||||||
|
w2.begin_object();
|
||||||
|
w2.key("cmd");
|
||||||
|
w2.begin_object();
|
||||||
|
w2.key("cmd_id"); w2.integer(5);
|
||||||
|
w2.key("method"); w2.string("GET");
|
||||||
|
w2.key("resource"); w2.string("property.json?name=op");
|
||||||
|
w2.key("data"); w2.string("");
|
||||||
|
w2.key("uri"); w2.string("/local_lan/property/datapoint.json");
|
||||||
|
w2.end_object();
|
||||||
|
w2.end_object();
|
||||||
|
w2.end_array();
|
||||||
|
w2.end_object();
|
||||||
|
REQUIRE(w2.ok());
|
||||||
|
CHECK(std::string(buf) ==
|
||||||
|
"{\"cmds\":[{\"cmd\":{\"cmd_id\":5,\"method\":\"GET\","
|
||||||
|
"\"resource\":\"property.json?name=op\",\"data\":\"\","
|
||||||
|
"\"uri\":\"/local_lan/property/datapoint.json\"}}]}");
|
||||||
|
|
||||||
|
// key_exchange ответ + массив целых + bool/null
|
||||||
|
Writer w3(buf, sizeof(buf));
|
||||||
|
w3.begin_object();
|
||||||
|
w3.key("random_2"); w3.string("abcd1234");
|
||||||
|
w3.key("time_2"); w3.integer(-123);
|
||||||
|
w3.key("flags");
|
||||||
|
w3.begin_array();
|
||||||
|
w3.integer(1); w3.integer(2); w3.integer(3);
|
||||||
|
w3.end_array();
|
||||||
|
w3.key("ok"); w3.boolean(true);
|
||||||
|
w3.key("nothing"); w3.literal("null");
|
||||||
|
w3.end_object();
|
||||||
|
REQUIRE(w3.ok());
|
||||||
|
CHECK(std::string(buf) ==
|
||||||
|
"{\"random_2\":\"abcd1234\",\"time_2\":-123,\"flags\":[1,2,3],"
|
||||||
|
"\"ok\":true,\"nothing\":null}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("json writer: экранирование и переполнение") {
|
||||||
|
char buf[64];
|
||||||
|
Writer w(buf, sizeof(buf));
|
||||||
|
w.begin_object();
|
||||||
|
w.key("a\"b\\c"); // ключ с кавычкой и обратным слешем
|
||||||
|
w.string("x\ny\"z");
|
||||||
|
w.end_object();
|
||||||
|
REQUIRE(w.ok());
|
||||||
|
CHECK(std::string(buf) == "{\"a\\\"b\\\\c\":\"x\\ny\\\"z\"}");
|
||||||
|
|
||||||
|
// Переполнение буфера — ok() = false, не выходим за границы.
|
||||||
|
Writer w2(buf, sizeof(buf));
|
||||||
|
w2.begin_object();
|
||||||
|
w2.key("data");
|
||||||
|
w2.string("0123456789012345678901234567890123456789012345678901234567890123456789");
|
||||||
|
w.end_object();
|
||||||
|
CHECK_FALSE(w2.ok());
|
||||||
|
CHECK(strlen(buf) < sizeof(buf)); // NUL-терминировано всегда
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("json parser: форматы протокола") {
|
||||||
|
// datapoint push
|
||||||
|
char buf[128];
|
||||||
|
strcpy(buf, "{\"name\":\"fan_speed\",\"value\":3,\"metadata\":{}}");
|
||||||
|
Doc doc;
|
||||||
|
REQUIRE(doc.parse(buf));
|
||||||
|
char name[32];
|
||||||
|
REQUIRE(doc.get_string("name", name, sizeof(name)));
|
||||||
|
CHECK(std::string(name) == "fan_speed");
|
||||||
|
int64_t value = -1;
|
||||||
|
REQUIRE(doc.get_int("value", &value));
|
||||||
|
CHECK(value == 3);
|
||||||
|
bool b = true;
|
||||||
|
CHECK_FALSE(doc.get_bool("value", &b)); // не bool
|
||||||
|
CHECK_FALSE(doc.get_string("absent", name, sizeof(name)));
|
||||||
|
|
||||||
|
// key_exchange
|
||||||
|
strcpy(buf, "{\"key_exchange\":{\"ver\":1,\"proto\":1,\"key_id\":62999,"
|
||||||
|
"\"random_1\":\"ab+/12\",\"time_1\":-987654321,\"sec\":\"\"}}");
|
||||||
|
REQUIRE(doc.parse(buf));
|
||||||
|
int64_t ver = -1, key_id = -1, time_1 = -1;
|
||||||
|
REQUIRE(doc.get_int("ver", &ver));
|
||||||
|
REQUIRE(doc.get_int("key_id", &key_id));
|
||||||
|
REQUIRE(doc.get_int("time_1", &time_1));
|
||||||
|
char rnd[32];
|
||||||
|
REQUIRE(doc.get_string("random_1", rnd, sizeof(rnd)));
|
||||||
|
CHECK(ver == 1);
|
||||||
|
CHECK(key_id == 62999);
|
||||||
|
CHECK(time_1 == -987654321);
|
||||||
|
CHECK(std::string(rnd) == "ab+/12");
|
||||||
|
|
||||||
|
// envelope payload
|
||||||
|
strcpy(buf, "{\"seq_no\":42,\"data\":{\"name\":\"x\",\"value\":true}}");
|
||||||
|
REQUIRE(doc.parse(buf));
|
||||||
|
int64_t seq = -1;
|
||||||
|
REQUIRE(doc.get_int("seq_no", &seq));
|
||||||
|
CHECK(seq == 42);
|
||||||
|
bool v = false;
|
||||||
|
REQUIRE(doc.get_bool("value", &v));
|
||||||
|
CHECK(v);
|
||||||
|
|
||||||
|
// Мусор
|
||||||
|
strcpy(buf, "not json");
|
||||||
|
CHECK_FALSE(doc.parse(buf));
|
||||||
|
strcpy(buf, "[1,2]");
|
||||||
|
CHECK_FALSE(doc.parse(buf)); // верхний уровень — объект
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("json parser: экранированные строки") {
|
||||||
|
char buf[96];
|
||||||
|
strcpy(buf, "{\"name\":\"a\\\"b\\\\c\\nd\"}");
|
||||||
|
Doc doc;
|
||||||
|
REQUIRE(doc.parse(buf));
|
||||||
|
char out[32];
|
||||||
|
REQUIRE(doc.get_string("name", out, sizeof(out)));
|
||||||
|
CHECK(std::string(out) == "a\"b\\c\nd");
|
||||||
|
// Не помещается в out
|
||||||
|
char small[4];
|
||||||
|
CHECK_FALSE(doc.get_string("name", small, sizeof(small)));
|
||||||
|
}
|
||||||
20
third_party/jsmn/LICENSE
vendored
Normal file
20
third_party/jsmn/LICENSE
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2010 Serge A. Zaitsev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
471
third_party/jsmn/jsmn.h
vendored
Normal file
471
third_party/jsmn/jsmn.h
vendored
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 Serge Zaitsev
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef JSMN_H
|
||||||
|
#define JSMN_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef JSMN_STATIC
|
||||||
|
#define JSMN_API static
|
||||||
|
#else
|
||||||
|
#define JSMN_API extern
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON type identifier. Basic types are:
|
||||||
|
* o Object
|
||||||
|
* o Array
|
||||||
|
* o String
|
||||||
|
* o Other primitive: number, boolean (true/false) or null
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
JSMN_UNDEFINED = 0,
|
||||||
|
JSMN_OBJECT = 1 << 0,
|
||||||
|
JSMN_ARRAY = 1 << 1,
|
||||||
|
JSMN_STRING = 1 << 2,
|
||||||
|
JSMN_PRIMITIVE = 1 << 3
|
||||||
|
} jsmntype_t;
|
||||||
|
|
||||||
|
enum jsmnerr {
|
||||||
|
/* Not enough tokens were provided */
|
||||||
|
JSMN_ERROR_NOMEM = -1,
|
||||||
|
/* Invalid character inside JSON string */
|
||||||
|
JSMN_ERROR_INVAL = -2,
|
||||||
|
/* The string is not a full JSON packet, more bytes expected */
|
||||||
|
JSMN_ERROR_PART = -3
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON token description.
|
||||||
|
* type type (object, array, string etc.)
|
||||||
|
* start start position in JSON data string
|
||||||
|
* end end position in JSON data string
|
||||||
|
*/
|
||||||
|
typedef struct jsmntok {
|
||||||
|
jsmntype_t type;
|
||||||
|
int start;
|
||||||
|
int end;
|
||||||
|
int size;
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
int parent;
|
||||||
|
#endif
|
||||||
|
} jsmntok_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON parser. Contains an array of token blocks available. Also stores
|
||||||
|
* the string being parsed now and current position in that string.
|
||||||
|
*/
|
||||||
|
typedef struct jsmn_parser {
|
||||||
|
unsigned int pos; /* offset in the JSON string */
|
||||||
|
unsigned int toknext; /* next token to allocate */
|
||||||
|
int toksuper; /* superior token node, e.g. parent object or array */
|
||||||
|
} jsmn_parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create JSON parser over an array of tokens
|
||||||
|
*/
|
||||||
|
JSMN_API void jsmn_init(jsmn_parser *parser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run JSON parser. It parses a JSON data string into and array of tokens, each
|
||||||
|
* describing
|
||||||
|
* a single JSON object.
|
||||||
|
*/
|
||||||
|
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
|
||||||
|
jsmntok_t *tokens, const unsigned int num_tokens);
|
||||||
|
|
||||||
|
#ifndef JSMN_HEADER
|
||||||
|
/**
|
||||||
|
* Allocates a fresh unused token from the token pool.
|
||||||
|
*/
|
||||||
|
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
|
||||||
|
const size_t num_tokens) {
|
||||||
|
jsmntok_t *tok;
|
||||||
|
if (parser->toknext >= num_tokens) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
tok = &tokens[parser->toknext++];
|
||||||
|
tok->start = tok->end = -1;
|
||||||
|
tok->size = 0;
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
tok->parent = -1;
|
||||||
|
#endif
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills token type and boundaries.
|
||||||
|
*/
|
||||||
|
static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
|
||||||
|
const int start, const int end) {
|
||||||
|
token->type = type;
|
||||||
|
token->start = start;
|
||||||
|
token->end = end;
|
||||||
|
token->size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills next available token with JSON primitive.
|
||||||
|
*/
|
||||||
|
static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
|
||||||
|
const size_t len, jsmntok_t *tokens,
|
||||||
|
const size_t num_tokens) {
|
||||||
|
jsmntok_t *token;
|
||||||
|
int start;
|
||||||
|
|
||||||
|
start = parser->pos;
|
||||||
|
|
||||||
|
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
|
||||||
|
switch (js[parser->pos]) {
|
||||||
|
#ifndef JSMN_STRICT
|
||||||
|
/* In strict mode primitive must be followed by "," or "}" or "]" */
|
||||||
|
case ':':
|
||||||
|
#endif
|
||||||
|
case '\t':
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
case ' ':
|
||||||
|
case ',':
|
||||||
|
case ']':
|
||||||
|
case '}':
|
||||||
|
goto found;
|
||||||
|
default:
|
||||||
|
/* to quiet a warning from gcc*/
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef JSMN_STRICT
|
||||||
|
/* In strict mode primitive must be followed by a comma/object/array */
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_PART;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
found:
|
||||||
|
if (tokens == NULL) {
|
||||||
|
parser->pos--;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||||
|
if (token == NULL) {
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_NOMEM;
|
||||||
|
}
|
||||||
|
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
token->parent = parser->toksuper;
|
||||||
|
#endif
|
||||||
|
parser->pos--;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills next token with JSON string.
|
||||||
|
*/
|
||||||
|
static int jsmn_parse_string(jsmn_parser *parser, const char *js,
|
||||||
|
const size_t len, jsmntok_t *tokens,
|
||||||
|
const size_t num_tokens) {
|
||||||
|
jsmntok_t *token;
|
||||||
|
|
||||||
|
int start = parser->pos;
|
||||||
|
|
||||||
|
/* Skip starting quote */
|
||||||
|
parser->pos++;
|
||||||
|
|
||||||
|
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
|
||||||
|
char c = js[parser->pos];
|
||||||
|
|
||||||
|
/* Quote: end of string */
|
||||||
|
if (c == '\"') {
|
||||||
|
if (tokens == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||||
|
if (token == NULL) {
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_NOMEM;
|
||||||
|
}
|
||||||
|
jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
token->parent = parser->toksuper;
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backslash: Quoted symbol expected */
|
||||||
|
if (c == '\\' && parser->pos + 1 < len) {
|
||||||
|
int i;
|
||||||
|
parser->pos++;
|
||||||
|
switch (js[parser->pos]) {
|
||||||
|
/* Allowed escaped symbols */
|
||||||
|
case '\"':
|
||||||
|
case '/':
|
||||||
|
case '\\':
|
||||||
|
case 'b':
|
||||||
|
case 'f':
|
||||||
|
case 'r':
|
||||||
|
case 'n':
|
||||||
|
case 't':
|
||||||
|
break;
|
||||||
|
/* Allows escaped symbol \uXXXX */
|
||||||
|
case 'u':
|
||||||
|
parser->pos++;
|
||||||
|
for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0';
|
||||||
|
i++) {
|
||||||
|
/* If it isn't a hex character we have an error */
|
||||||
|
if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
|
||||||
|
(js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
|
||||||
|
(js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
parser->pos++;
|
||||||
|
}
|
||||||
|
parser->pos--;
|
||||||
|
break;
|
||||||
|
/* Unexpected symbol */
|
||||||
|
default:
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser->pos = start;
|
||||||
|
return JSMN_ERROR_PART;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON string and fill tokens.
|
||||||
|
*/
|
||||||
|
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
|
||||||
|
jsmntok_t *tokens, const unsigned int num_tokens) {
|
||||||
|
int r;
|
||||||
|
int i;
|
||||||
|
jsmntok_t *token;
|
||||||
|
int count = parser->toknext;
|
||||||
|
|
||||||
|
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
|
||||||
|
char c;
|
||||||
|
jsmntype_t type;
|
||||||
|
|
||||||
|
c = js[parser->pos];
|
||||||
|
switch (c) {
|
||||||
|
case '{':
|
||||||
|
case '[':
|
||||||
|
count++;
|
||||||
|
if (tokens == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
token = jsmn_alloc_token(parser, tokens, num_tokens);
|
||||||
|
if (token == NULL) {
|
||||||
|
return JSMN_ERROR_NOMEM;
|
||||||
|
}
|
||||||
|
if (parser->toksuper != -1) {
|
||||||
|
jsmntok_t *t = &tokens[parser->toksuper];
|
||||||
|
#ifdef JSMN_STRICT
|
||||||
|
/* In strict mode an object or array can't become a key */
|
||||||
|
if (t->type == JSMN_OBJECT) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
t->size++;
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
token->parent = parser->toksuper;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
|
||||||
|
token->start = parser->pos;
|
||||||
|
parser->toksuper = parser->toknext - 1;
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
case ']':
|
||||||
|
if (tokens == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
if (parser->toknext < 1) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
token = &tokens[parser->toknext - 1];
|
||||||
|
for (;;) {
|
||||||
|
if (token->start != -1 && token->end == -1) {
|
||||||
|
if (token->type != type) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
token->end = parser->pos + 1;
|
||||||
|
parser->toksuper = token->parent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (token->parent == -1) {
|
||||||
|
if (token->type != type || parser->toksuper == -1) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
token = &tokens[token->parent];
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
for (i = parser->toknext - 1; i >= 0; i--) {
|
||||||
|
token = &tokens[i];
|
||||||
|
if (token->start != -1 && token->end == -1) {
|
||||||
|
if (token->type != type) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
parser->toksuper = -1;
|
||||||
|
token->end = parser->pos + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Error if unmatched closing bracket */
|
||||||
|
if (i == -1) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
for (; i >= 0; i--) {
|
||||||
|
token = &tokens[i];
|
||||||
|
if (token->start != -1 && token->end == -1) {
|
||||||
|
parser->toksuper = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
case '\"':
|
||||||
|
r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
|
||||||
|
if (r < 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
if (parser->toksuper != -1 && tokens != NULL) {
|
||||||
|
tokens[parser->toksuper].size++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
case ' ':
|
||||||
|
break;
|
||||||
|
case ':':
|
||||||
|
parser->toksuper = parser->toknext - 1;
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
if (tokens != NULL && parser->toksuper != -1 &&
|
||||||
|
tokens[parser->toksuper].type != JSMN_ARRAY &&
|
||||||
|
tokens[parser->toksuper].type != JSMN_OBJECT) {
|
||||||
|
#ifdef JSMN_PARENT_LINKS
|
||||||
|
parser->toksuper = tokens[parser->toksuper].parent;
|
||||||
|
#else
|
||||||
|
for (i = parser->toknext - 1; i >= 0; i--) {
|
||||||
|
if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
|
||||||
|
if (tokens[i].start != -1 && tokens[i].end == -1) {
|
||||||
|
parser->toksuper = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#ifdef JSMN_STRICT
|
||||||
|
/* In strict mode primitives are: numbers and booleans */
|
||||||
|
case '-':
|
||||||
|
case '0':
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
case 't':
|
||||||
|
case 'f':
|
||||||
|
case 'n':
|
||||||
|
/* And they must not be keys of the object */
|
||||||
|
if (tokens != NULL && parser->toksuper != -1) {
|
||||||
|
const jsmntok_t *t = &tokens[parser->toksuper];
|
||||||
|
if (t->type == JSMN_OBJECT ||
|
||||||
|
(t->type == JSMN_STRING && t->size != 0)) {
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/* In non-strict mode every unquoted value is a primitive */
|
||||||
|
default:
|
||||||
|
#endif
|
||||||
|
r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
|
||||||
|
if (r < 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
if (parser->toksuper != -1 && tokens != NULL) {
|
||||||
|
tokens[parser->toksuper].size++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
#ifdef JSMN_STRICT
|
||||||
|
/* Unexpected char in strict mode */
|
||||||
|
default:
|
||||||
|
return JSMN_ERROR_INVAL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens != NULL) {
|
||||||
|
for (i = parser->toknext - 1; i >= 0; i--) {
|
||||||
|
/* Unmatched opened object or array */
|
||||||
|
if (tokens[i].start != -1 && tokens[i].end == -1) {
|
||||||
|
return JSMN_ERROR_PART;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new parser based over a given buffer with an array of tokens
|
||||||
|
* available.
|
||||||
|
*/
|
||||||
|
JSMN_API void jsmn_init(jsmn_parser *parser) {
|
||||||
|
parser->pos = 0;
|
||||||
|
parser->toknext = 0;
|
||||||
|
parser->toksuper = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* JSMN_HEADER */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* JSMN_H */
|
||||||
Reference in New Issue
Block a user