2026-09-06 21:28:55 -07:00
|
|
|
/*
|
|
|
|
|
streamer-tools OBS Camera Plugin - libcurl HTTP backend (Linux/macOS)
|
|
|
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
|
|
|
|
|
2026-09-07 04:44:16 -07:00
|
|
|
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
|
2026-09-06 21:28:55 -07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#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));
|
2026-09-06 22:59:57 -07:00
|
|
|
// 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);
|
2026-09-06 21:28:55 -07:00
|
|
|
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
|