/* streamer-tools OBS Camera Plugin - WinHTTP backend (Windows) 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 */ // WinHTTP rather than libcurl on Windows: it ships with the OS, does TLS // through SChannel (so no OpenSSL to build or ship), and needs no package // manager on the self-hosted `winvm-builder` runner -- which, per the // scaffold README, is a bare VM without even cmake preinstalled. #include "stplugin/http.h" #include #include #include #include #include namespace stplugin { namespace { constexpr std::size_t kMaxResponseBytes = 4u * 1024u * 1024u; std::wstring widen(const std::string &s) { if (s.empty()) return std::wstring(); const int needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); if (needed <= 0) return std::wstring(); std::wstring out(static_cast(needed), L'\0'); MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), &out[0], needed); return out; } std::string lastErrorMessage(const char *what) { return std::string(what) + " failed (GetLastError=" + std::to_string(GetLastError()) + ")"; } /// RAII for the three WinHTTP handle kinds, which all close the same way. class Handle { public: Handle() = default; explicit Handle(HINTERNET h) : h_(h) {} ~Handle() { if (h_) WinHttpCloseHandle(h_); } Handle(const Handle &) = delete; Handle &operator=(const Handle &) = delete; void reset(HINTERNET h) { if (h_) WinHttpCloseHandle(h_); h_ = h; } HINTERNET get() const { return h_; } explicit operator bool() const { return h_ != nullptr; } private: HINTERNET h_ = nullptr; }; class WinHttpClient : public HttpClient { public: HttpResponse send(const HttpRequest &request) override { HttpResponse response; const std::wstring url = widen(request.url); if (url.empty()) { response.network_error = "empty or non-UTF-8 URL"; return response; } URL_COMPONENTS parts{}; parts.dwStructSize = sizeof(parts); wchar_t host[256] = {0}; wchar_t path[4096] = {0}; wchar_t extra[4096] = {0}; parts.lpszHostName = host; parts.dwHostNameLength = static_cast(sizeof(host) / sizeof(host[0])); parts.lpszUrlPath = path; parts.dwUrlPathLength = static_cast(sizeof(path) / sizeof(path[0])); parts.lpszExtraInfo = extra; parts.dwExtraInfoLength = static_cast(sizeof(extra) / sizeof(extra[0])); if (!WinHttpCrackUrl(url.c_str(), static_cast(url.size()), 0, &parts)) { response.network_error = lastErrorMessage("WinHttpCrackUrl"); return response; } if (parts.nScheme != INTERNET_SCHEME_HTTP && parts.nScheme != INTERNET_SCHEME_HTTPS) { response.network_error = "unsupported URL scheme"; return response; } Handle session(WinHttpOpen(L"streamer-tools-obs-plugin/1.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)); if (!session) { response.network_error = lastErrorMessage("WinHttpOpen"); return response; } const DWORD timeout = static_cast(request.timeout_ms); WinHttpSetTimeouts(session.get(), static_cast(timeout), static_cast(timeout), static_cast(timeout), static_cast(timeout)); Handle connect(WinHttpConnect(session.get(), host, parts.nPort, 0)); if (!connect) { response.network_error = lastErrorMessage("WinHttpConnect"); return response; } std::wstring target(path); target += extra; const DWORD flags = (parts.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0u; Handle req(WinHttpOpenRequest(connect.get(), widen(request.method).c_str(), target.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags)); if (!req) { response.network_error = lastErrorMessage("WinHttpOpenRequest"); return response; } std::wstring headers; if (!request.content_type.empty()) headers = L"Content-Type: " + widen(request.content_type) + L"\r\n"; const LPCWSTR header_ptr = headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS : headers.c_str(); const DWORD header_len = headers.empty() ? 0u : static_cast(headers.size()); void *body_ptr = request.body.empty() ? WINHTTP_NO_REQUEST_DATA : const_cast(request.body.data()); const DWORD body_len = static_cast(request.body.size()); if (!WinHttpSendRequest(req.get(), header_ptr, header_len, body_ptr, body_len, body_len, 0)) { response.network_error = lastErrorMessage("WinHttpSendRequest"); return response; } if (!WinHttpReceiveResponse(req.get(), nullptr)) { response.network_error = lastErrorMessage("WinHttpReceiveResponse"); return response; } DWORD status = 0; DWORD status_size = sizeof(status); if (!WinHttpQueryHeaders(req.get(), WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size, WINHTTP_NO_HEADER_INDEX)) { response.network_error = lastErrorMessage("WinHttpQueryHeaders"); return response; } response.status = static_cast(status); std::string body; for (;;) { DWORD available = 0; if (!WinHttpQueryDataAvailable(req.get(), &available)) { response.network_error = lastErrorMessage("WinHttpQueryDataAvailable"); return response; } if (available == 0) break; if (body.size() + available > kMaxResponseBytes) { response.network_error = "response body exceeded 4 MiB"; return response; } std::vector chunk(available); DWORD read = 0; if (!WinHttpReadData(req.get(), chunk.data(), available, &read)) { response.network_error = lastErrorMessage("WinHttpReadData"); return response; } if (read == 0) break; body.append(chunk.data(), read); } response.body = std::move(body); return response; } }; } // namespace HttpClient *createPlatformHttpClient() { return new WinHttpClient(); } } // namespace stplugin