Files
obs-streamer-tools-plugin/core/src/http_curl.cpp
T
shadowdaoandClaude Sonnet 5 969b8db94a
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Successful in 12m10s
license: relicense first-party code from GPL-2.0-or-later to Apache-2.0
Owner sign-off: replace root LICENSE with Apache License 2.0, add a root
NOTICE file, and swap the GPL-2.0 boilerplate header in every first-party
core/ and obs-adapter/ source file for a short Apache-2.0 notice.

This resolves review finding C2 (GPLv2 top-level LICENSE vs. the vendored
Apache-2.0 LiveKit SDK is a license-compatibility violation): the whole
repo is now Apache-2.0, matching LiveKit, so there's no GPL/Apache clash
left. Updated the README Status gate and the CI workflow comment to reflect
that C2 is resolved, while leaving the C1 WebRTC/OpenH264 patent/royalty
gate untouched -- that question is still open and still blocks release.

third_party/ stays under its own upstream licenses; only this project's own
code changed hands. All 6 CTest suites still pass after the header swap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 04:44:16 -07:00

140 lines
5.2 KiB
C++

/*
streamer-tools OBS Camera Plugin - libcurl HTTP backend (Linux/macOS)
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
*/
#include "stplugin/http.h"
#include <curl/curl.h>
#include <cstddef>
#include <mutex>
namespace stplugin {
namespace {
/// Hard cap on a response body. The two endpoints this client talks to return
/// a few hundred bytes; anything larger is a misconfigured proxy or a wrong
/// URL, and must not be allowed to grow OBS's heap without bound.
constexpr std::size_t kMaxResponseBytes = 4u * 1024u * 1024u;
struct WriteContext {
std::string body;
bool overflowed = false;
};
std::size_t writeCallback(char *ptr, std::size_t size, std::size_t nmemb, void *userdata)
{
auto *ctx = static_cast<WriteContext *>(userdata);
const std::size_t bytes = size * nmemb;
if (ctx->body.size() + bytes > kMaxResponseBytes) {
ctx->overflowed = true;
return 0; // aborts the transfer with CURLE_WRITE_ERROR
}
ctx->body.append(ptr, bytes);
return bytes;
}
/// curl_global_init is not thread-safe and must run once per process before
/// any easy handle is created. OBS may create several sources concurrently.
void ensureCurlGlobalInit()
{
static std::once_flag once;
std::call_once(once, [] { curl_global_init(CURL_GLOBAL_DEFAULT); });
}
class CurlHttpClient : public HttpClient {
public:
HttpResponse send(const HttpRequest &request) override
{
ensureCurlGlobalInit();
HttpResponse response;
CURL *curl = curl_easy_init();
if (!curl) {
response.network_error = "curl_easy_init failed";
return response;
}
WriteContext ctx;
struct curl_slist *headers = nullptr;
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, static_cast<long>(request.timeout_ms));
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>(request.timeout_ms));
// Redirects are never legitimate here: this client only ever talks to
// two fixed, first-party streamer-tools API endpoints, and the read
// key travels as a URL query parameter (see api_client.cpp). Blindly
// following a redirect -- including an HTTPS->HTTP downgrade, which
// curl does not refuse by default -- would hand that key to whatever
// host the redirect points at. A redirect from our own server is a
// configuration error, so treat it as a failed request instead of
// silently following it. This also brings this backend in line with
// http_winhttp.cpp, which already refuses HTTPS->HTTP downgrades by
// default.
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "streamer-tools-obs-plugin/1.0");
// NOSIGNAL is required whenever curl is used off the main thread:
// without it curl installs a SIGALRM handler for DNS timeouts, which
// is process-global and would be a rude thing to do inside OBS.
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
// TLS verification stays on. The read key is a credential; sending it
// to an unverified host is exactly the failure this must not have.
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
if (request.method == "POST") {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(request.body.size()));
} else if (request.method != "GET") {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str());
}
if (!request.content_type.empty()) {
const std::string header = "Content-Type: " + request.content_type;
headers = curl_slist_append(headers, header.c_str());
}
// Fastify answers a bare POST with no body fine, but some proxies
// insert an Expect: 100-continue round trip; suppress it.
headers = curl_slist_append(headers, "Expect:");
if (headers)
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
const CURLcode rc = curl_easy_perform(curl);
if (rc == CURLE_OK) {
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
response.status = status;
response.body = std::move(ctx.body);
} else if (ctx.overflowed) {
response.network_error = "response body exceeded 4 MiB";
} else {
response.network_error = curl_easy_strerror(rc);
}
if (headers)
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return response;
}
};
} // namespace
HttpClient *createPlatformHttpClient()
{
return new CurlHttpClient();
}
} // namespace stplugin