changes in task handling

This commit is contained in:
tdv
2025-10-15 12:10:23 +03:00
parent 57c4769eeb
commit 2385252fcc
5 changed files with 345 additions and 260 deletions

View File

@@ -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} )

View File

@@ -104,5 +104,20 @@ namespace snoop
std::filesystem::remove(path, ec);
}
};
inline std::vector<uint8_t> 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<uint8_t>(bytes.begin(), bytes.end());
}
}
}

View File

@@ -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<std::filesystem::path> ExtractClientKeyTemp() {
auto exec = [](const std::string& cmd) {
std::array<char, 4096> 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

View File

@@ -147,6 +147,13 @@ namespace snoop
int port = 0;
};
std::unique_ptr<httplib::SSLClient> 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*$)");
@@ -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<httplib::Client>(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,24 +363,14 @@ namespace snoop
}
}
// Prepare client key (temp file) for this upload pass
std::optional<std::filesystem::path> tmpKey;
try
{
tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring();
}
catch (const std::exception &e)
if (!EnsureUploadClient())
{
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);
for (const auto &filePath : files)
{
auto fileName = filePath.filename().string();
@@ -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));
}
}

View File

@@ -57,13 +57,6 @@ namespace snoop
std::function<void()> stopRecordingNow; // e.g., writerService->StopRecordingNow()
};
// DeviceControlService(std::shared_ptr<ConfigService> 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<ConfigService> 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<httplib::SSLClient> 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]
@@ -196,7 +196,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<httplib::SSLClient> MakeClient(const Url &u,
const std::filesystem::path &ca,
@@ -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<httplib::Client>(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<std::filesystem::path> 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,44 +454,44 @@ 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<long long>(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<long long>(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<long long>(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");
}
}
// (3) Not sleeping -> poll tasks
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());
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)", getPath);
spdlog::warn("GET {} failed (no response)", path);
m_taskClient.reset(); // force rebuild next iter
}
else if (res->status == 204)
{
@@ -440,30 +504,24 @@ namespace snoop
{
auto handler = ResolveHandler(t.type);
auto [ok, resultJson, err] = handler(t);
PostResult(*cli, guid, t.id, ok, resultJson, err);
PostResult(*m_taskClient, guid, t.id, ok, resultJson, err);
}
}
else
{
spdlog::warn("GET {} -> HTTP {}, body: {}", getPath, res->status, res->body);
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();
}
// cleanup temp key ASAP
if (tmpKey)
{
std::error_code ec;
std::filesystem::remove(*tmpKey, ec);
}
// (4) Backoff before next poll
SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter());
}
}
}
};
} // namespace snoop