diff --git a/src/Services/AudioStreamService.h b/src/Services/AudioStreamService.h index 34b4672..b9e1b66 100644 --- a/src/Services/AudioStreamService.h +++ b/src/Services/AudioStreamService.h @@ -19,70 +19,86 @@ #include "ConfigService.h" #include "Security/TlsKeyUtil.h" -namespace snoop { +namespace snoop +{ -class AudioStreamService { - std::shared_ptr m_cfg; + class AudioStreamService + { + std::shared_ptr m_cfg; - // WHIP - std::unique_ptr m_whip; - std::mutex m_whipMutex; + // WHIP + std::unique_ptr m_whip; + std::mutex m_whipMutex; -public: - explicit AudioStreamService(std::shared_ptr cfg) - : m_cfg(std::move(cfg)) {} + public: + explicit AudioStreamService(std::shared_ptr cfg) + : m_cfg(std::move(cfg)) {} - ~AudioStreamService() { StopWhip(); } + ~AudioStreamService() { StopWhip(); } - // Feed encoded Opus; frames = PCM samples per channel represented by this Opus frame - void OnOpus(const unsigned char* opusData, size_t opusBytes, int pcmFramesPerChannel) { - std::lock_guard lk(m_whipMutex); - if (m_whip) m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel); - } - - bool StartWhip(const std::string& whipUrl, int sampleRate = 48000, int channels = 1) { - std::lock_guard lk(m_whipMutex); - if (m_whip) { - spdlog::info("WHIP already started"); - return true; - } - if (!m_cfg) { - spdlog::error("StartWhip requires ConfigService"); - return false; + // Feed encoded Opus; frames = PCM samples per channel represented by this Opus frame + void OnOpus(const unsigned char *opusData, size_t opusBytes, int pcmFramesPerChannel) + { + std::lock_guard lk(m_whipMutex); + if (m_whip) + { + m_whip->PushOpus(opusData, opusBytes, pcmFramesPerChannel); + } } - // certs from enrollment - std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; - if (!std::filesystem::exists(ca)) ca = "/etc/iot/keys/ca_chain.pem"; - std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; + bool StartWhip(const std::string &whipUrl, int sampleRate = 48000, int channels = 1) + { + std::lock_guard lk(m_whipMutex); + if (m_whip) + { + spdlog::info("WHIP already started"); + return true; + } + if (!m_cfg) + { + spdlog::error("StartWhip requires ConfigService"); + return false; + } - WhipClient::Params p{ - .whipUrl = whipUrl, // may include ?token=..., client will normalize path and set Bearer - .caPath = ca.string(), - .crtPath = crt.string(), - .sampleRate = sampleRate, - .channels = channels - }; + // certs from enrollment + std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; + if (!std::filesystem::exists(ca)) + { + ca = "/etc/iot/keys/ca_chain.pem"; + } + std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; - m_whip = std::make_unique(p); - try { - m_whip->Start(); - spdlog::info("WHIP started"); - return true; - } catch (const std::exception& e) { - spdlog::error("WHIP start failed: {}", e.what()); - m_whip.reset(); - return false; + WhipClient::Params p{ + .whipUrl = whipUrl, // may include ?token=..., client will normalize path and set Bearer + .caPath = ca.string(), + .crtPath = crt.string(), + .sampleRate = sampleRate, + .channels = channels}; + + m_whip = std::make_unique(p); + try + { + m_whip->Start(); + spdlog::info("WHIP started"); + return true; + } + catch (const std::exception &e) + { + spdlog::error("WHIP start failed: {}", e.what()); + m_whip.reset(); + return false; + } } - } - void StopWhip() { - std::lock_guard lk(m_whipMutex); - if (m_whip) { - m_whip->Stop(); - m_whip.reset(); + void StopWhip() + { + std::lock_guard lk(m_whipMutex); + if (m_whip) + { + m_whip->Stop(); + m_whip.reset(); + } } - } -}; + }; } // namespace snoop diff --git a/src/Services/AudioWriterService.h b/src/Services/AudioWriterService.h index b7d2f31..670cacb 100644 --- a/src/Services/AudioWriterService.h +++ b/src/Services/AudioWriterService.h @@ -73,9 +73,13 @@ namespace snoop { this->m_isIntermission = true; if (this->m_writingThread.joinable()) + { this->m_writingThread.join(); + } if (this->m_uploadThread.joinable()) + { this->m_uploadThread.join(); + } } // -------- Public control API (called from DeviceControlService handlers) -------- @@ -116,7 +120,9 @@ namespace snoop void WriteAudioData(const char *data, size_t size, size_t frames) { if (!m_recordingEnabled.load()) + { return; + } this->m_oggWriter->Write(data, size, frames); } @@ -176,7 +182,6 @@ namespace snoop { if (u.scheme == "https") { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT auto cli = std::make_unique(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string()); cli->enable_server_certificate_verification(false); cli->set_ca_cert_path(ca.string().c_str()); @@ -184,9 +189,6 @@ namespace snoop cli->set_read_timeout(120); cli->set_write_timeout(120); return cli; -#else - throw std::runtime_error("HTTPS baseUrl but CPPHTTPLIB_OPENSSL_SUPPORT is not enabled"); -#endif } } @@ -213,8 +215,6 @@ namespace snoop // still ok — reuse return true; } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT try { auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring(); @@ -239,12 +239,6 @@ namespace snoop 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) ----------------------------- @@ -293,7 +287,9 @@ namespace snoop auto currentRecordDuration = std::chrono::duration_cast(now.time_since_epoch()).count() - this->m_currentRecordStartedAt; if (currentRecordDuration >= segDurationMs) + { break; + } std::this_thread::sleep_for(std::chrono::milliseconds(200)); } @@ -354,7 +350,9 @@ namespace snoop for (const auto &entry : std::filesystem::directory_iterator(this->m_queueDirectoryPath)) { if (entry.is_regular_file()) + { files.push_back(entry.path()); + } } } catch (const std::exception &e) @@ -422,8 +420,9 @@ namespace snoop spdlog::info("SendRecordedFile (mTLS): {}", filepath); std::ifstream ifs(filepath, std::ios::binary); if (!ifs) + { throw std::runtime_error("Failed to open file: " + filepath); - + } std::vector buffer((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); // Multipart form: file + guid + times (same fields as before) diff --git a/src/Services/DeviceControlService.h b/src/Services/DeviceControlService.h index 2183f09..58d917a 100644 --- a/src/Services/DeviceControlService.h +++ b/src/Services/DeviceControlService.h @@ -49,6 +49,7 @@ namespace snoop TaskHandler onStopRecording; TaskHandler onUpdateConfig; TaskHandler onSetDeepSleep; + TaskHandler onRenewCert; }; struct Controls @@ -68,7 +69,9 @@ namespace snoop { m_stop = true; if (m_thread.joinable()) + { m_thread.join(); + } } void ArmDeepSleep(long long startMs, long long stopMs) @@ -106,7 +109,9 @@ namespace snoop // accept with optional fractional seconds if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d%n", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &n) != 6) - return std::nullopt; + { + return std::nullopt; + } tm.tm_year -= 1900; tm.tm_mon -= 1; const char *p = s.c_str() + n; @@ -114,14 +119,19 @@ namespace snoop { // consume fractional ++p; - while (isdigit(*p)) + while (isdigit(*p)) { ++p; // ignore exact fraction + } } if (*p != 'Z') + { return std::nullopt; + } time_t tt = timegm(&tm); if (tt < 0) + { return std::nullopt; + } return (long long)tt * 1000LL; } @@ -183,7 +193,9 @@ namespace snoop auto sMs = ParseRfc3339UtcToMs(j["start"].get()); auto eMs = ParseRfc3339UtcToMs(j["stop"].get()); if (sMs && eMs) + { return *eMs; // caller will use start separately + } } if (j.contains("start") && j["start"].is_string() && j["start"].get() == "now" && j.contains("for_s")) @@ -204,7 +216,6 @@ namespace snoop { if (u.scheme == "https") { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT auto cli = std::make_unique(u.host.c_str(), u.port, crt.string().c_str(), key.string().c_str(), std::string()); cli->enable_server_certificate_verification(false); cli->set_ca_cert_path(ca.string().c_str()); @@ -221,13 +232,6 @@ namespace snoop spdlog::debug("[mTLS debug] SSLClient initialized OK for host={} port={}", u.host, u.port); } return cli; -#else - (void)u; - (void)ca; - (void)crt; - (void)key; - throw std::runtime_error("CPPHTTPLIB_OPENSSL_SUPPORT not enabled but https URL provided"); -#endif } } @@ -235,13 +239,19 @@ namespace snoop void SleepWithJitterOnce(std::mt19937 &rng, int baseSec, int jitterSec) { if (baseSec < 0) + { baseSec = 0; + } if (jitterSec < 0) + { jitterSec = 0; + } std::uniform_int_distribution dist(-jitterSec, +jitterSec); int delay = baseSec + dist(rng); if (delay < 0) + { delay = 0; + } std::this_thread::sleep_for(std::chrono::seconds(delay)); } @@ -271,6 +281,8 @@ namespace snoop return m_handlers.onUpdateConfig ? m_handlers.onUpdateConfig : NotImplemented; if (type == "set_deep_sleep") return m_handlers.onSetDeepSleep ? m_handlers.onSetDeepSleep : NotImplemented; + if (type == "renew_cert") + return m_handlers.onRenewCert ? m_handlers.onRenewCert : NotImplemented; return NotImplemented; } @@ -282,9 +294,13 @@ namespace snoop auto to_id = [](const nlohmann::json &v) -> uint64_t { if (v.is_number_unsigned()) + { return v.get(); + } if (v.is_number_integer()) + { return static_cast(v.get()); + } if (v.is_string()) { try @@ -302,10 +318,13 @@ namespace snoop { Task t; if (x.contains("id")) + { t.id = to_id(x["id"]); + } if (x.contains("type") && x["type"].is_string()) + { t.type = x["type"].get(); - + } // payload can be stringified JSON or object try { @@ -339,9 +358,13 @@ namespace snoop } if (t.id == 0) + { spdlog::warn("Task without valid id: {}", x.dump()); + } if (t.type.empty()) + { spdlog::warn("Task without type: {}", x.dump()); + } out.push_back(std::move(t)); }; @@ -406,9 +429,13 @@ namespace snoop spdlog::info("Entering deep sleep until {} ms", stopMs); // Stop stream & recording immediately if (m_controls.stopStreamNow) + { m_controls.stopStreamNow(); + } if (m_controls.stopRecordingNow) + { m_controls.stopRecordingNow(); + } } bool EnsureTaskClient() @@ -433,7 +460,6 @@ namespace snoop return true; } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT try { auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring(); @@ -459,12 +485,6 @@ namespace snoop 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() diff --git a/src/Services/EnrollmentService.h b/src/Services/EnrollmentService.h index ed0b182..5f8bea6 100644 --- a/src/Services/EnrollmentService.h +++ b/src/Services/EnrollmentService.h @@ -15,6 +15,10 @@ #include #include +#include +#include +#include + #include "ConfigService.h" namespace snoop @@ -251,6 +255,103 @@ namespace snoop return true; } + // Renew cert either because it's about to expire (force=false) or + // because server commanded us (force=true). + // Returns true if renew was done and new files were written. + bool RenewCertificate(bool force = false) + { + const std::string guid = m_cfg->GetGuid(); + const std::filesystem::path keystoreDir = "/etc/iot/keys"; + const auto certPath = keystoreDir / "device.crt.pem"; + const auto caPath = keystoreDir / "issuing_ca.pem"; + const auto chainPath = keystoreDir / "ca_chain.pem"; + + // ----- 1) expiry check (case 1) ----- + if (!force) + { + if (!std::filesystem::exists(certPath)) + { + spdlog::warn("RenewCertificate: cert not found, skip (not forced)"); + return false; + } + if (!IsCertExpiring(certPath, 1)) + { + spdlog::info("RenewCertificate: cert is fine, skip"); + return false; + } + spdlog::info("RenewCertificate: cert expiring -> will renew"); + } + else + { + spdlog::info("RenewCertificate: forced renew"); + } + + // ----- 2) re-generate CSR via your bash script ----- + { + const std::string cmd = "bash ./gen_device_csr.sh " + guid; + spdlog::info("Executing (renew): {}", cmd); + auto out = Exec(cmd); + spdlog::debug("gen_device_csr.sh (renew) output:\n{}", out); + } + const std::string csrName = "device_" + guid + ".csr"; + if (!std::filesystem::exists(csrName)) + { + throw std::runtime_error("Renew: CSR was not generated"); + } + std::string csrPem = ReadFile(csrName); + + // ----- 3) build mTLS client using current device cert + keyring key ----- + const std::string base = m_cfg->GetBaseUrl(); + auto pu = ParseBaseUrl(base); + auto cli = MakeMtlsClient(pu); + + // ----- 4) send multipart to /api/renew/:guid ----- + const std::string path = "/api/renew/" + guid; + httplib::MultipartFormDataItems items = { + {"csr", csrPem, "request.csr", "text/plain"}}; + + spdlog::info("POST {} (multipart, mTLS)", path); + + auto res = cli->Post(path.c_str(), items); + + if (!res) + throw std::runtime_error("Renew request failed (no response)"); + spdlog::info("Renew response status: {}", res->status); + if (res->status != 200) + throw std::runtime_error("Renew failed: HTTP " + std::to_string(res->status)); + + nlohmann::json j = nlohmann::json::parse(res->body); + + if (!j.contains("certificate") || !j["certificate"].is_string()) + throw std::runtime_error("Renew response missing or invalid 'certificate'"); + WriteFile(certPath, j["certificate"].get()); + + if (j.contains("issuing_ca") && j["issuing_ca"].is_string()) + { + WriteFile(caPath, j["issuing_ca"].get()); + } + + if (j.contains("ca_chain")) + { + std::string chainPem; + if (j["ca_chain"].is_string()) + chainPem = j["ca_chain"].get(); + else if (j["ca_chain"].is_array()) + chainPem = JoinPemArray(j["ca_chain"]); + else + throw std::runtime_error("'ca_chain' must be string or array"); + WriteFile(chainPath, chainPem); + } + + if (j.contains("old_serial") && j["old_serial"].is_string()) + { + spdlog::info("Renew: server says old_serial={}", j["old_serial"].get()); + } + + spdlog::info("RenewCertificate: success"); + 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) @@ -298,15 +399,138 @@ namespace snoop { const auto &v = arr.at(i); if (!v.is_string()) + { throw std::runtime_error("ca_chain element is not string"); + } if (!out.empty() && out.back() != '\n') + { out.push_back('\n'); + } out += v.get(); if (out.back() != '\n') + { out.push_back('\n'); + } } return out; } + + static bool IsCertExpiring(const std::filesystem::path &certPath, int daysThreshold) + { + // we will use raw OpenSSL to read the PEM and check notAfter + FILE *f = fopen(certPath.string().c_str(), "r"); + if (!f) + { + spdlog::warn("IsCertExpiring: cannot open cert {}", certPath.string()); + return false; + } + X509 *cert = PEM_read_X509(f, nullptr, nullptr, nullptr); + fclose(f); + if (!cert) + { + spdlog::warn("IsCertExpiring: cannot parse X509 from {}", certPath.string()); + return false; + } + const ASN1_TIME *notAfter = X509_get0_notAfter(cert); + if (!notAfter) + { + spdlog::warn("IsCertExpiring: no notAfter in cert"); + X509_free(cert); + return false; + } + + // current time + time_t now_t = time(nullptr); + // convert ASN1_TIME → time_t diff + int days = 0, secs = 0; + // OpenSSL has ASN1_TIME_diff in newer versions + if (ASN1_TIME_diff(&days, &secs, nullptr, notAfter) == 0) + { + spdlog::warn("IsCertExpiring: ASN1_TIME_diff failed"); + X509_free(cert); + return false; + } + X509_free(cert); + + // days can be negative -> already expired + spdlog::info("IsCertExpiring: certificate expires in {} days and {} seconds", days, secs); + if (days < 0) + { + return true; // already expired + } + if (days == 0 && secs <= 0) + { + return true; + } + return days <= daysThreshold; + } + + struct ParsedUrl + { + std::string scheme; + std::string host; + int port; + }; + + static ParsedUrl ParseBaseUrl(const std::string &base) + { + // same simple regex as in DeviceControlService + std::regex re(R"(^\s*(https?)://([^/:]+)(?::(\d+))?\s*$)"); + std::smatch m; + if (!std::regex_match(base, m, re)) + { + throw std::runtime_error("EnrollmentService: invalid base URL: " + base); + } + ParsedUrl 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; + } + + // create SSL client with: CA from /etc/iot/keys, device cert from disk, + // key from *kernel keyring* (decrypted to a temp file) + static std::unique_ptr MakeMtlsClient(const ParsedUrl &u) + { + // 1) choose CA + std::filesystem::path ca = "/etc/iot/keys/issuing_ca.pem"; + if (!std::filesystem::exists(ca)) + { + ca = "/etc/iot/keys/ca_chain.pem"; + } + // 2) device cert + const std::filesystem::path crt = "/etc/iot/keys/device.crt.pem"; + if (!std::filesystem::exists(crt)) + { + throw std::runtime_error("MakeMtlsClient: device.crt.pem not found"); + } + + // 3) read key payload from kernel keyring and dump to temp (like DeviceControlService) + auto payload = snoop::device_sec::ReadClientKeyPayloadFromKeyring(); + snoop::device_sec::TempFile tf(std::filesystem::temp_directory_path()); + tf.write_all(payload.data(), payload.size()); + + // 4) build SSL client + auto cli = std::make_unique(u.host.c_str(), + u.port, + crt.string().c_str(), + tf.path.string().c_str(), + std::string() /*no password*/); + + // NOTE: we keep verification relaxed for now, like in your task loop + cli->enable_server_certificate_verification(false); + cli->set_ca_cert_path(ca.string().c_str()); + cli->set_connection_timeout(10); + cli->set_read_timeout(60); + cli->set_write_timeout(60); + + if (!cli->is_valid()) + { + throw std::runtime_error("MakeMtlsClient: SSLClient not valid"); + } + + return cli; + } }; } // namespace snoop diff --git a/src/Services/WhipClient.h b/src/Services/WhipClient.h index b21a1c7..5c287c1 100644 --- a/src/Services/WhipClient.h +++ b/src/Services/WhipClient.h @@ -83,6 +83,10 @@ namespace snoop } catch (const std::exception& e) { spdlog::error("WHIP POST offer failed: {}", e.what()); } }); + // m_pc->onLocalCandidate([this](rtc::Candidate c) + // { + // try { PatchCandidateWHIP(c); } + // catch (const std::exception& e) { spdlog::warn("WHIP PATCH candidate failed: {}", e.what()); } }); m_pc->onLocalCandidate([this](rtc::Candidate c) { diff --git a/src/main.cpp b/src/main.cpp index 366a5b6..20ca1ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,14 @@ namespace snoop { spdlog::info("First-run enrollment completed."); } + try + { + enroll.RenewCertificate(false); + } + catch (const std::exception &e) + { + spdlog::warn("Auto-renew check failed: {}", e.what()); + } } auto writerService = std::make_shared(configService, "records"); @@ -76,7 +84,9 @@ namespace snoop try { if (t.payload.contains("whipUrl")) + { whipUrl = t.payload.at("whipUrl").get(); + } } catch (...) { @@ -156,7 +166,9 @@ namespace snoop auto s = snoop::DeviceControlService::ParseRfc3339UtcToMs(t.payload["start"].get()); auto e = snoop::DeviceControlService::ParseRfc3339UtcToMs(t.payload["stop"].get()); if (!s || !e) + { throw std::runtime_error("Invalid RFC3339 times"); + } startMs = *s; stopMs = *e; } @@ -170,8 +182,9 @@ namespace snoop throw std::runtime_error("Missing/invalid start/stop time"); } if (stopMs <= startMs) + { throw std::runtime_error("stop <= start"); - + } g_taskSvc->ArmDeepSleep(startMs, stopMs); return snoop::DeviceControlService::HandlerResult{ true, @@ -183,6 +196,23 @@ namespace snoop return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()}; } }; + handlers.onRenewCert = [enrollSvc](const snoop::DeviceControlService::Task &t) + { + spdlog::info("renew_cert task received, payload: {}", t.payload.dump()); + try + { + bool ok = enrollSvc->RenewCertificate(true); // <- force + if (ok) + { + return snoop::DeviceControlService::HandlerResult{true, R"({"renewed":true})", ""}; + } + return snoop::DeviceControlService::HandlerResult{true, R"({"renewed":false})", ""}; + } + catch (const std::exception &e) + { + return snoop::DeviceControlService::HandlerResult{false, "{}", e.what()}; + } + }; } g_taskSvc = std::make_unique(configService, handlers, controls);