/* streamer-tools OBS Camera Plugin - libcurl HTTP backend (Linux/macOS) Copyright (C) 2026 CyberCoveLLC 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 #include #include 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(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(request.timeout_ms)); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, static_cast(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(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