- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
(переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
(обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
286 lines
9.5 KiB
C++
286 lines
9.5 KiB
C++
#include "ayla/httpd.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <new>
|
|
|
|
#include "ayla/log.hpp"
|
|
|
|
namespace fgl::ayla {
|
|
|
|
struct ThreadCtx {
|
|
HttpServer* self;
|
|
};
|
|
|
|
namespace {
|
|
|
|
// Статусные строки для используемых кодов.
|
|
struct StatusText { int code; const char* text; };
|
|
constexpr StatusText kStatusTexts[] = {
|
|
{200, "OK"}, {206, "Partial Content"}, {400, "Bad Request"},
|
|
{401, "Unauthorized"}, {404, "Not Found"}, {412, "Precondition Failed"},
|
|
{426, "Upgrade Required"}, {500, "Internal Server Error"},
|
|
};
|
|
|
|
const char* status_text(int code) {
|
|
for (const auto& st : kStatusTexts) {
|
|
if (st.code == code) return st.text;
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
bool recv_line(int fd, char* buf, size_t buflen, size_t* out_len) {
|
|
size_t len = 0;
|
|
for (;;) {
|
|
if (len + 1 >= buflen) return false; // слишком длинно
|
|
uint8_t ch;
|
|
long n = fgl::plat::tcp_recv(fd, &ch, 1);
|
|
if (n <= 0) return false;
|
|
if (ch == '\n') {
|
|
if (len > 0 && buf[len - 1] == '\r') len--; // CRLF
|
|
buf[len] = '\0';
|
|
*out_len = len;
|
|
return true;
|
|
}
|
|
buf[len++] = static_cast<char>(ch);
|
|
}
|
|
}
|
|
|
|
// Регистронезависимое сравнение первых n символов.
|
|
bool ieq_prefix(const char* a, const char* b, size_t n) {
|
|
for (size_t i = 0; i < n; i++) {
|
|
char ca = a[i] | 0x20, cb = b[i] | 0x20;
|
|
if (ca != cb) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Читает заголовки до пустой строки; суммарный объём ограничен
|
|
// kHttpdMaxHeadersTotal (защита от бесконечного стрима заголовков).
|
|
bool recv_headers(int fd, long* content_length, bool* connection_close) {
|
|
char line[kHttpdMaxHeaderLine];
|
|
size_t len;
|
|
size_t total = 2; // завершающий CRLF
|
|
*content_length = 0;
|
|
*connection_close = false;
|
|
for (;;) {
|
|
if (!recv_line(fd, line, sizeof(line), &len)) return false;
|
|
if (len == 0) return true; // конец заголовков
|
|
total += len + 2;
|
|
if (total > kHttpdMaxHeadersTotal) return false;
|
|
if (ieq_prefix(line, "Content-Length:", 15)) {
|
|
*content_length = strtol(line + 15, nullptr, 10);
|
|
} else if (ieq_prefix(line, "Connection:", 11)) {
|
|
const char* v = line + 11;
|
|
while (*v == ' ') v++;
|
|
if (ieq_prefix(v, "close", 5)) *connection_close = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// close_after — добавить Connection: close и не ждать продолжения.
|
|
bool send_response(int fd, const HttpResponse& resp, bool close_after) {
|
|
char head[192];
|
|
int n = snprintf(head, sizeof(head),
|
|
"HTTP/1.1 %d %s\r\n"
|
|
"Content-Type: %s\r\n"
|
|
"Content-Length: %u\r\n"
|
|
"Connection: %s\r\n"
|
|
"\r\n",
|
|
resp.status, status_text(resp.status), resp.content_type,
|
|
static_cast<unsigned>(resp.body_len),
|
|
close_after ? "close" : "keep-alive");
|
|
if (n <= 0 || static_cast<size_t>(n) >= sizeof(head)) return false;
|
|
if (fgl::plat::tcp_send(fd, head, static_cast<size_t>(n)) !=
|
|
static_cast<long>(n)) {
|
|
return false;
|
|
}
|
|
if (resp.body != nullptr && resp.body_len > 0) {
|
|
if (fgl::plat::tcp_send(fd, resp.body, resp.body_len) !=
|
|
static_cast<long>(resp.body_len)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void parse_target(const char* full_target, HttpRequest* req) {
|
|
const char* q = strchr(full_target, '?');
|
|
size_t path_len = (q != nullptr) ? static_cast<size_t>(q - full_target)
|
|
: strlen(full_target);
|
|
if (path_len >= sizeof(req->target)) path_len = sizeof(req->target) - 1;
|
|
memcpy(req->target, full_target, path_len);
|
|
req->target[path_len] = '\0';
|
|
req->query[0] = '\0';
|
|
if (q != nullptr && q[1] != '\0') {
|
|
size_t qlen = strlen(q + 1);
|
|
if (qlen >= sizeof(req->query)) qlen = sizeof(req->query) - 1;
|
|
memcpy(req->query, q + 1, qlen);
|
|
req->query[qlen] = '\0';
|
|
}
|
|
}
|
|
|
|
constexpr uint32_t kHttpdThreadStack = 24576; // commands-путь: envelope+crypto ~10КБ поверх буферов запроса
|
|
constexpr uint32_t kAcceptPollMs = 100;
|
|
constexpr uint32_t kClientRxTimeoutMs = 30000;
|
|
constexpr uint32_t kClientTxTimeoutMs = 10000;
|
|
|
|
} // namespace
|
|
|
|
HttpServer::~HttpServer() { stop(); }
|
|
|
|
bool HttpServer::start(uint16_t port, HttpHandler handler, void* ctx,
|
|
const char* thread_name) {
|
|
if (running_.load(std::memory_order_acquire)) return false;
|
|
handler_ = handler;
|
|
ctx_ = ctx;
|
|
int fd = fgl::plat::tcp_listen(port);
|
|
if (fd < 0) {
|
|
FGL_LOGE("httpd: tcp_listen(%u) failed", static_cast<unsigned>(port));
|
|
return false;
|
|
}
|
|
listen_fd_ = fd;
|
|
port_ = fgl::plat::tcp_local_port(fd);
|
|
|
|
auto* tc = new (std::nothrow) ThreadCtx{this};
|
|
if (tc == nullptr) {
|
|
fgl::plat::tcp_close(listen_fd_);
|
|
listen_fd_ = -1;
|
|
return false;
|
|
}
|
|
thread_ctx_ = tc;
|
|
running_.store(true, std::memory_order_release);
|
|
if (!fgl::plat::thread_create(
|
|
[](void* p) {
|
|
static_cast<ThreadCtx*>(p)->self->run();
|
|
},
|
|
tc, thread_name, kHttpdThreadStack, &thread_)) {
|
|
running_.store(false, std::memory_order_release);
|
|
delete tc;
|
|
thread_ctx_ = nullptr;
|
|
fgl::plat::tcp_close(listen_fd_);
|
|
listen_fd_ = -1;
|
|
return false;
|
|
}
|
|
FGL_LOGI("httpd: listening on port %u", static_cast<unsigned>(port_));
|
|
return true;
|
|
}
|
|
|
|
void HttpServer::stop() {
|
|
if (!running_.exchange(false, std::memory_order_acq_rel)) {
|
|
return;
|
|
}
|
|
// Прерываем активное соединение, чтобы поток не ждал rx-таймаута.
|
|
int active = active_fd_.exchange(-1, std::memory_order_acq_rel);
|
|
if (active >= 0) {
|
|
fgl::plat::tcp_shutdown(active);
|
|
}
|
|
// Гарантированно ждём завершения потока; poll-цикл замечает running_
|
|
// в пределах kAcceptPollMs.
|
|
if (thread_ != nullptr) {
|
|
fgl::plat::thread_join(thread_);
|
|
thread_ = nullptr;
|
|
}
|
|
delete thread_ctx_;
|
|
thread_ctx_ = nullptr;
|
|
if (listen_fd_ >= 0) {
|
|
fgl::plat::tcp_close(listen_fd_);
|
|
listen_fd_ = -1;
|
|
}
|
|
}
|
|
|
|
void HttpServer::run() {
|
|
while (running_.load(std::memory_order_acquire)) {
|
|
if (!fgl::plat::tcp_poll_readable(listen_fd_, kAcceptPollMs)) {
|
|
continue; // таймаут поллинга — перепроверяем running_
|
|
}
|
|
uint32_t peer_ip = 0;
|
|
uint16_t peer_port = 0;
|
|
int fd = fgl::plat::tcp_accept(listen_fd_, &peer_ip, &peer_port);
|
|
if (fd < 0) {
|
|
if (!running_.load(std::memory_order_acquire)) break;
|
|
fgl::plat::sleep_ms(20);
|
|
continue;
|
|
}
|
|
fgl::plat::tcp_set_timeout(fd, kClientRxTimeoutMs, kClientTxTimeoutMs);
|
|
active_fd_.store(fd, std::memory_order_release);
|
|
if (!running_.load(std::memory_order_acquire)) {
|
|
// stop() мог произойти в зазоре accept->store: закрываем и выходим.
|
|
// (Порядок store(fd)->load(running_) против exchange-ов в stop()
|
|
// закрывает все интерливинги: либо stop() увидит fd и shutdown'нет.)
|
|
active_fd_.store(-1, std::memory_order_release);
|
|
fgl::plat::tcp_close(fd);
|
|
break;
|
|
}
|
|
handle_connection(fd, peer_ip, peer_port);
|
|
active_fd_.store(-1, std::memory_order_release);
|
|
fgl::plat::tcp_close(fd);
|
|
}
|
|
}
|
|
|
|
void HttpServer::handle_connection(int fd, uint32_t peer_ip, uint16_t peer_port) {
|
|
while (running_.load(std::memory_order_acquire)) { // keep-alive
|
|
char line[kHttpdMaxTarget + kHttpdMaxQuery + 16];
|
|
size_t len;
|
|
if (!recv_line(fd, line, sizeof(line), &len)) return;
|
|
|
|
HttpRequest req {};
|
|
req.peer_ip = peer_ip;
|
|
req.peer_port = peer_port;
|
|
|
|
// "<METHOD> <target> HTTP/x.y"
|
|
char* sp1 = strchr(line, ' ');
|
|
if (sp1 == nullptr) return;
|
|
*sp1 = '\0';
|
|
if (strlen(line) >= sizeof(req.method)) return;
|
|
strcpy(req.method, line);
|
|
char* target = sp1 + 1;
|
|
char* sp2 = strrchr(target, ' ');
|
|
if (sp2 == nullptr) return;
|
|
*sp2 = '\0';
|
|
parse_target(target, &req);
|
|
|
|
long content_length = 0;
|
|
bool connection_close = false;
|
|
if (!recv_headers(fd, &content_length, &connection_close)) return;
|
|
if (content_length < 0 ||
|
|
static_cast<size_t>(content_length) > kHttpdMaxBody) {
|
|
HttpResponse resp;
|
|
resp.status = 400;
|
|
send_response(fd, resp, /*close_after=*/true);
|
|
// shutdown до close предотвращает RST, обгоняющий ответ
|
|
// (recv после SHUT_RDWR сразу вернёт 0, дренаж не нужен).
|
|
fgl::plat::tcp_shutdown(fd);
|
|
return;
|
|
}
|
|
if (content_length > 0) {
|
|
size_t need = static_cast<size_t>(content_length);
|
|
size_t done = 0;
|
|
while (done < need) {
|
|
long n = fgl::plat::tcp_recv(fd, req.body + done, need - done);
|
|
if (n <= 0) return;
|
|
done += static_cast<size_t>(n);
|
|
}
|
|
req.body_len = done;
|
|
}
|
|
|
|
FGL_LOGD("httpd: %s %s?%s from %u.%u.%u.%u", req.method, req.target,
|
|
req.query, static_cast<unsigned>((peer_ip >> 24) & 0xffu),
|
|
static_cast<unsigned>((peer_ip >> 16) & 0xffu),
|
|
static_cast<unsigned>((peer_ip >> 8) & 0xffu),
|
|
static_cast<unsigned>(peer_ip & 0xffu));
|
|
|
|
HttpResponse resp; // по умолчанию 404 — «пустой httpd»
|
|
resp.status = 404;
|
|
bool keep = true;
|
|
if (handler_ != nullptr) {
|
|
keep = handler_(req, resp, ctx_);
|
|
}
|
|
if (!send_response(fd, resp, /*close_after=*/!keep)) return;
|
|
if (!keep || connection_close) return;
|
|
}
|
|
}
|
|
|
|
} // namespace fgl::ayla
|