changes in task handling
This commit is contained in:
@@ -36,6 +36,7 @@ set( HEADERS
|
|||||||
src/Services/ConfigService.h
|
src/Services/ConfigService.h
|
||||||
src/Services/DeviceControlService.h
|
src/Services/DeviceControlService.h
|
||||||
src/Services/EnrollmentService.h
|
src/Services/EnrollmentService.h
|
||||||
|
src/Security/TlsKeyUtil.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
||||||
|
|||||||
@@ -1,108 +1,123 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
namespace snoop
|
namespace snoop
|
||||||
{
|
{
|
||||||
namespace device_sec
|
namespace device_sec
|
||||||
{
|
{
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
static std::string Trim(const std::string &s)
|
static std::string Trim(const std::string &s)
|
||||||
{
|
{
|
||||||
auto b = s.find_first_not_of(" \t\r\n");
|
auto b = s.find_first_not_of(" \t\r\n");
|
||||||
auto e = s.find_last_not_of(" \t\r\n");
|
auto e = s.find_last_not_of(" \t\r\n");
|
||||||
if (b == std::string::npos)
|
if (b == std::string::npos)
|
||||||
return "";
|
return "";
|
||||||
return s.substr(b, e - b + 1);
|
return s.substr(b, e - b + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string Exec(const std::string &cmd)
|
static std::string Exec(const std::string &cmd)
|
||||||
{
|
{
|
||||||
std::array<char, 4096> buf{};
|
std::array<char, 4096> buf{};
|
||||||
std::string out;
|
std::string out;
|
||||||
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
|
FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
|
||||||
if (!pipe)
|
if (!pipe)
|
||||||
throw std::runtime_error("popen failed: " + cmd);
|
throw std::runtime_error("popen failed: " + cmd);
|
||||||
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
|
while (fgets(buf.data(), (int)buf.size(), pipe) != nullptr)
|
||||||
out.append(buf.data());
|
out.append(buf.data());
|
||||||
int rc = pclose(pipe);
|
int rc = pclose(pipe);
|
||||||
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
|
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
|
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// dumps the client key from keyring to a temp file and returns its path
|
// dumps the client key from keyring to a temp file and returns its path
|
||||||
static std::filesystem::path ExtractClientKeyFromKernelKeyring()
|
static std::filesystem::path ExtractClientKeyFromKernelKeyring()
|
||||||
{
|
{
|
||||||
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
|
std::string id = Trim(Exec("keyctl search @s user iot-client-key | tail -n1"));
|
||||||
if (id.empty())
|
if (id.empty())
|
||||||
throw std::runtime_error("iot-client-key not found in keyring");
|
throw std::runtime_error("iot-client-key not found in keyring");
|
||||||
// Create a secure temp file
|
// Create a secure temp file
|
||||||
char tmpl[] = "/run/iot/iot-keyXXXXXX";
|
char tmpl[] = "/run/iot/iot-keyXXXXXX";
|
||||||
int fd = mkstemp(tmpl);
|
int fd = mkstemp(tmpl);
|
||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
throw std::runtime_error("mkstemp failed for client key");
|
throw std::runtime_error("mkstemp failed for client key");
|
||||||
close(fd);
|
close(fd);
|
||||||
std::filesystem::path p(tmpl);
|
std::filesystem::path p(tmpl);
|
||||||
|
|
||||||
// Pipe the key payload into the temp file
|
// Pipe the key payload into the temp file
|
||||||
std::string cmd = "keyctl pipe " + id + " > " + p.string();
|
std::string cmd = "keyctl pipe " + id + " > " + p.string();
|
||||||
Exec(cmd);
|
Exec(cmd);
|
||||||
|
|
||||||
// quick sanity
|
// quick sanity
|
||||||
if (std::filesystem::file_size(p) == 0)
|
if (std::filesystem::file_size(p) == 0)
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::remove(p, ec);
|
std::filesystem::remove(p, ec);
|
||||||
throw std::runtime_error("keyctl pipe produced empty client key");
|
throw std::runtime_error("keyctl pipe produced empty client key");
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TempFile
|
struct TempFile
|
||||||
{
|
{
|
||||||
std::filesystem::path path;
|
std::filesystem::path path;
|
||||||
int fd{-1};
|
int fd{-1};
|
||||||
explicit TempFile(const std::filesystem::path &dir, const char *pattern = "iot-keyXXXXXX")
|
explicit TempFile(const std::filesystem::path &dir, const char *pattern = "iot-keyXXXXXX")
|
||||||
{
|
{
|
||||||
std::string tmpl = (dir / pattern).string();
|
std::string tmpl = (dir / pattern).string();
|
||||||
std::vector<char> name(tmpl.begin(), tmpl.end());
|
std::vector<char> name(tmpl.begin(), tmpl.end());
|
||||||
name.push_back('\0');
|
name.push_back('\0');
|
||||||
fd = mkstemp(name.data());
|
fd = mkstemp(name.data());
|
||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
throw std::runtime_error("mkstemp failed");
|
throw std::runtime_error("mkstemp failed");
|
||||||
fchmod(fd, S_IRUSR | S_IWUSR);
|
fchmod(fd, S_IRUSR | S_IWUSR);
|
||||||
path = name.data();
|
path = name.data();
|
||||||
}
|
}
|
||||||
void write_all(const void *data, size_t n)
|
void write_all(const void *data, size_t n)
|
||||||
{
|
{
|
||||||
const uint8_t *p = static_cast<const uint8_t *>(data);
|
const uint8_t *p = static_cast<const uint8_t *>(data);
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
while (off < n)
|
while (off < n)
|
||||||
{
|
{
|
||||||
ssize_t w = ::write(fd, p + off, n - off);
|
ssize_t w = ::write(fd, p + off, n - off);
|
||||||
if (w <= 0)
|
if (w <= 0)
|
||||||
throw std::runtime_error("write failed");
|
throw std::runtime_error("write failed");
|
||||||
off += (size_t)w;
|
off += (size_t)w;
|
||||||
}
|
}
|
||||||
fsync(fd);
|
fsync(fd);
|
||||||
}
|
}
|
||||||
~TempFile()
|
~TempFile()
|
||||||
{
|
{
|
||||||
if (fd >= 0)
|
if (fd >= 0)
|
||||||
::close(fd);
|
::close(fd);
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::remove(path, ec);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "WhipClient.h"
|
#include "WhipClient.h"
|
||||||
#include "ConfigService.h"
|
#include "ConfigService.h"
|
||||||
|
#include "Security/TlsKeyUtil.h"
|
||||||
|
|
||||||
namespace snoop {
|
namespace snoop {
|
||||||
|
|
||||||
@@ -59,8 +60,8 @@ public:
|
|||||||
std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
|
std::filesystem::path crt = "/etc/iot/keys/device.crt.pem";
|
||||||
|
|
||||||
// extract client key via keyctl
|
// extract client key via keyctl
|
||||||
auto tmpKey = ExtractClientKeyTemp();
|
auto tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring();
|
||||||
if (!tmpKey) {
|
if (!tmpKey.string().empty()) {
|
||||||
spdlog::error("Cannot extract client key for WHIP (keyctl user iot-client-key)");
|
spdlog::error("Cannot extract client key for WHIP (keyctl user iot-client-key)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ public:
|
|||||||
.whipUrl = whipUrl,
|
.whipUrl = whipUrl,
|
||||||
.caPath = ca.string(),
|
.caPath = ca.string(),
|
||||||
.crtPath = crt.string(),
|
.crtPath = crt.string(),
|
||||||
.keyPath = tmpKey->string(),
|
.keyPath = tmpKey,
|
||||||
.sampleRate= sampleRate,
|
.sampleRate= sampleRate,
|
||||||
.channels = channels
|
.channels = channels
|
||||||
};
|
};
|
||||||
@@ -77,11 +78,11 @@ public:
|
|||||||
try {
|
try {
|
||||||
m_whip->Start();
|
m_whip->Start();
|
||||||
spdlog::info("WHIP started");
|
spdlog::info("WHIP started");
|
||||||
m_tmpKeyPath = *tmpKey;
|
m_tmpKeyPath = tmpKey;
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
spdlog::error("WHIP start failed: {}", e.what());
|
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();
|
m_whip.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -100,36 +101,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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
|
} // namespace snoop
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ namespace snoop
|
|||||||
int port = 0;
|
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)
|
static Url ParseBase(const std::string &base)
|
||||||
{
|
{
|
||||||
std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)");
|
std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)");
|
||||||
@@ -163,9 +170,9 @@ namespace snoop
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<httplib::SSLClient> MakeClientMTLS(const Url &u,
|
std::unique_ptr<httplib::SSLClient> MakeClientMTLS(const Url &u,
|
||||||
const std::filesystem::path &ca,
|
const std::filesystem::path &ca,
|
||||||
const std::filesystem::path &crt,
|
const std::filesystem::path &crt,
|
||||||
const std::filesystem::path &key)
|
const std::filesystem::path &key)
|
||||||
{
|
{
|
||||||
if (u.scheme == "https")
|
if (u.scheme == "https")
|
||||||
{
|
{
|
||||||
@@ -181,14 +188,63 @@ namespace snoop
|
|||||||
throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled");
|
throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// else
|
}
|
||||||
// {
|
|
||||||
// auto cli = std::make_unique<httplib::Client>(u.host.c_str(), u.port);
|
bool EnsureUploadClient()
|
||||||
// cli->set_connection_timeout(10);
|
{
|
||||||
// cli->set_read_timeout(120);
|
// Recompute endpoint pieces
|
||||||
// cli->set_write_timeout(120);
|
const auto baseUrl = this->m_configService->GetBaseUrl();
|
||||||
// return cli;
|
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) -----------------------------
|
// ----------------------------- Existing logic (adjusted) -----------------------------
|
||||||
@@ -307,23 +363,13 @@ namespace snoop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare client key (temp file) for this upload pass
|
|
||||||
std::optional<std::filesystem::path> tmpKey;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring();
|
if (!EnsureUploadClient())
|
||||||
}
|
{
|
||||||
catch (const std::exception &e)
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
{
|
continue;
|
||||||
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)
|
for (const auto &filePath : files)
|
||||||
{
|
{
|
||||||
@@ -341,7 +387,7 @@ namespace snoop
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (SendRecordedFileMTLS(*client, filePath.string(),
|
if (SendRecordedFileMTLS(*m_uploadClient, filePath.string(),
|
||||||
std::stoull(startedAt),
|
std::stoull(startedAt),
|
||||||
std::stoull(stoppedAt)))
|
std::stoull(stoppedAt)))
|
||||||
{
|
{
|
||||||
@@ -356,6 +402,7 @@ namespace snoop
|
|||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
spdlog::error("Exception during file upload: {}", e.what());
|
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());
|
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));
|
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,13 +57,6 @@ namespace snoop
|
|||||||
std::function<void()> stopRecordingNow; // e.g., writerService->StopRecordingNow()
|
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)
|
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))
|
: 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
|
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)
|
static Url ParseBase(const std::string &base)
|
||||||
{
|
{
|
||||||
// very small parser: scheme://host[:port]
|
// 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
|
// 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)
|
// Create HTTPS client configured for mTLS (or HTTP if base is http)
|
||||||
std::unique_ptr<httplib::SSLClient> MakeClient(const Url &u,
|
std::unique_ptr<httplib::SSLClient> MakeClient(const Url &u,
|
||||||
@@ -212,19 +211,24 @@ namespace snoop
|
|||||||
cli->set_connection_timeout(10);
|
cli->set_connection_timeout(10);
|
||||||
cli->set_read_timeout(60);
|
cli->set_read_timeout(60);
|
||||||
cli->set_write_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;
|
return cli;
|
||||||
#else
|
#else
|
||||||
|
(void)u;
|
||||||
|
(void)ca;
|
||||||
|
(void)crt;
|
||||||
|
(void)key;
|
||||||
throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided");
|
throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided");
|
||||||
#endif
|
#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
|
// simple jittered sleep
|
||||||
@@ -354,34 +358,94 @@ namespace snoop
|
|||||||
m_controls.stopRecordingNow();
|
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()
|
void RunLoop()
|
||||||
{
|
{
|
||||||
const std::string guid = m_cfg->GetGuid();
|
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{}()};
|
std::mt19937 rng{std::random_device{}()};
|
||||||
|
|
||||||
while (!m_stop)
|
while (!m_stop)
|
||||||
{
|
{
|
||||||
// Extract client key from kernel keyring to a temp file each cycle (kept minimal on disk)
|
// (1) Ensure we have a reusable client
|
||||||
std::optional<std::filesystem::path> tmpKey;
|
if (!EnsureTaskClient())
|
||||||
try
|
|
||||||
{
|
{
|
||||||
tmpKey = snoop::device_sec::ExtractClientKeyFromKernelKeyring();
|
spdlog::info("Sleep after task client");
|
||||||
}
|
|
||||||
catch (const std::exception &e)
|
|
||||||
{
|
|
||||||
spdlog::error("Key extraction failed: {}", e.what());
|
|
||||||
SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter());
|
SleepWithJitterOnce(rng, m_cfg->GetPollingInterwall(), m_cfg->GetJitter());
|
||||||
continue;
|
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 now = NowMs();
|
||||||
const auto startMs = m_sleepStartMs.load();
|
const auto startMs = m_sleepStartMs.load();
|
||||||
const auto untilMs = m_sleepUntilMs.load();
|
const auto untilMs = m_sleepUntilMs.load();
|
||||||
@@ -390,78 +454,72 @@ namespace snoop
|
|||||||
{
|
{
|
||||||
if (now < startMs)
|
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);
|
auto waitMs = std::min<long long>(startMs - now, 60'000);
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
|
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (now >= startMs && now < untilMs)
|
if (now >= startMs && now < untilMs)
|
||||||
{
|
{
|
||||||
// In deep sleep window: do not send any requests; just wait
|
// sleeping: no requests; ensure we stopped stream/recording on entry
|
||||||
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
|
|
||||||
if (now - startMs < 1200)
|
if (now - startMs < 1200)
|
||||||
{ // within ~1.2s window
|
{
|
||||||
if (m_controls.stopStreamNow)
|
if (m_controls.stopStreamNow)
|
||||||
m_controls.stopStreamNow();
|
m_controls.stopStreamNow();
|
||||||
if (m_controls.stopRecordingNow)
|
if (m_controls.stopRecordingNow)
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
if (now >= untilMs)
|
if (now >= untilMs)
|
||||||
{
|
{
|
||||||
// Sleep period over; clear flags and resume normal polling
|
// wake up
|
||||||
m_sleepStartMs = 0;
|
m_sleepStartMs = 0;
|
||||||
m_sleepUntilMs = 0;
|
m_sleepUntilMs = 0;
|
||||||
spdlog::info("Deep sleep finished; resuming operation");
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user