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/sysroot/
|
||||||
/toolchain/build/
|
/toolchain/build/
|
||||||
|
|
||||||
|
.vscode/
|
||||||
@@ -32,6 +32,8 @@ set( HEADERS
|
|||||||
src/Services/AudioStreamService.h
|
src/Services/AudioStreamService.h
|
||||||
src/Services/AudioWriterService.h
|
src/Services/AudioWriterService.h
|
||||||
src/Services/ConfigService.h
|
src/Services/ConfigService.h
|
||||||
|
src/Services/DeviceControlService.h
|
||||||
|
src/Services/EnrollmentService.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
add_executable( ${PROJECT_NAME} ${SOURCES} ${HEADERS} )
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"m_guid": "123e4567-e89b-12d3-a456-426614174000",
|
"m_guid": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
"m_recordingDuration": 10000,
|
"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;
|
std::string m_guid;
|
||||||
unsigned long long m_recordingDuration = 0;
|
unsigned long long m_recordingDuration = 0;
|
||||||
std::string m_baseUrl;
|
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 {
|
class ConfigService {
|
||||||
std::shared_ptr<Config> m_config;
|
std::shared_ptr<Config> m_config;
|
||||||
@@ -52,6 +54,29 @@ public:
|
|||||||
return this->m_config->m_baseUrl;
|
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:
|
private:
|
||||||
std::string ReadFile( const std::string& path ) {
|
std::string ReadFile( const std::string& path ) {
|
||||||
std::fstream f;
|
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
|
||||||
122
src/main.cpp
122
src/main.cpp
@@ -8,6 +8,8 @@
|
|||||||
#include "Services/AudioStreamService.h"
|
#include "Services/AudioStreamService.h"
|
||||||
#include "Services/AudioWriterService.h"
|
#include "Services/AudioWriterService.h"
|
||||||
#include "Services/ConfigService.h"
|
#include "Services/ConfigService.h"
|
||||||
|
#include "Services/EnrollmentService.h"
|
||||||
|
#include "Services/DeviceControlService.h"
|
||||||
|
|
||||||
#ifdef USE_ALSA_ADAPTER
|
#ifdef USE_ALSA_ADAPTER
|
||||||
#include "AudioAdapters/AlsaAudioAdapter.h"
|
#include "AudioAdapters/AlsaAudioAdapter.h"
|
||||||
@@ -17,42 +19,122 @@ using AudioAdapter = snoop::AlsaAudioAdapter;
|
|||||||
using AudioAdapter = snoop::PortAudioAdapter;
|
using AudioAdapter = snoop::PortAudioAdapter;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// sudo apt-get install libasound2-dev
|
// sudo apt-get install libasound2-dev
|
||||||
|
|
||||||
|
namespace snoop
|
||||||
namespace snoop {
|
{
|
||||||
template<AudioEncoderConcept TAudioEncoder, AudioWriterConcept TAudioWriter>
|
template <AudioEncoderConcept TAudioEncoder, AudioWriterConcept TAudioWriter>
|
||||||
void Main() {
|
void Main()
|
||||||
try {
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
int sampleRate = 48000;
|
int sampleRate = 48000;
|
||||||
int channels = 1;
|
int channels = 1;
|
||||||
int framesPerBuffer = 2880;
|
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{};
|
||||||
|
|
||||||
|
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"})", ""};
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::unique_ptr<snoop::DeviceControlService> g_taskSvc;
|
||||||
|
g_taskSvc = std::make_unique<snoop::DeviceControlService>(configService, handlers);
|
||||||
|
}
|
||||||
auto sioClient = std::make_shared<sio::client>();
|
auto sioClient = std::make_shared<sio::client>();
|
||||||
sioClient->connect( configService->GetBaseUrl() );
|
sioClient->connect(configService->GetBaseUrl());
|
||||||
|
|
||||||
auto writerService = std::make_shared<AudioWriterService>( configService, "records" );
|
auto writerService = std::make_shared<AudioWriterService>(configService, "records");
|
||||||
AudioStreamService streamer( sioClient, configService->GetGuid() );
|
AudioStreamService streamer(sioClient, configService->GetGuid());
|
||||||
TAudioEncoder encoder( sampleRate, channels, framesPerBuffer, [&]( auto input, auto size ) -> int {
|
TAudioEncoder encoder(sampleRate, channels, framesPerBuffer, [&](auto input, auto size) -> int
|
||||||
|
{
|
||||||
streamer.SendAudioData( input, size );
|
streamer.SendAudioData( input, size );
|
||||||
writerService->WriteAudioData( input, size, framesPerBuffer );
|
writerService->WriteAudioData( input, size, framesPerBuffer );
|
||||||
return paContinue;
|
return paContinue; });
|
||||||
} );
|
|
||||||
|
|
||||||
while( true ) {
|
while (true)
|
||||||
sleep( 1000 );
|
{
|
||||||
|
sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &ex)
|
||||||
|
{
|
||||||
|
spdlog::error("Exception: {}", ex.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch( const std::exception& ex ) {
|
|
||||||
spdlog::error( "Exception: {}", ex.what() );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
int main()
|
||||||
|
{
|
||||||
snoop::Main<snoop::OpusEncoder<AudioAdapter>, snoop::OggAudioWriter>();
|
snoop::Main<snoop::OpusEncoder<AudioAdapter>, snoop::OggAudioWriter>();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user