From 2385252fcce6c4bdc327a0bcdd301c00304413c8 Mon Sep 17 00:00:00 2001 From: tdv Date: Wed, 15 Oct 2025 12:10:23 +0300 Subject: [PATCH] changes in task handling --- CMakeLists.txt | 1 + src/Security/TlsKeyUtil.h | 229 +++++++++++++++------------- src/Services/AudioStreamService.h | 40 +---- src/Services/AudioWriterService.h | 109 ++++++++----- src/Services/DeviceControlService.h | 226 +++++++++++++++++---------- 5 files changed, 345 insertions(+), 260 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a73145..b53ecc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set( HEADERS src/Services/ConfigService.h src/Services/DeviceControlService.h src/Services/EnrollmentService.h + src/Security/TlsKeyUtil.h ) add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} ) diff --git a/src/Security/TlsKeyUtil.h b/src/Security/TlsKeyUtil.h index 2261f98..13ffff3 100644 --- a/src/Security/TlsKeyUtil.h +++ b/src/Security/TlsKeyUtil.h @@ -1,108 +1,123 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace snoop -{ - namespace device_sec - { - // --- helpers --- - static std::string Trim(const std::string &s) - { - auto b = s.find_first_not_of(" \t\r\n"); - auto e = s.find_last_not_of(" \t\r\n"); - if (b == std::string::npos) - return ""; - return s.substr(b, e - b + 1); - } - - static std::string Exec(const std::string &cmd) - { - std::array buf{}; - std::string out; - FILE *pipe = popen((cmd + " 2>&1").c_str(), "r"); - if (!pipe) - throw std::runtime_error("popen failed: " + cmd); - while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) - out.append(buf.data()); - int rc = pclose(pipe); - int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc; - if (exitCode != 0) - spdlog::warn("Command '{}' exited with code {}", cmd, exitCode); - return out; - } - - // dumps the client key from keyring to a temp file and returns its path - static std::filesystem::path ExtractClientKeyFromKernelKeyring() - { - std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); - if (id.empty()) - throw std::runtime_error("iot-client-key not found in keyring"); - // Create a secure temp file - char tmpl[] = "/run/iot/iot-keyXXXXXX"; - int fd = mkstemp(tmpl); - if (fd < 0) - throw std::runtime_error("mkstemp failed for client key"); - close(fd); - std::filesystem::path p(tmpl); - - // Pipe the key payload into the temp file - std::string cmd = "keyctl pipe " + id + " > " + p.string(); - Exec(cmd); - - // quick sanity - if (std::filesystem::file_size(p) == 0) - { - std::error_code ec; - std::filesystem::remove(p, ec); - throw std::runtime_error("keyctl pipe produced empty client key"); - } - return p; - } - - struct TempFile - { - std::filesystem::path path; - int fd{-1}; - explicit TempFile(const std::filesystem::path &dir, const char *pattern = "iot-keyXXXXXX") - { - std::string tmpl = (dir / pattern).string(); - std::vector name(tmpl.begin(), tmpl.end()); - name.push_back('\0'); - fd = mkstemp(name.data()); - if (fd < 0) - throw std::runtime_error("mkstemp failed"); - fchmod(fd, S_IRUSR | S_IWUSR); - path = name.data(); - } - void write_all(const void *data, size_t n) - { - const uint8_t *p = static_cast(data); - size_t off = 0; - while (off < n) - { - ssize_t w = ::write(fd, p + off, n - off); - if (w <= 0) - throw std::runtime_error("write failed"); - off += (size_t)w; - } - fsync(fd); - } - ~TempFile() - { - if (fd >= 0) - ::close(fd); - std::error_code ec; - std::filesystem::remove(path, ec); - } - }; - } +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace snoop +{ + namespace device_sec + { + // --- helpers --- + static std::string Trim(const std::string &s) + { + auto b = s.find_first_not_of(" \t\r\n"); + auto e = s.find_last_not_of(" \t\r\n"); + if (b == std::string::npos) + return ""; + return s.substr(b, e - b + 1); + } + + static std::string Exec(const std::string &cmd) + { + std::array buf{}; + std::string out; + FILE *pipe = popen((cmd + " 2>&1").c_str(), "r"); + if (!pipe) + throw std::runtime_error("popen failed: " + cmd); + while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) + out.append(buf.data()); + int rc = pclose(pipe); + int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc; + if (exitCode != 0) + spdlog::warn("Command '{}' exited with code {}", cmd, exitCode); + return out; + } + + // dumps the client key from keyring to a temp file and returns its path + static std::filesystem::path ExtractClientKeyFromKernelKeyring() + { + std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); + if (id.empty()) + throw std::runtime_error("iot-client-key not found in keyring"); + // Create a secure temp file + char tmpl[] = "/run/iot/iot-keyXXXXXX"; + int fd = mkstemp(tmpl); + if (fd < 0) + throw std::runtime_error("mkstemp failed for client key"); + close(fd); + std::filesystem::path p(tmpl); + + // Pipe the key payload into the temp file + std::string cmd = "keyctl pipe " + id + " > " + p.string(); + Exec(cmd); + + // quick sanity + if (std::filesystem::file_size(p) == 0) + { + std::error_code ec; + std::filesystem::remove(p, ec); + throw std::runtime_error("keyctl pipe produced empty client key"); + } + return p; + } + + struct TempFile + { + std::filesystem::path path; + int fd{-1}; + explicit TempFile(const std::filesystem::path &dir, const char *pattern = "iot-keyXXXXXX") + { + std::string tmpl = (dir / pattern).string(); + std::vector name(tmpl.begin(), tmpl.end()); + name.push_back('\0'); + fd = mkstemp(name.data()); + if (fd < 0) + throw std::runtime_error("mkstemp failed"); + fchmod(fd, S_IRUSR | S_IWUSR); + path = name.data(); + } + void write_all(const void *data, size_t n) + { + const uint8_t *p = static_cast(data); + size_t off = 0; + while (off < n) + { + ssize_t w = ::write(fd, p + off, n - off); + if (w <= 0) + throw std::runtime_error("write failed"); + off += (size_t)w; + } + fsync(fd); + } + ~TempFile() + { + if (fd >= 0) + ::close(fd); + std::error_code ec; + std::filesystem::remove(path, ec); + } + }; + + inline std::vector ReadClientKeyPayloadFromKeyring() + { + // 1) get key id + std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1")); + if (id.empty()) + throw std::runtime_error("iot-client-key not found in keyring"); + + // 2) capture payload (no redirection to file) + std::string bytes = Exec("keyctl pipe " + id); + if (bytes.empty()) + throw std::runtime_error("keyctl pipe returned empty payload"); + + return std::vector(bytes.begin(), bytes.end()); + } + } } \ No newline at end of file diff --git a/src/Services/AudioStreamService.h b/src/Services/AudioStreamService.h index dfeaf32..e40811c 100644 --- a/src/Services/AudioStreamService.h +++ b/src/Services/AudioStreamService.h @@ -17,6 +17,7 @@ #include "WhipClient.h" #include "ConfigService.h" +#include "Security/TlsKeyUtil.h" namespace snoop { @@ -59,8 +60,8 @@ public: std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; // extract client key via keyctl - auto tmpKey = ExtractClientKeyTemp(); - if (!tmpKey) { + auto tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring(); + if (!tmpKey.string().empty()) { spdlog::error("Cannot extract client key for WHIP (keyctl user iot-client-key)"); return false; } @@ -69,7 +70,7 @@ public: .whipUrl = whipUrl, .caPath = ca.string(), .crtPath = crt.string(), - .keyPath = tmpKey->string(), + .keyPath = tmpKey, .sampleRate= sampleRate, .channels = channels }; @@ -77,11 +78,11 @@ public: try { m_whip->Start(); spdlog::info("WHIP started"); - m_tmpKeyPath = *tmpKey; + m_tmpKeyPath = tmpKey; return true; } catch (const std::exception& e) { spdlog::error("WHIP start failed: {}", e.what()); - std::error_code ec; std::filesystem::remove(*tmpKey, ec); + std::error_code ec; std::filesystem::remove(tmpKey, ec); m_whip.reset(); return false; } @@ -100,36 +101,7 @@ public: } private: - static std::optional ExtractClientKeyTemp() { - auto exec = [](const std::string& cmd) { - std::array buf{}; - std::string out; - FILE* pipe = popen((cmd + " 2>&1").c_str(), "r"); - if (!pipe) return std::string{}; - while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr) out.append(buf.data()); - pclose(pipe); - return out; - }; - auto trim = [](std::string s){ - auto b=s.find_first_not_of(" \t\r\n"), e=s.find_last_not_of(" \t\r\n"); - return (b==std::string::npos) ? std::string{} : s.substr(b, e-b+1); - }; - std::string id = trim(exec("keyctl search @s user iot-client-key | tail -n1")); - if (id.empty()) return std::nullopt; - - char tmpl[] = "/run/iot-whip-keyXXXXXX"; - int fd = mkstemp(tmpl); - if (fd < 0) return std::nullopt; - close(fd); - std::filesystem::path p(tmpl); - exec("keyctl pipe " + id + " > " + p.string()); - if (!std::filesystem::exists(p) || std::filesystem::file_size(p) == 0) { - std::error_code ec; std::filesystem::remove(p, ec); - return std::nullopt; - } - return p; - } }; } // namespace snoop diff --git a/src/Services/AudioWriterService.h b/src/Services/AudioWriterService.h index 9441bdf..79a5bcf 100644 --- a/src/Services/AudioWriterService.h +++ b/src/Services/AudioWriterService.h @@ -147,6 +147,13 @@ namespace snoop int port = 0; }; + std::unique_ptr m_uploadClient; + std::string m_uploadHost; + int m_uploadPort = 0; + bool m_uploadHttps = true; + std::filesystem::path m_uploadCa; + std::filesystem::path m_uploadCrt; + static Url ParseBase(const std::string &base) { std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)"); @@ -163,9 +170,9 @@ namespace snoop } std::unique_ptr MakeClientMTLS(const Url &u, - const std::filesystem::path &ca, - const std::filesystem::path &crt, - const std::filesystem::path &key) + const std::filesystem::path &ca, + const std::filesystem::path &crt, + const std::filesystem::path &key) { if (u.scheme == "https") { @@ -181,14 +188,63 @@ namespace snoop throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled"); #endif } - // else - // { - // auto cli = std::make_unique(u.host.c_str(), u.port); - // cli->set_connection_timeout(10); - // cli->set_read_timeout(120); - // cli->set_write_timeout(120); - // return cli; - // } + } + + bool EnsureUploadClient() + { + // Recompute endpoint pieces + const auto baseUrl = this->m_configService->GetBaseUrl(); + Url url = ParseBase(baseUrl); + + // Detect changes requiring re-init + bool needReinit = + !m_uploadClient || + m_uploadHost != url.host || m_uploadPort != url.port || + (url.scheme == "https") != m_uploadHttps; + + // Setup CA/CRT (from enrollment) + std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; + if (!std::filesystem::exists(ca)) + ca = "/etc/iot/keys/ca_chain.pem"; + const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; + + if (!needReinit) + { + // still ok — reuse + return true; + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + try + { + auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring(); + // pick a writable dir (use /tmp to avoid /run root perms surprises) + snoop::device_sec::TempFile tf(std::filesystem::temp_directory_path()); + tf.write_all(payload.data(), payload.size()); + + // OpenSSL loads the files now; once this returns, the key is in-memory. + auto cli = MakeClientMTLS(url, ca, crt, tf.path); + // tf destructor will remove the file at scope end + // Swap in + m_uploadClient = std::move(cli); + m_uploadHost = url.host; + m_uploadPort = url.port; + m_uploadHttps = (url.scheme == "https"); + m_uploadCa = ca; + m_uploadCrt = crt; + return true; + } + catch (const std::exception &e) + { + spdlog::error("EnsureUploadClient: failed to set client cert: {}", e.what()); + return false; + } +#else + (void)ca; + (void)crt; + spdlog::error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT not enabled"); + return false; +#endif } // ----------------------------- Existing logic (adjusted) ----------------------------- @@ -307,23 +363,13 @@ namespace snoop } } - // Prepare client key (temp file) for this upload pass - std::optional tmpKey; try { - tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring(); - } - catch (const std::exception &e) - { - spdlog::error("Cannot extract client key for mTLS: {}", e.what()); - // Wait a bit and retry later - std::this_thread::sleep_for(std::chrono::seconds(1)); - continue; - } - - try - { - auto client = MakeClientMTLS(url, ca, crt, *tmpKey); + if (!EnsureUploadClient()) + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } for (const auto &filePath : files) { @@ -341,7 +387,7 @@ namespace snoop try { - if (SendRecordedFileMTLS(*client, filePath.string(), + if (SendRecordedFileMTLS(*m_uploadClient, filePath.string(), std::stoull(startedAt), std::stoull(stoppedAt))) { @@ -356,6 +402,7 @@ namespace snoop catch (const std::exception &e) { spdlog::error("Exception during file upload: {}", e.what()); + m_uploadClient.reset(); } } } @@ -363,14 +410,6 @@ namespace snoop { spdlog::error("mTLS client setup failed: {}", e.what()); } - - // cleanup temp key asap - if (tmpKey) - { - std::error_code ec; - std::filesystem::remove(*tmpKey, ec); - } - std::this_thread::sleep_for(std::chrono::milliseconds(800)); } } diff --git a/src/Services/DeviceControlService.h b/src/Services/DeviceControlService.h index 5cb2126..1811d08 100644 --- a/src/Services/DeviceControlService.h +++ b/src/Services/DeviceControlService.h @@ -57,13 +57,6 @@ namespace snoop std::function stopRecordingNow; // e.g., writerService->StopRecordingNow() }; - // DeviceControlService(std::shared_ptr cfg, Handlers handlers) - // : m_cfg(std::move(cfg)), m_handlers(std::move(handlers)) - // { - // m_stop = false; - // m_thread = std::thread(&DeviceControlService::RunLoop, this); - // } - DeviceControlService(std::shared_ptr cfg, Handlers handlers, Controls controls) : m_cfg(std::move(cfg)), m_handlers(std::move(handlers)), m_controls(std::move(controls)) { @@ -156,6 +149,13 @@ namespace snoop int port = 0; // default if 0 }; + std::unique_ptr m_taskClient; + std::string m_taskHost; + int m_taskPort = 0; + bool m_taskHttps = true; + std::filesystem::path m_taskCa; + std::filesystem::path m_taskCrt; + static Url ParseBase(const std::string &base) { // very small parser: scheme://host[:port] @@ -195,7 +195,6 @@ namespace snoop } // dumps the client key from keyring to a temp file and returns its path - // Create HTTPS client configured for mTLS (or HTTP if base is http) std::unique_ptr MakeClient(const Url &u, @@ -212,19 +211,24 @@ namespace snoop cli->set_connection_timeout(10); cli->set_read_timeout(60); cli->set_write_timeout(60); + + if (!cli->is_valid()) + { + spdlog::warn("[mTLS debug] SSLClient is not valid right after creation."); + } + else + { + spdlog::debug("[mTLS debug] SSLClient initialized OK for host={} port={}", u.host, u.port); + } return cli; #else + (void)u; + (void)ca; + (void)crt; + (void)key; throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided"); #endif } - // else - // { - // auto cli = std::make_unique(u.host.c_str(), u.port); - // cli->set_connection_timeout(10); - // cli->set_read_timeout(60); - // cli->set_write_timeout(60); - // return cli; - // } } // simple jittered sleep @@ -354,34 +358,94 @@ namespace snoop m_controls.stopRecordingNow(); } + bool EnsureTaskClient() + { + // Recompute endpoint pieces + const auto baseUrl = this->m_cfg->GetBaseUrl(); + Url url = ParseBase(baseUrl); + + bool needReinit = !m_taskClient || + m_taskHost != url.host || m_taskPort != url.port || + (url.scheme == "https") != m_taskHttps; + + // Setup CA/CRT (from enrollment) + std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; + if (!std::filesystem::exists(ca)) + ca = "/etc/iot/keys/ca_chain.pem"; + const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; + + if (!needReinit) + { + // still ok — reuse + return true; + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + try + { + auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring(); + // pick a writable dir (use /tmp to avoid /run root perms surprises) + snoop::device_sec::TempFile tf(std::filesystem::temp_directory_path()); + tf.write_all(payload.data(), payload.size()); + + // OpenSSL loads the files now; once this returns, the key is in-memory. + auto cli = MakeClient(url, ca, crt, tf.path); + spdlog::info("Creating new client because of change"); + // tf destructor will remove the file at scope end + // Swap in + m_taskClient = std::move(cli); + m_taskHost = url.host; + m_taskPort = url.port; + m_taskHttps = (url.scheme == "https"); + m_taskCa = ca; + m_taskCrt = crt; + return true; + } + catch (const std::exception &e) + { + spdlog::error("EnsureTaskClient: failed to set client cert: {}", e.what()); + return false; + } +#else + (void)ca; + (void)crt; + spdlog::error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT not enabled"); + return false; +#endif + } + void RunLoop() { const std::string guid = m_cfg->GetGuid(); - const auto base = m_cfg->GetBaseUrl(); - Url url = ParseBase(base); - - // Cert paths from enrollment step - std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; // or ca_chain.pem if you prefer - if (!std::filesystem::exists(ca)) - ca = "/etc/iot/keys/ca_chain.pem"; - std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; - std::mt19937 rng{std::random_device{}()}; while (!m_stop) { - // Extract client key from kernel keyring to a temp file each cycle (kept minimal on disk) - std::optional tmpKey; - try + // (1) Ensure we have a reusable client + if (!EnsureTaskClient()) { - tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring(); - } - catch (const std::exception &e) - { - spdlog::error("Key extraction failed: {}", e.what()); + spdlog::info("Sleep after task client"); SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter()); continue; } + //// FOR SSL DEBUG - remove in prod + if (m_taskClient) + { + long verify_result = m_taskClient->get_openssl_verify_result(); + if (verify_result == X509_V_OK) + { + spdlog::info("[mTLS debug] OpenSSL verify: SUCCESS (X509_V_OK)"); + } + else + { + const char *err_str = X509_verify_cert_error_string(verify_result); + spdlog::error("[mTLS debug] OpenSSL verify failed: {} ({})", + err_str ? err_str : "Unknown error", verify_result); + } + } + ///////////////////////////////// + + // (2) Deep-sleep gate const auto now = NowMs(); const auto startMs = m_sleepStartMs.load(); const auto untilMs = m_sleepUntilMs.load(); @@ -390,78 +454,72 @@ namespace snoop { if (now < startMs) { - // Not yet time to sleep: wait until start (no network calls) + // not yet sleeping: wait until start, no requests auto waitMs = std::min(startMs - now, 60'000); std::this_thread::sleep_for(std::chrono::milliseconds(waitMs)); continue; } if (now >= startMs && now < untilMs) { - // In deep sleep window: do not send any requests; just wait - auto waitMs = std::min(untilMs - now, 60'000); - std::this_thread::sleep_for(std::chrono::milliseconds(waitMs)); - // On first entry, ensure we stopped immediately + // sleeping: no requests; ensure we stopped stream/recording on entry if (now - startMs < 1200) - { // within ~1.2s window + { if (m_controls.stopStreamNow) m_controls.stopStreamNow(); if (m_controls.stopRecordingNow) m_controls.stopRecordingNow(); } + auto waitMs = std::min(untilMs - now, 60'000); + std::this_thread::sleep_for(std::chrono::milliseconds(waitMs)); continue; } if (now >= untilMs) { - // Sleep period over; clear flags and resume normal polling + // wake up m_sleepStartMs = 0; m_sleepUntilMs = 0; spdlog::info("Deep sleep finished; resuming operation"); } - try - { - auto cli = MakeClient(url, ca, crt, *tmpKey); - - // --- GET /tasks/:guid - const std::string getPath = "/api/tasks/" + guid; - spdlog::info("GET {}", getPath); - auto res = cli->Get(getPath.c_str()); - if (!res) - { - spdlog::warn("GET {} failed (no response)", getPath); - } - else if (res->status == 204) - { - spdlog::debug("No tasks (204)."); - } - else if (res->status >= 200 && res->status < 300) - { - auto tasks = ParseTasks(res->body); - for (const auto &t : tasks) - { - auto handler = ResolveHandler(t.type); - auto [ok, resultJson, err] = handler(t); - PostResult(*cli, guid, t.id, ok, resultJson, err); - } - } - else - { - spdlog::warn("GET {} -> HTTP {}, body: {}", getPath, res->status, res->body); - } - } - catch (const std::exception &e) - { - spdlog::error("Task loop error: {}", e.what()); - } - - // cleanup temp key ASAP - if (tmpKey) - { - std::error_code ec; - std::filesystem::remove(*tmpKey, ec); - } - - SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter()); } + + // (3) Not sleeping -> poll tasks + try + { + const std::string path = "/api/tasks/" + guid; + spdlog::info("GET {}", path); + auto res = m_taskClient->Get(path.c_str()); + if (!res) + { + spdlog::warn("GET {} failed (no response)", path); + m_taskClient.reset(); // force rebuild next iter + } + else if (res->status == 204) + { + spdlog::debug("No tasks (204)."); + } + else if (res->status >= 200 && res->status < 300) + { + auto tasks = ParseTasks(res->body); + for (const auto &t : tasks) + { + auto handler = ResolveHandler(t.type); + auto [ok, resultJson, err] = handler(t); + PostResult(*m_taskClient, guid, t.id, ok, resultJson, err); + } + } + else + { + spdlog::warn("GET {} -> HTTP {}, body: {}", path, res->status, res->body); + } + } + catch (const std::exception &e) + { + spdlog::error("Task loop error: {}", e.what()); + m_taskClient.reset(); + } + + // (4) Backoff before next poll + SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter()); } } };