#include "ayla/httpd.hpp" #include #include #include #include #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(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(resp.body_len), close_after ? "close" : "keep-alive"); if (n <= 0 || static_cast(n) >= sizeof(head)) return false; if (fgl::plat::tcp_send(fd, head, static_cast(n)) != static_cast(n)) { return false; } if (resp.body != nullptr && resp.body_len > 0) { if (fgl::plat::tcp_send(fd, resp.body, resp.body_len) != static_cast(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(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(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(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(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; // " 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(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(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(n); } req.body_len = done; } FGL_LOGD("httpd: %s %s?%s from %u.%u.%u.%u", req.method, req.target, req.query, static_cast((peer_ip >> 24) & 0xffu), static_cast((peer_ip >> 16) & 0xffu), static_cast((peer_ip >> 8) & 0xffu), static_cast(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