Implements the two read-key-scoped calls in apps/server/src/obs/plugin.routes.ts: GET /api/obs/:slug/slots and POST /api/obs/:slug/token. Three pieces, all in core/ with no OBS dependency: - stplugin::json -- a small, strict JSON reader. Hand-rolled rather than vendoring nlohmann because the only JSON this plugin ever sees is two fixed-shape responses from its own server, and the parser has to build on three platforms with no package-manager step in CI. It never throws, bounds its recursion (kMaxDepth=32) so a hostile response cannot overflow the stack inside OBS, rejects trailing garbage, and returns the caller's fallback for wrong-typed access instead of aborting. - stplugin::HttpClient -- a two-method injectable interface, with libcurl behind it on Linux/macOS and WinHTTP on Windows. WinHTTP rather than curl on Windows because it ships with the OS and does TLS through SChannel: the self-hosted winvm-builder runner has no package manager, and per the scaffold README does not even have cmake preinstalled. Both backends cap the response body at 4 MiB, keep TLS verification on (the read key is a credential), and honour a whole-request timeout. - stplugin::ApiClient -- maps the responses onto an ApiStatus enum that distinguishes NotFound (404), Unavailable (503), NetworkError, MalformedResponse and InvalidConfig. It deliberately does not claim to know whether a 404 was a wrong key or an unknown slug, because the server deliberately does not say. Server URLs are normalised the way an operator actually pastes them, defaulting to https so the read key is never sent in the clear by accident, and redactedUrl() exists so a URL can be logged without the key. Tests (279 checks across two new suites) run at two levels: a fake HttpClient covering every response and error branch, and a real loopback HTTP server on 127.0.0.1 driving the actual platform backend -- so libcurl on Linux/macOS and WinHTTP on Windows are each exercised in CI rather than assumed. The loopback cases deliberately include the ones that must not hang OBS: a truncated JSON body, a connection accepted and closed without a reply, non-HTTP garbage, a dead port, and a stalled server that has to be cut off by the client's own timeout. Verified locally on Ubuntu 24.04: ctest --test-dir build --output-on-failure -> 4/4 passed test_json: 158 checks passed test_api_client: 121 checks passed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
119 lines
4.0 KiB
C++
119 lines
4.0 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);
|
|
|
|
SlotsResult fetchSlots(const ConnectionConfig &config) const;
|
|
TokenResult requestToken(const ConnectionConfig &config) 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);
|
|
|
|
private:
|
|
std::shared_ptr<HttpClient> http_;
|
|
};
|
|
|
|
} // namespace stplugin
|