Code review findings from before merging feat/livekit-integration to main: - I1: sourceDestroy set self->stopping outside self->mutex, then notified. The worker's condition-variable predicate reads `stopping` under that same mutex, so the store+notify could land between the worker's predicate check and it entering the wait, dropping the notification and leaving the worker asleep for its full backoff (up to 30s) with the OBS UI thread blocked in worker.join(). Now set under the lock, matching how `generation` is already mutated in applySettings. - I3: the LiveKit SDK archive download in cmake/LiveKitSDK.cmake had no SHA256 pin wired up from the top-level CMakeLists.txt, unlike the obs-deps bootstrap right next to it. Added real SHA256 hashes -- computed by downloading each release archive and running sha256sum -- for every triple the pinned v1.10.1 release can resolve to (Linux x64/arm64, macOS x64/arm64, Windows x64), keyed by version+triple so a future version bump fails loudly (via message(WARNING)) instead of silently going unverified. Verified end-to-end locally: a deliberately wrong hash makes the configure step fail with a HASH mismatch error. Only Linux was also build-tested in this environment; macOS/Windows archives were downloaded and hashed but not build-tested here. - I4: the curl HTTP backend followed up to 3 redirects while the read key travels as a URL query parameter, so a malicious/misconfigured redirect (including an HTTPS->HTTP downgrade, which curl doesn't refuse by default) could leak the key. This client only ever talks to two fixed, first-party endpoints, so redirects are disabled outright (CURLOPT_FOLLOWLOCATION 0), matching the WinHTTP backend's existing default behavior. Left normalizeServerUrl's explicit-http:// pass-through as-is with a comment, per review guidance. - I5: ApiClient::redactedUrl was tested but never called. No current call site logs a request URL, so rather than inventing one, added a one-line comment marking it a deliberate guard rail for future logging. - I7: the LiveKit SDK log bridge (livekitLogToObs) wrote SDK messages straight into the OBS log. LiveKit's signaling URL carries the access token as a query parameter; defensively scrub "access_token=" and "key=" values before they ever reach obs_log. New ApiClient::redactSensitiveParams generalizes redactedUrl's redaction pattern to arbitrary text (not just a bare URL), with 6 new unit tests in test_api_client.cpp. - I2: added a code comment on session.cpp's auto_subscribe=true noting the known, unaddressed bandwidth/CPU cost of pulling every participant's track in multi-camera rooms, and that per-publication unsubscribe is a future optimization. No behavior change (out of scope per review). Verified: cmake configure + build + `ctest --test-dir build --output-on-failure` all pass, 6/6 suites (test_api_client now 127 checks, up from 121). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
134 lines
4.9 KiB
C++
134 lines
4.9 KiB
C++
/*
|
|
streamer-tools OBS Camera Plugin - streamer-tools API client
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with this program. If not, see <https://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
// Client for the two read-key-scoped endpoints in
|
|
// apps/server/src/obs/plugin.routes.ts (streamer-tools repo):
|
|
//
|
|
// GET /api/obs/:slug/slots?key=<readKey>
|
|
// 200 { slots: [ { identity, displayName, live } ] }
|
|
// 404 { error: 'not found' } wrong key OR unknown room
|
|
// 503 { error: 'livekit not configured' }
|
|
//
|
|
// POST /api/obs/:slug/token?key=<readKey>
|
|
// 200 { lkToken, wsUrl, identity }
|
|
// 404 / 503 as above
|
|
//
|
|
// The server deliberately answers a wrong key and an unknown slug identically
|
|
// (404), so this client must not claim to know which it was.
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "stplugin/core.h"
|
|
#include "stplugin/http.h"
|
|
|
|
namespace stplugin {
|
|
|
|
enum class ApiStatus {
|
|
Ok,
|
|
/// server URL / slug / key were not all filled in
|
|
InvalidConfig,
|
|
/// request never completed (DNS, TLS, timeout, ...)
|
|
NetworkError,
|
|
/// HTTP 404: unknown room slug or wrong read key -- indistinguishable
|
|
NotFound,
|
|
/// HTTP 503: the server has no LiveKit credentials configured
|
|
Unavailable,
|
|
/// any other non-2xx status
|
|
HttpError,
|
|
/// 2xx but the body was not the JSON shape this client expects
|
|
MalformedResponse,
|
|
};
|
|
|
|
/// A short, operator-facing description. Never includes the read key.
|
|
const char *describeApiStatus(ApiStatus status);
|
|
|
|
struct SlotInfo {
|
|
/// LiveKit participant identity -- this is what the session wrapper
|
|
/// subscribes to, and what gets persisted in the OBS source settings.
|
|
std::string identity;
|
|
/// Human label for the dropdown; the server falls back to identity.
|
|
std::string display_name;
|
|
/// Currently publishing camera video.
|
|
bool live = false;
|
|
};
|
|
|
|
struct SlotsResult {
|
|
ApiStatus status = ApiStatus::InvalidConfig;
|
|
/// Detail for logs/UI. Never contains the read key.
|
|
std::string message;
|
|
std::vector<SlotInfo> slots;
|
|
|
|
bool ok() const { return status == ApiStatus::Ok; }
|
|
};
|
|
|
|
struct TokenResult {
|
|
ApiStatus status = ApiStatus::InvalidConfig;
|
|
std::string message;
|
|
/// LiveKit JWT for a hidden, subscribe-only participant.
|
|
std::string lk_token;
|
|
/// LiveKit websocket URL to connect to.
|
|
std::string ws_url;
|
|
/// The obs:<slug>:<nonce> identity the server minted for us.
|
|
std::string identity;
|
|
|
|
bool ok() const { return status == ApiStatus::Ok; }
|
|
};
|
|
|
|
class ApiClient {
|
|
public:
|
|
/// Takes ownership of the HTTP client, so tests can inject a fake.
|
|
explicit ApiClient(std::shared_ptr<HttpClient> http);
|
|
|
|
/// @param timeout_ms whole-request timeout. Kept as a parameter because
|
|
/// the properties dialog's "Refresh" button runs on OBS's UI thread with
|
|
/// an operator waiting, and must give up sooner than a background
|
|
/// reconnect would.
|
|
SlotsResult fetchSlots(const ConnectionConfig &config, int timeout_ms = 10000) const;
|
|
TokenResult requestToken(const ConnectionConfig &config, int timeout_ms = 10000) const;
|
|
|
|
/// Accepts what an operator would actually paste: a bare hostname, a URL
|
|
/// with a trailing slash, extra whitespace. Returns an empty string if
|
|
/// nothing usable is left. Defaults to https:// when no scheme is given,
|
|
/// because the read key must never be sent in the clear by accident.
|
|
static std::string normalizeServerUrl(const std::string &raw);
|
|
|
|
/// Exposed for tests and for logging: the exact URL a call will hit,
|
|
/// with the read key replaced by "***".
|
|
static std::string redactedUrl(const std::string &url);
|
|
|
|
/// Scrubs "access_token=<value>" and "key=<value>" out of arbitrary
|
|
/// text -- not necessarily a bare URL/query string -- replacing each
|
|
/// value with "<redacted>". Unlike redactedUrl (which only has to
|
|
/// handle "&"-delimited query parameters), a value here can be followed
|
|
/// by a quote or whitespace, because the text this scrubs is a free-form
|
|
/// log line that may merely *contain* a URL. Used by the OBS adapter's
|
|
/// LiveKit SDK log bridge: LiveKit's signaling URL carries the access
|
|
/// token as a query parameter, and the SDK's own log lines could
|
|
/// include it.
|
|
static std::string redactSensitiveParams(const std::string &text);
|
|
|
|
private:
|
|
std::shared_ptr<HttpClient> http_;
|
|
};
|
|
|
|
} // namespace stplugin
|