modified config and created enrolment service and device control service
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@
|
||||
/toolchain/sysroot/
|
||||
/toolchain/build/
|
||||
|
||||
.vscode/
|
||||
@@ -32,6 +32,8 @@ set( HEADERS
|
||||
src/Services/AudioStreamService.h
|
||||
src/Services/AudioWriterService.h
|
||||
src/Services/ConfigService.h
|
||||
src/Services/DeviceControlService.h
|
||||
src/Services/EnrollmentService.h
|
||||
)
|
||||
|
||||
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"m_guid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"m_recordingDuration": 10000,
|
||||
"m_baseUrl": "http://localhost:3000"
|
||||
"m_baseUrl": "http://localhost:3000",
|
||||
"m_polling": 120,
|
||||
"m_jitter": 10
|
||||
}
|
||||
39
gen_device_csr.sh
Normal file
39
gen_device_csr.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <DEVICE_GUID>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GUID="$1"
|
||||
|
||||
# Output files
|
||||
KEY_FILE="device_${GUID}.key"
|
||||
CSR_FILE="device_${GUID}.csr"
|
||||
CONF_FILE="csr_${GUID}.conf"
|
||||
|
||||
# Generate config for CSR
|
||||
cat > "$CONF_FILE" <<EOF
|
||||
[ req ]
|
||||
default_md = sha256
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
req_extensions = req_ext
|
||||
|
||||
[ dn ]
|
||||
CN = $GUID
|
||||
|
||||
[ req_ext ]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[ alt_names ]
|
||||
URI.1 = urn:device:$GUID
|
||||
EOF
|
||||
|
||||
# Generate private key
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out "$KEY_FILE"
|
||||
chmod 600 "$KEY_FILE"
|
||||
|
||||
# Generate CSR
|
||||
openssl req -new -key "$KEY_FILE" -out "$CSR_FILE" -config "$CONF_FILE"
|
||||
11
key-load.service
Normal file
11
key-load.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Load IoT TLS key into kernel keyring
|
||||
After=network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/load-iot-key.sh
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
20
load-iot-key.sh
Normal file
20
load-iot-key.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CPU_SERIAL=$(awk '/Serial/ {print $3}' /proc/cpuinfo)
|
||||
KEK=$(echo -n "$CPU_SERIAL" | \
|
||||
openssl dgst -sha256 -hmac "server-provided-salt" | \
|
||||
awk '{print $2}')
|
||||
|
||||
# Decrypt into tmpfs
|
||||
mkdir -p /run/iot
|
||||
openssl enc -d -aes-256-gcm -pbkdf2 \
|
||||
-pass pass:$KEK \
|
||||
-in /etc/iot/keys/device.key.enc \
|
||||
-out /run/iot/device.key
|
||||
|
||||
# Load into kernel keyring (root-only key)
|
||||
keyctl padd user iot-client-key @s < /run/iot/device.key
|
||||
|
||||
# Securely erase plaintext
|
||||
shred -u /run/iot/device.key
|
||||
@@ -15,9 +15,11 @@ public:
|
||||
std::string m_guid;
|
||||
unsigned long long m_recordingDuration = 0;
|
||||
std::string m_baseUrl;
|
||||
unsigned int m_polling = 120;
|
||||
unsigned int m_jitter = 10;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Config, m_guid, m_recordingDuration, m_baseUrl )
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Config, m_guid, m_recordingDuration, m_baseUrl, m_polling, m_jitter)
|
||||
|
||||
class ConfigService {
|
||||
std::shared_ptr<Config> m_config;
|
||||
@@ -52,6 +54,29 @@ public:
|
||||
return this->m_config->m_baseUrl;
|
||||
}
|
||||
|
||||
void SetBaseUrl(std::string newBaseUrl) {
|
||||
this->m_config->m_baseUrl = newBaseUrl;
|
||||
this->RewriteConfig();
|
||||
}
|
||||
|
||||
[[nodiscard]] unsigned int GetPollingInterwall() {
|
||||
return this->m_config->m_polling;
|
||||
}
|
||||
|
||||
void SetPollingInterwall(unsigned int newPollInterwall) {
|
||||
this->m_config->m_polling = newPollInterwall;
|
||||
this->RewriteConfig();
|
||||
}
|
||||
|
||||
[[nodscard]] unsigned int GetJitter() {
|
||||
return this->m_config->m_jitter;
|
||||
}
|
||||
|
||||
void SetJitter(unsigned int newJitter) {
|
||||
this->m_config->m_jitter = newJitter;
|
||||
this -> RewriteConfig();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string ReadFile( const std::string& path ) {
|
||||
std::fstream f;
|
||||
|
||||
295
src/Services/DeviceControlService.h
Normal file
295
src/Services/DeviceControlService.h
Normal file
@@ -0,0 +1,295 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <array>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <httplib.h> // build with CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include <sys/wait.h> // for WEXITSTATUS
|
||||
|
||||
#include "ConfigService.h"
|
||||
|
||||
namespace snoop {
|
||||
|
||||
class DeviceControlService {
|
||||
public:
|
||||
struct Task {
|
||||
uint64_t id{};
|
||||
std::string type;
|
||||
nlohmann::json payload = nlohmann::json::object();
|
||||
};
|
||||
|
||||
// Handler returns: {success, result_json_string, error_message}
|
||||
using HandlerResult = std::tuple<bool, std::string, std::string>;
|
||||
using TaskHandler = std::function<HandlerResult(const Task&)>;
|
||||
|
||||
struct Handlers {
|
||||
// Fill any you implement. Unset => default “not implemented”.
|
||||
TaskHandler onStartStream;
|
||||
TaskHandler onStopStream;
|
||||
TaskHandler onStartRecording;
|
||||
TaskHandler onStopRecording;
|
||||
TaskHandler onUpdateConfig;
|
||||
TaskHandler onSetDeepSleep;
|
||||
};
|
||||
|
||||
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() {
|
||||
m_stop = true;
|
||||
if (m_thread.joinable()) m_thread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ConfigService> m_cfg;
|
||||
Handlers m_handlers;
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_stop{false};
|
||||
|
||||
// --- 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<char, 4096> 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;
|
||||
}
|
||||
|
||||
struct Url {
|
||||
std::string scheme; // http/https
|
||||
std::string host; // hostname or ip
|
||||
int port = 0; // default if 0
|
||||
};
|
||||
|
||||
static Url ParseBase(const std::string& base) {
|
||||
// very small parser: scheme://host[:port]
|
||||
std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)");
|
||||
std::smatch m;
|
||||
if (!std::regex_match(base, m, re)) {
|
||||
throw std::runtime_error("Invalid base URL for DeviceControlService: " + base);
|
||||
}
|
||||
Url u;
|
||||
u.scheme = m[1].str();
|
||||
u.host = m[2].str();
|
||||
u.port = m[3].matched ? std::stoi(m[3].str()) : (u.scheme == "https" ? 443 : 80);
|
||||
return u;
|
||||
}
|
||||
|
||||
// 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-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;
|
||||
}
|
||||
|
||||
// Create HTTPS client configured for mTLS (or HTTP if base is http)
|
||||
std::unique_ptr<httplib::Client> MakeClient(const Url& u,
|
||||
const std::filesystem::path& ca,
|
||||
const std::filesystem::path& crt,
|
||||
const std::filesystem::path& key) {
|
||||
if (u.scheme == "https") {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
auto cli = std::make_unique<httplib::SSLClient>(u.host.c_str(), u.port);
|
||||
cli->enable_server_certificate_verification(true);
|
||||
cli->set_ca_cert_path(ca.string().c_str());
|
||||
cli->set_client_cert_file(crt.string().c_str(), key.string().c_str(), nullptr);
|
||||
// Recommended timeouts for long polling-ish flows
|
||||
cli->set_connection_timeout(10);
|
||||
cli->set_read_timeout(60);
|
||||
cli->set_write_timeout(60);
|
||||
return cli;
|
||||
#else
|
||||
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
|
||||
void SleepWithJitterOnce(std::mt19937& rng, int baseSec, int jitterSec) {
|
||||
if (baseSec < 0) baseSec = 0;
|
||||
if (jitterSec < 0) jitterSec = 0;
|
||||
std::uniform_int_distribution<int> dist(-jitterSec, +jitterSec);
|
||||
int delay = baseSec + dist(rng);
|
||||
if (delay < 0) delay = 0;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(delay));
|
||||
}
|
||||
|
||||
// default fallback if handler is not provided
|
||||
static HandlerResult NotImplemented(const Task& t) {
|
||||
std::string msg = "Handler not implemented for type: " + t.type;
|
||||
spdlog::warn("{}", msg);
|
||||
return {false, "{}", msg};
|
||||
}
|
||||
|
||||
TaskHandler ResolveHandler(std::string_view type) const {
|
||||
if (type == "start_stream") return m_handlers.onStartStream ? m_handlers.onStartStream : NotImplemented;
|
||||
if (type == "stop_stream") return m_handlers.onStopStream ? m_handlers.onStopStream : NotImplemented;
|
||||
if (type == "start_recording") return m_handlers.onStartRecording ? m_handlers.onStartRecording : NotImplemented;
|
||||
if (type == "stop_recording") return m_handlers.onStopRecording ? m_handlers.onStopRecording : NotImplemented;
|
||||
if (type == "update_config") return m_handlers.onUpdateConfig ? m_handlers.onUpdateConfig : NotImplemented;
|
||||
if (type == "set_deep_sleep") return m_handlers.onSetDeepSleep ? m_handlers.onSetDeepSleep : NotImplemented;
|
||||
return NotImplemented;
|
||||
}
|
||||
|
||||
static std::vector<Task> ParseTasks(const std::string& body) {
|
||||
// server might return single object or array
|
||||
std::vector<Task> tasks;
|
||||
auto j = nlohmann::json::parse(body);
|
||||
|
||||
auto push_one = [&](const nlohmann::json& x){
|
||||
Task t;
|
||||
t.id = x.value("id", 0);
|
||||
t.type = x.value("type", "");
|
||||
try {
|
||||
if (x.contains("payload")) {
|
||||
const auto& raw = x.at("payload");
|
||||
if (raw.is_string()) {
|
||||
// payload is quoted JSON string -> parse inner if valid, else keep as string
|
||||
try { t.payload = nlohmann::json::parse(raw.get<std::string>()); }
|
||||
catch (...) { t.payload = raw.get<std::string>(); }
|
||||
} else if (raw.is_object() || raw.is_array()) {
|
||||
t.payload = raw;
|
||||
} else {
|
||||
t.payload = nlohmann::json::object();
|
||||
}
|
||||
}
|
||||
} catch (...) { t.payload = nlohmann::json::object(); }
|
||||
tasks.push_back(std::move(t));
|
||||
};
|
||||
|
||||
if (j.is_array()) {
|
||||
for (auto& it : j) push_one(it);
|
||||
} else if (j.is_object()) {
|
||||
push_one(j);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
void PostResult(httplib::Client& cli, const std::string& guid,
|
||||
uint64_t taskId, bool success,
|
||||
const std::string& resultJson, const std::string& err) {
|
||||
nlohmann::json dto = {
|
||||
{"taskId", taskId},
|
||||
{"success", success},
|
||||
{"result", resultJson.empty() ? "{}" : resultJson},
|
||||
{"error", err}
|
||||
};
|
||||
std::string path = "/api/tasks/" + guid;
|
||||
auto res = cli.Post(path.c_str(), dto.dump(), "application/json");
|
||||
if (!res) {
|
||||
spdlog::error("POST {} failed (no response)", path);
|
||||
return;
|
||||
}
|
||||
spdlog::info("POST {} -> HTTP {}", path, res->status);
|
||||
}
|
||||
|
||||
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 {
|
||||
tmpKey = ExtractClientKeyFromKernelKeyring();
|
||||
} catch (const std::exception& e) {
|
||||
spdlog::error("Key extraction failed: {}", e.what());
|
||||
SleepWithJitterOnce(rng, m_cfg->GetPollingSeconds(), m_cfg->GetJitterSeconds());
|
||||
continue;
|
||||
}
|
||||
|
||||
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->GetPollingSeconds(), m_cfg->GetJitterSeconds());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace snoop
|
||||
203
src/Services/EnrollmentService.h
Normal file
203
src/Services/EnrollmentService.h
Normal file
@@ -0,0 +1,203 @@
|
||||
// src/Services/EnrollmentService.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <regex>
|
||||
#include <memory>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <httplib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "ConfigService.h"
|
||||
|
||||
namespace snoop {
|
||||
|
||||
class EnrollmentService {
|
||||
std::shared_ptr<ConfigService> m_cfg;
|
||||
|
||||
static std::string Exec(const std::string& cmd) {
|
||||
std::array<char, 4096> 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());
|
||||
}
|
||||
auto rc = pclose(pipe);
|
||||
int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : rc;
|
||||
if (exitCode != 0)
|
||||
spdlog::warn("Command '{}' exited with code {}", cmd, exitCode);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void WriteFile(const std::filesystem::path& p, const std::string& data, bool createParents=true) {
|
||||
if (createParents) std::filesystem::create_directories(p.parent_path());
|
||||
std::ofstream f(p, std::ios::binary);
|
||||
if (!f) throw std::runtime_error("Cannot open file for write: " + p.string());
|
||||
f.write(data.data(), (std::streamsize)data.size());
|
||||
}
|
||||
|
||||
static std::string ReadFile(const std::filesystem::path& p) {
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f) throw std::runtime_error("Cannot open file for read: " + p.string());
|
||||
std::ostringstream ss; ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
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 ParseCpuSerial() {
|
||||
// Default for “empty” case: 12 times '9'
|
||||
std::string fallback(12, '9');
|
||||
|
||||
std::ifstream f("/proc/cpuinfo");
|
||||
if (!f) {
|
||||
spdlog::warn("/proc/cpuinfo not available, using fallback serial");
|
||||
return fallback;
|
||||
}
|
||||
std::string line;
|
||||
std::regex re(R"(^\s*Serial\s*:\s*([0-9A-Fa-f]+)\s*$)");
|
||||
while (std::getline(f, line)) {
|
||||
std::smatch m;
|
||||
if (std::regex_match(line, m, re) && m.size() == 2) {
|
||||
auto serial = Trim(m[1].str());
|
||||
if (!serial.empty()) return serial;
|
||||
}
|
||||
}
|
||||
spdlog::warn("CPU Serial not found, using fallback");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit EnrollmentService(std::shared_ptr<ConfigService> cfg) : m_cfg(std::move(cfg)) {}
|
||||
|
||||
// Returns true if enrollment was performed now; false if it was already done
|
||||
bool EnsureEnrolled() {
|
||||
const std::string guid = m_cfg->GetGuid();
|
||||
const std::filesystem::path keystoreDir = "/etc/iot/keys";
|
||||
const auto encKeyPath = keystoreDir / "device.key.enc";
|
||||
const auto certPath = keystoreDir / "device.crt.pem";
|
||||
const auto caPath = keystoreDir / "issuing_ca.pem";
|
||||
const auto chainPath = keystoreDir / "ca_chain.pem";
|
||||
|
||||
// If we already have encrypted key + cert, assume done
|
||||
if (std::filesystem::exists(encKeyPath) &&
|
||||
std::filesystem::exists(certPath)) {
|
||||
spdlog::info("Enrollment already completed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
spdlog::info("Starting first-run enrollment...");
|
||||
|
||||
// 1) Run gen_device_csr.sh <GUID>
|
||||
{
|
||||
// Assumes script is in the working dir or in PATH
|
||||
const std::string cmd = "bash ./gen_device_csr.sh " + guid;
|
||||
spdlog::info("Executing: {}", cmd);
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("gen_device_csr.sh output:\n{}", out);
|
||||
}
|
||||
const std::string keyName = "device_" + guid + ".key";
|
||||
const std::string csrName = "device_" + guid + ".csr";
|
||||
|
||||
if (!std::filesystem::exists(keyName) || !std::filesystem::exists(csrName)) {
|
||||
throw std::runtime_error("CSR or key was not generated by gen_device_csr.sh");
|
||||
}
|
||||
|
||||
// 2) CPU serial (awk '/Serial/ {print $3}' /proc/cpuinfo), if empty -> 9 * 12
|
||||
const std::string cpuSerial = ParseCpuSerial();
|
||||
spdlog::info("CPU_SERIAL = {}", cpuSerial);
|
||||
|
||||
// 3) KEK = HMAC-SHA256(cpuSerial, key=GUID) (bash equiv used)
|
||||
std::string kek;
|
||||
{
|
||||
// careful to avoid trailing newline
|
||||
const std::string cmd = "printf %s \"" + cpuSerial + "\""
|
||||
" | openssl dgst -sha256 -hmac \"" + guid + "\" | awk '{print $2}'";
|
||||
spdlog::info("Deriving KEK with HMAC-SHA256");
|
||||
kek = Trim(Exec(cmd));
|
||||
if (kek.empty()) throw std::runtime_error("Failed to derive KEK");
|
||||
spdlog::debug("KEK (hex) = {}", kek);
|
||||
}
|
||||
|
||||
// 4) Encrypt the private key to /etc/iot/keys/device.key.enc using KEK; shred original
|
||||
{
|
||||
std::filesystem::create_directories(keystoreDir);
|
||||
const std::string cmd =
|
||||
"openssl enc -aes-256-gcm -pbkdf2 -salt "
|
||||
"-pass pass:" + kek + " "
|
||||
"-in " + keyName + " "
|
||||
"-out " + encKeyPath.string() + " "
|
||||
"&& shred -u " + keyName;
|
||||
spdlog::info("Encrypting private key and shredding plaintext...");
|
||||
auto out = Exec(cmd);
|
||||
spdlog::debug("openssl enc output:\n{}", out);
|
||||
|
||||
if (!std::filesystem::exists(encKeyPath)) {
|
||||
throw std::runtime_error("Encrypted key not created: " + encKeyPath.string());
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Send CSR to /enroll/:guid as multipart form, field `json: {"csr":"<...>"}`
|
||||
std::string csrPem = ReadFile(csrName);
|
||||
{
|
||||
httplib::Client cli(m_cfg->GetBaseUrl());
|
||||
httplib::MultipartFormDataItems items = {
|
||||
{ "json", std::string("{\"csr\":\"") + EscapeJsonForOneLine(csrPem) + "\"}", "enroll.json", "application/json" }
|
||||
};
|
||||
|
||||
const std::string path = "/api/enroll/" + guid;
|
||||
spdlog::info("POST {} (multipart)", path);
|
||||
auto res = cli.Post(path.c_str(), items);
|
||||
if (!res) throw std::runtime_error("Enroll request failed (no response)");
|
||||
spdlog::info("Enroll response status: {}", res->status);
|
||||
if (res->status != 200 && res->status != 201)
|
||||
throw std::runtime_error("Enroll failed: HTTP " + std::to_string(res->status));
|
||||
|
||||
// 6) Expect JSON: { "certificate": "...", "issuing_ca":"...", "ca_chain":"..." }
|
||||
nlohmann::json j = nlohmann::json::parse(res->body);
|
||||
if (!j.contains("certificate"))
|
||||
throw std::runtime_error("Enroll response missing 'certificate'");
|
||||
|
||||
WriteFile(certPath, j["certificate"].get<std::string>());
|
||||
if (j.contains("issuing_ca")) WriteFile(caPath, j["issuing_ca"].get<std::string>());
|
||||
if (j.contains("ca_chain")) WriteFile(chainPath, j["ca_chain"].get<std::string>());
|
||||
spdlog::info("Enrollment artifacts saved to {}", keystoreDir.string());
|
||||
}
|
||||
|
||||
// (Optional) cleanup CSR after successful enrollment
|
||||
try { std::filesystem::remove(csrName); } catch (...) {}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// very small helper to pack PEM into JSON string value (single-line, escaped quotes & newlines)
|
||||
static std::string EscapeJsonForOneLine(const std::string& in) {
|
||||
std::string out; out.reserve(in.size()*2);
|
||||
for (char c : in) {
|
||||
switch (c) {
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\"': out += "\\\""; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': /* skip */ break;
|
||||
default: out += c; break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace snoop
|
||||
130
src/main.cpp
130
src/main.cpp
@@ -8,6 +8,8 @@
|
||||
#include "Services/AudioStreamService.h"
|
||||
#include "Services/AudioWriterService.h"
|
||||
#include "Services/ConfigService.h"
|
||||
#include "Services/EnrollmentService.h"
|
||||
#include "Services/DeviceControlService.h"
|
||||
|
||||
#ifdef USE_ALSA_ADAPTER
|
||||
#include "AudioAdapters/AlsaAudioAdapter.h"
|
||||
@@ -17,42 +19,122 @@ using AudioAdapter = snoop::AlsaAudioAdapter;
|
||||
using AudioAdapter = snoop::PortAudioAdapter;
|
||||
#endif
|
||||
|
||||
|
||||
// sudo apt-get install libasound2-dev
|
||||
|
||||
namespace snoop
|
||||
{
|
||||
template <AudioEncoderConcept TAudioEncoder, AudioWriterConcept TAudioWriter>
|
||||
void Main()
|
||||
{
|
||||
try
|
||||
{
|
||||
int sampleRate = 48000;
|
||||
int channels = 1;
|
||||
int framesPerBuffer = 2880;
|
||||
|
||||
namespace snoop {
|
||||
template<AudioEncoderConcept TAudioEncoder, AudioWriterConcept TAudioWriter>
|
||||
void Main() {
|
||||
try {
|
||||
int sampleRate = 48000;
|
||||
int channels = 1;
|
||||
int framesPerBuffer = 2880;
|
||||
auto configService = std::make_shared<ConfigService>("config.json");
|
||||
|
||||
auto configService = std::make_shared<ConfigService>( "config.json" );
|
||||
// ---- FIRST-RUN ENROLLMENT ----
|
||||
{
|
||||
EnrollmentService enroll(configService);
|
||||
const bool didEnroll = enroll.EnsureEnrolled();
|
||||
if (didEnroll)
|
||||
{
|
||||
spdlog::info("First-run enrollment completed.");
|
||||
}
|
||||
}
|
||||
// ------------------------------
|
||||
{
|
||||
snoop::DeviceControlService::Handlers handlers{};
|
||||
|
||||
auto sioClient = std::make_shared<sio::client>();
|
||||
sioClient->connect( configService->GetBaseUrl() );
|
||||
handlers.onStartStream = [](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("start_stream payload: {}", t.payload.dump());
|
||||
// TODO: start your streaming pipeline using payload["whipUrl"], etc.
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"status":"started"})", ""};
|
||||
};
|
||||
handlers.onStopStream = [](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("stop_stream payload: {}", t.payload.dump());
|
||||
// TODO: stop streaming
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"status":"stopped"})", ""};
|
||||
};
|
||||
handlers.onStartRecording = [&](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("start_recording payload: {}", t.payload.dump());
|
||||
// TODO: if you gate writes in AudioWriterService, flip to recording mode here
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"recording":"on"})", ""};
|
||||
};
|
||||
handlers.onStopRecording = [&](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("stop_recording payload: {}", t.payload.dump());
|
||||
// TODO: flip to recording off
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"recording":"off"})", ""};
|
||||
};
|
||||
handlers.onUpdateConfig = [&](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("update_config payload: {}", t.payload.dump());
|
||||
try
|
||||
{
|
||||
if (t.payload.contains("duration"))
|
||||
{
|
||||
auto ms = static_cast<unsigned long long>(t.payload.at("duration").get<int>() * 1000ULL);
|
||||
configService->SetRecordingDuration(ms);
|
||||
}
|
||||
if (t.payload.contains("sleep"))
|
||||
{
|
||||
configService->SetPollingInterwall(t.payload.at("sleep").get<int>());
|
||||
}
|
||||
if (t.payload.contains("jitter"))
|
||||
{
|
||||
configService->SetJitter(t.payload.at("jitter").get<int>());
|
||||
}
|
||||
if (t.payload.contains("endpoint"))
|
||||
{
|
||||
configService->SetBaseUrl(t.payload.at("endpoint").get<std::string>());
|
||||
}
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"updated":true})", ""};
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()};
|
||||
}
|
||||
};
|
||||
handlers.onSetDeepSleep = [](const snoop::DeviceControlService::Task &t)
|
||||
{
|
||||
spdlog::info("set_deep_sleep payload: {}", t.payload.dump());
|
||||
// TODO: use platform power management or just extend sleep window
|
||||
return snoop::DeviceControlService::HandlerResult{true, R"({"sleep":"scheduled"})", ""};
|
||||
};
|
||||
|
||||
auto writerService = std::make_shared<AudioWriterService>( configService, "records" );
|
||||
AudioStreamService streamer( sioClient, configService->GetGuid() );
|
||||
TAudioEncoder encoder( sampleRate, channels, framesPerBuffer, [&]( auto input, auto size ) -> int {
|
||||
static std::unique_ptr<snoop::DeviceControlService> g_taskSvc;
|
||||
g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers);
|
||||
}
|
||||
auto sioClient = std::make_shared<sio::client>();
|
||||
sioClient->connect(configService->GetBaseUrl());
|
||||
|
||||
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
|
||||
AudioStreamService streamer(sioClient, configService->GetGuid());
|
||||
TAudioEncoder encoder(sampleRate, channels, framesPerBuffer, [&](auto input, auto size) -> int
|
||||
{
|
||||
streamer.SendAudioData( input, size );
|
||||
writerService->WriteAudioData( input, size, framesPerBuffer );
|
||||
return paContinue;
|
||||
} );
|
||||
return paContinue; });
|
||||
|
||||
while( true ) {
|
||||
sleep( 1000 );
|
||||
}
|
||||
|
||||
} catch( const std::exception& ex ) {
|
||||
spdlog::error( "Exception: {}", ex.what() );
|
||||
while (true)
|
||||
{
|
||||
sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (const std::exception &ex)
|
||||
{
|
||||
spdlog::error("Exception: {}", ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int main()
|
||||
{
|
||||
snoop::Main<snoop::OpusEncoder<AudioAdapter>, snoop::OggAudioWriter>();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user