blob: 87c3993d92aaf9198db09dd419d69517df14e469 [file]
/*
* Copyright 2026 Rive
*/
#if defined(WITH_RIVE_SCRIPTNET) && defined(_WIN32)
#include "rive/scriptnet/net.hpp"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <winhttp.h>
#include <ws2tcpip.h>
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <cwctype>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "ws2_32.lib")
// Script requests on Windows, over WinHTTP in asynchronous mode. As on Apple,
// the session has no ambient authority: cookies are neither kept nor sent,
// automatic authentication is off so neither the user's Windows identity
// (NTLM, Negotiate) nor a stored credential answers a challenge, and WinHTTP
// has no cache.
//
// Redirects are followed here, not by WinHTTP: its own would resend every
// header, Authorization included, and the callback it announces a hop
// through can neither edit them nor reliably stop the hop. Each 3xx is
// instead carried to the next request by NetPolicy::redirect, which rewrites
// it the way the Fetch standard does and runs the policy, and the hop goes
// out as a request of its own.
//
// The address a hop connects to is checked here, from the callback WinHTTP
// makes as it connects, so a public name that resolves to a local or
// private address is refused before the request is sent (see NetPolicy).
// WinHTTP still completes the connection, and for https: its handshake,
// before the close takes effect, as a browser's private network access
// check connects before it blocks; no request bytes follow. Through a
// proxy the address is the proxy's, which WinHTTP announces by resolving
// the proxy's name instead of the host's; what the proxy then reaches is
// the proxy's to judge.
//
// WinHTTP's status callback runs on its own threads and, for the request
// whose call is in progress, may re-enter on the calling thread. Every
// outcome is therefore reported from WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING,
// which WinHTTP guarantees is the last callback for a handle and never
// overlaps another. Ending an exchange, whatever the reason (policy, size,
// timeout, the script), is recording why and closing its handle; a handle is
// never closed while a frame on any thread is inside a WinHTTP call on it,
// which WinHTTP forbids. No WinHTTP call is made with a lock held: the call
// may deliver its completion before returning.
using namespace rive;
using namespace rive::scriptnet;
namespace
{
using Clock = std::chrono::steady_clock;
constexpr DWORD kReadChunkBytes = 64 * 1024;
std::wstring toWide(const std::string& utf8)
{
if (utf8.empty())
{
return std::wstring();
}
int length = MultiByteToWideChar(
CP_UTF8,
0,
utf8.data(),
(int)utf8.size(),
nullptr,
0);
if (length <= 0)
{
return std::wstring();
}
std::wstring wide((size_t)length, L'\0');
MultiByteToWideChar(
CP_UTF8,
0,
utf8.data(),
(int)utf8.size(),
wide.data(),
length);
return wide;
}
std::string toUtf8(const wchar_t* wide, size_t length)
{
if (length == 0)
{
return std::string();
}
int size = WideCharToMultiByte(
CP_UTF8,
0,
wide,
(int)length,
nullptr,
0,
nullptr,
nullptr);
if (size <= 0)
{
return std::string();
}
std::string utf8((size_t)size, '\0');
WideCharToMultiByte(
CP_UTF8,
0,
wide,
(int)length,
utf8.data(),
size,
nullptr,
nullptr);
return utf8;
}
// Header text, back to the bytes the server sent. WinHTTP decodes headers
// with the system's ANSI code page (byte 0x80 comes back as U+20AC on a
// Windows-1252 machine), so encoding with that code page undoes it, and a
// header carrying raw UTF-8, a Location say, reaches the policy and the
// script as the bytes it was.
std::string headerBytes(const wchar_t* text, size_t length)
{
if (length == 0)
{
return std::string();
}
int size = WideCharToMultiByte(
CP_ACP,
0,
text,
(int)length,
nullptr,
0,
nullptr,
nullptr);
if (size <= 0)
{
return std::string();
}
std::string bytes((size_t)size, '\0');
WideCharToMultiByte(
CP_ACP,
0,
text,
(int)length,
bytes.data(),
size,
nullptr,
nullptr);
return bytes;
}
// The system's text for a Win32 or WinHTTP error, or a generic line.
std::string errorMessage(DWORD code)
{
wchar_t* text = nullptr;
DWORD length = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
GetModuleHandleW(L"winhttp.dll"),
code,
0,
reinterpret_cast<wchar_t*>(&text),
0,
nullptr);
std::string message;
if (text != nullptr)
{
message = toUtf8(text, length);
LocalFree(text);
}
while (!message.empty() && (message.back() == '\r' ||
message.back() == '\n' || message.back() == ' '))
{
message.pop_back();
}
if (message.empty())
{
message = "request failed (error " + std::to_string(code) + ")";
}
return message;
}
// Trims a buffer WinHTTP filled to the string it holds.
void trimAtNul(std::wstring* value)
{
value->resize(wcsnlen(value->data(), value->size()));
}
bool queryHeader(HINTERNET request, DWORD info, std::wstring* value)
{
DWORD size = 0;
WinHttpQueryHeaders(request, info, nullptr, nullptr, &size, nullptr);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
{
return false;
}
std::wstring buffer(size / sizeof(wchar_t), L'\0');
if (!WinHttpQueryHeaders(
request,
info,
nullptr,
buffer.data(),
&size,
nullptr))
{
return false;
}
trimAtNul(&buffer);
*value = std::move(buffer);
return true;
}
bool queryNumber(HINTERNET request, DWORD info, DWORD* value)
{
DWORD size = sizeof(DWORD);
return WinHttpQueryHeaders(
request,
info | WINHTTP_QUERY_FLAG_NUMBER,
nullptr,
value,
&size,
nullptr) != FALSE;
}
// Splits WINHTTP_QUERY_RAW_HEADERS_CRLF, whose first line is the status
// line, into header lines. Order and duplicates are kept.
void parseHeaders(const std::wstring& raw, HttpHeaders* headers)
{
size_t start = 0;
bool statusLine = true;
while (start < raw.size())
{
size_t end = raw.find(L"\r\n", start);
if (end == std::wstring::npos)
{
end = raw.size();
}
const wchar_t* line = raw.data() + start;
size_t length = end - start;
start = end + 2;
if (statusLine)
{
statusLine = false;
continue;
}
const wchar_t* colon = std::find(line, line + length, L':');
if (length == 0 || colon == line + length)
{
continue;
}
size_t valueStart = (size_t)(colon - line) + 1;
while (valueStart < length &&
(line[valueStart] == L' ' || line[valueStart] == L'\t'))
{
valueStart++;
}
size_t valueEnd = length;
while (valueEnd > valueStart &&
(line[valueEnd - 1] == L' ' || line[valueEnd - 1] == L'\t'))
{
valueEnd--;
}
headers->push_back(
{headerBytes(line, (size_t)(colon - line)),
headerBytes(line + valueStart, valueEnd - valueStart)});
}
}
// HEAD answers and null body statuses carry no body, whatever their
// Content-Length says about the resource.
bool expectsBody(bool head, uint16_t status)
{
if (head)
{
return false;
}
switch (status)
{
case 101:
case 103:
case 204:
case 205:
case 304:
return false;
}
return true;
}
bool isRedirect(uint16_t status)
{
return status == 301 || status == 302 || status == 303 || status == 307 ||
status == 308;
}
const std::string* findHeader(const HttpHeaders& headers, const char* name)
{
for (const HttpHeader& header : headers)
{
if (lowerAscii(header.name) == name)
{
return &header.value;
}
}
return nullptr;
}
// The bytes of an address as WinHTTP's callbacks spell it: dotted decimal,
// or IPv6 in brackets, possibly with a scope.
bool parseAddress(const wchar_t* text, uint8_t* bytes, size_t* length)
{
std::wstring address(text);
if (!address.empty() && address.front() == L'[')
{
address.erase(0, 1);
}
size_t end = address.find_first_of(L"]%");
if (end != std::wstring::npos)
{
address.resize(end);
}
if (address.find(L':') != std::wstring::npos)
{
in6_addr parsed;
if (InetPtonW(AF_INET6, address.c_str(), &parsed) != 1)
{
return false;
}
std::memcpy(bytes, &parsed, 16);
*length = 16;
return true;
}
in_addr parsed;
if (InetPtonW(AF_INET, address.c_str(), &parsed) != 1)
{
return false;
}
std::memcpy(bytes, &parsed, 4);
*length = 4;
return true;
}
// Host names as WinHTTP reports the one it resolves, case aside.
bool sameHost(const std::wstring& a, const std::wstring& b)
{
return a.size() == b.size() &&
std::equal(a.begin(), a.end(), b.begin(), [](wchar_t x, wchar_t y) {
return towlower(x) == towlower(y);
});
}
// Parses an http: or https: URL into `parts`, which point into `url`.
bool crackUrl(const std::wstring& url, URL_COMPONENTS* parts)
{
*parts = URL_COMPONENTS{};
parts->dwStructSize = sizeof(*parts);
parts->dwSchemeLength = (DWORD)-1;
parts->dwHostNameLength = (DWORD)-1;
parts->dwUrlPathLength = (DWORD)-1;
parts->dwExtraInfoLength = (DWORD)-1;
return WinHttpCrackUrl(url.c_str(), (DWORD)url.size(), 0, parts) &&
(parts->nScheme == INTERNET_SCHEME_HTTP ||
parts->nScheme == INTERNET_SCHEME_HTTPS) &&
parts->dwHostNameLength != 0;
}
class Request;
// What the provider and its requests share. Each side holds it, since a
// request's last callback can arrive after the provider is gone. The
// session lives here because closing it invalidates every request under
// it, pending ones included: whichever of the provider and the last request
// finishes second closes it.
struct Registry : public RefCnt<Registry>
{
std::mutex mutex;
HINTERNET session = nullptr;
std::unordered_map<RequestId, rcp<Request>> requests;
// Wakes the watchdog for a new deadline, and to stop.
std::condition_variable wake;
// The provider is gone; the last request out closes the session.
bool stopping = false;
// With the mutex held: the session, once the provider is gone and no
// request remains, for the caller to close with the mutex released.
HINTERNET takeSessionIfDone()
{
if (!stopping || !requests.empty() || session == nullptr)
{
return nullptr;
}
HINTERNET done = session;
session = nullptr;
return done;
}
};
// One exchange: a request and the hops its redirects lead to, each a WinHTTP
// request of its own, one at a time. The registry holds it until the
// exchange ends; WinHTTP holds it as each handle's context until that
// handle's closing callback; and each callback holds it while running, so a
// close inside one is safe.
//
// The handle of the moment is closed exactly once, to end the exchange or
// to move to the next hop, and never while a frame on any thread is inside
// a WinHTTP call on it: such frames hold a CallGuard, and the last one out
// performs a close that was decided meanwhile.
class Request : public RefCnt<Request>
{
public:
Request(
rcp<Registry> registry,
RequestId id,
const HttpRequest& request,
const NetLimits& limits)
: m_registry(std::move(registry))
, m_id(id)
, m_hop(request)
, m_deadline(Clock::now() + std::chrono::milliseconds(request.timeoutMs))
, m_maxBodyBytes(limits.maxResponseBodyBytes)
, m_allowLocalAddresses(limits.allowLocalAddresses)
{}
Clock::time_point deadline() const { return m_deadline; }
bool closing()
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_closing;
}
// Registered with the registry beforehand, so a stop or abort can already
// reach it.
void start(HINTERNET session)
{
m_session = session;
sendHop();
}
// Any thread. Ends the exchange with `error`, which the closing callback
// reports instead of the cancellation it causes. The first reason wins.
void stop(NetError error) { settle(false, &error); }
// Any thread. The script or the host gave up; the outcome is dropped.
void abort() { settle(false, nullptr); }
static void CALLBACK onStatus(
HINTERNET handle,
DWORD_PTR context,
DWORD status,
LPVOID info,
DWORD length)
{
if (context == 0)
{
return;
}
rcp<Request> request = ref_rcp(reinterpret_cast<Request*>(context));
if (status == WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING)
{
request->onClosing();
return;
}
// Nothing below matters once the handle is on its way out, and the
// guard keeps it open under the calls made here.
CallGuard guard(request.get());
if (!guard.held())
{
return;
}
switch (status)
{
case WINHTTP_CALLBACK_STATUS_RESOLVING_NAME:
request->onResolving(static_cast<const wchar_t*>(info));
break;
case WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER:
request->onConnecting(static_cast<const wchar_t*>(info));
break;
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
if (!WinHttpReceiveResponse(handle, nullptr))
{
request->stop({NetErrorCode::Network, errorMessage(GetLastError())});
}
break;
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
request->onHeaders(handle);
break;
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
request->onRead(handle, length);
break;
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
request->onError(static_cast<WINHTTP_ASYNC_RESULT*>(info)->dwError);
break;
}
}
private:
// Held by every frame inside a WinHTTP call on the handle of the moment,
// on any thread. Not granted once that handle is to be closed; the last
// holder out performs the close.
class CallGuard
{
public:
explicit CallGuard(Request* request) : m_request(request)
{
std::lock_guard<std::mutex> lock(m_request->m_mutex);
m_held = !m_request->m_closing && !m_request->m_hopping;
if (m_held)
{
m_request->m_calls++;
}
}
~CallGuard()
{
if (!m_held)
{
return;
}
HINTERNET toClose = nullptr;
{
std::lock_guard<std::mutex> lock(m_request->m_mutex);
m_request->m_calls--;
toClose = m_request->takeCloseLocked();
}
if (toClose != nullptr)
{
WinHttpCloseHandle(toClose);
}
}
bool held() const { return m_held; }
private:
Request* m_request;
bool m_held = false;
};
// With m_mutex held: the handle to close now, if the exchange has ended
// or the next hop is due, the handle exists, no frame is inside a call on
// it, and it has not been closed already.
HINTERNET takeCloseLocked()
{
if (!(m_closing || m_hopping) || m_closeSent || m_request == nullptr ||
m_calls != 0)
{
return nullptr;
}
m_closeSent = true;
return m_request;
}
// Any thread. Ends the exchange once: with `error`, or as completed, or
// as cancelled when neither. Later calls change nothing, so the first
// reason recorded is the one reported. The handle is closed here unless
// a frame is inside a WinHTTP call on it, or there is none at the
// moment; then that frame, or the one opening the next, closes it.
void settle(bool completed, const NetError* error)
{
HINTERNET toClose = nullptr;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_closing)
{
return;
}
m_closing = true;
if (error != nullptr)
{
m_failed = true;
m_error = *error;
}
else
{
m_completed = completed;
}
toClose = takeCloseLocked();
}
if (toClose != nullptr)
{
WinHttpCloseHandle(toClose);
}
}
// Opens and sends m_hop as a WinHTTP request, the exchange's first or the
// one a redirect led to. The guard keeps a stop or abort from closing the
// handle under the calls made here; one decided meanwhile closes it on
// the way out, and the closing callback reports it.
void sendHop()
{
CallGuard guard(this);
if (!guard.held())
{
// Stopped between hops, with no handle to report through.
reportWithoutHandle();
return;
}
URL_COMPONENTS parts;
std::wstring url = toWide(m_hop.url);
if (!crackUrl(url, &parts))
{
failBeforeSending({NetErrorCode::Policy, "the URL could not be parsed"});
return;
}
std::wstring host(parts.lpszHostName, parts.dwHostNameLength);
m_hopHost = host;
m_proxied = false;
std::wstring path(parts.lpszUrlPath, parts.dwUrlPathLength);
path.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
if (path.empty())
{
path = L"/";
}
m_connection = WinHttpConnect(m_session, host.c_str(), parts.nPort, 0);
if (m_connection == nullptr)
{
failBeforeSending({NetErrorCode::Network, errorMessage(GetLastError())});
return;
}
DWORD flags = parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE
: 0;
HINTERNET request = WinHttpOpenRequest(
m_connection,
toWide(m_hop.method).c_str(),
path.c_str(),
nullptr,
nullptr,
nullptr,
flags);
if (request == nullptr)
{
failBeforeSending({NetErrorCode::Network, errorMessage(GetLastError())});
return;
}
// From here this hop's closing callback reports or continues the
// exchange, and WinHTTP holds this request until then.
ref();
DWORD_PTR context = reinterpret_cast<DWORD_PTR>(this);
if (!WinHttpSetOption(
request,
WINHTTP_OPTION_CONTEXT_VALUE,
&context,
sizeof(context)))
{
DWORD error = GetLastError();
unref();
WinHttpCloseHandle(request);
failBeforeSending({NetErrorCode::Network, errorMessage(error)});
return;
}
bool stopped;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_request = request;
stopped = m_closing;
}
if (stopped)
{
// The watchdog gave up before the handle existed. The guard closes
// it on the way out, and the closing callback reports the timeout.
return;
}
DWORD disabled = WINHTTP_DISABLE_COOKIES | WINHTTP_DISABLE_AUTHENTICATION;
DWORD autologon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
// Redirects are this file's to follow; see the top.
DWORD redirects = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
if (!WinHttpSetOption(
request,
WINHTTP_OPTION_DISABLE_FEATURE,
&disabled,
sizeof(disabled)) ||
!WinHttpSetOption(
request,
WINHTTP_OPTION_AUTOLOGON_POLICY,
&autologon,
sizeof(autologon)) ||
!WinHttpSetOption(
request,
WINHTTP_OPTION_REDIRECT_POLICY,
&redirects,
sizeof(redirects)))
{
stop({NetErrorCode::Network, errorMessage(GetLastError())});
return;
}
// Each phase gets the whole timeout; the watchdog bounds the exchange.
int timeout = (int)m_hop.timeoutMs;
WinHttpSetTimeouts(request, timeout, timeout, timeout, timeout);
for (const HttpHeader& header : m_hop.headers)
{
std::wstring line = toWide(header.name) + L": " + toWide(header.value);
if (!WinHttpAddRequestHeaders(
request,
line.c_str(),
(DWORD)-1,
WINHTTP_ADDREQ_FLAG_ADD |
WINHTTP_ADDREQ_FLAG_COALESCE_WITH_COMMA))
{
stop(
{NetErrorCode::Network,
"a header could not be set: " + errorMessage(GetLastError())});
return;
}
}
// The body must outlive the send, so it stays a member.
DWORD bodySize = (DWORD)m_hop.body.size();
if (!WinHttpSendRequest(
request,
nullptr,
0,
bodySize == 0 ? nullptr : m_hop.body.data(),
bodySize,
bodySize,
context))
{
stop({NetErrorCode::Network, errorMessage(GetLastError())});
}
}
// Before WinHTTP holds this hop, there is no closing callback to report
// through: the exchange ends here, with `error` unless a reason (a
// timeout, say) was recorded first.
void failBeforeSending(NetError error)
{
if (m_connection != nullptr)
{
WinHttpCloseHandle(m_connection);
m_connection = nullptr;
}
endWithoutHandle(&error);
}
// The exchange ended while no handle existed to close: report what was
// recorded.
void reportWithoutHandle() { endWithoutHandle(nullptr); }
void endWithoutHandle(const NetError* error)
{
bool failed;
NetError recorded;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_closing)
{
m_closing = true;
if (error != nullptr)
{
m_failed = true;
m_error = *error;
}
}
failed = m_failed;
recorded = m_error;
}
forget();
if (failed)
{
fail(m_id, std::move(recorded));
}
else
{
fail(m_id, {NetErrorCode::Aborted, "the request was cancelled"});
}
}
// Leaves the registry: the exchange is over.
void forget()
{
HINTERNET session = nullptr;
{
std::lock_guard<std::mutex> lock(m_registry->mutex);
m_registry->requests.erase(m_id);
session = m_registry->takeSessionIfDone();
}
if (session != nullptr)
{
WinHttpCloseHandle(session);
}
}
// WinHTTP is resolving the name it will connect to: the hop's host, or
// a proxy's.
void onResolving(const wchar_t* name)
{
m_proxied = !sameHost(name, m_hopHost);
}
// As the connection is made: the address the host resolved to is the
// policy's to judge, unless it is a proxy's. A refusal closes the handle
// before the request goes out.
void onConnecting(const wchar_t* address)
{
if (m_proxied || m_allowLocalAddresses)
{
return;
}
uint8_t bytes[16];
size_t length = 0;
std::string reason;
if (!parseAddress(address, bytes, &length))
{
reason = "the host resolves to an address that could not be checked";
}
else if (NetPolicy::checkAddress(bytes, length, &reason))
{
return;
}
stop(
{NetErrorCode::Policy,
m_hops == 0 ? reason : "a redirect was refused: " + reason});
}
void onHeaders(HINTERNET handle)
{
DWORD status = 0;
if (!queryNumber(handle, WINHTTP_QUERY_STATUS_CODE, &status) ||
status > 999)
{
stop({NetErrorCode::Network, "no HTTP response arrived"});
return;
}
HttpResponse response;
response.status = (uint16_t)status;
std::wstring text;
if (queryHeader(handle, WINHTTP_QUERY_STATUS_TEXT, &text))
{
response.statusText = headerBytes(text.data(), text.size());
}
if (response.statusText.empty())
{
// HTTP/2 and later carry no reason phrase.
response.statusText = standardReasonPhrase(response.status);
}
std::wstring raw;
if (queryHeader(handle, WINHTTP_QUERY_RAW_HEADERS_CRLF, &raw))
{
parseHeaders(raw, &response.headers);
}
response.url = m_hop.url;
// A redirect: the policy decides and rewrites, and this hop's handle
// closes once the guard lets go; its closing callback sends the next.
// A 3xx without a Location is an answer like any other.
const std::string* location = isRedirect(response.status)
? findHeader(response.headers, "location")
: nullptr;
if (location != nullptr)
{
HttpRequest next = m_hop;
NetError error;
if (!NetPolicy::redirect(
next,
response.status,
*location,
m_hops,
scriptnet::limits(),
&error))
{
stop({error.code, "a redirect was refused: " + error.message});
return;
}
m_hop = std::move(next);
m_hops++;
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_closing)
{
m_hopping = true;
}
return;
}
// Without a body to expect, the length is no reason to stop or reserve;
// onRead still counts what does arrive.
if (expectsBody(m_hop.method == "HEAD", response.status))
{
const std::string* length = findHeader(
response.headers,
"content-length");
unsigned long long expected = length == nullptr ? 0
: std::strtoull(
length->c_str(),
nullptr,
10);
if (expected > m_maxBodyBytes)
{
stop(
{NetErrorCode::TooLarge,
"the response body is larger than " +
std::to_string(m_maxBodyBytes) + " bytes"});
return;
}
m_body.reserve((size_t)expected);
}
m_response = std::move(response);
m_chunk.resize(kReadChunkBytes);
if (!WinHttpReadData(handle, m_chunk.data(), kReadChunkBytes, nullptr))
{
stop({NetErrorCode::Network, errorMessage(GetLastError())});
}
}
void onRead(HINTERNET handle, DWORD length)
{
if (length == 0)
{
settle(true, nullptr);
return;
}
if (m_body.size() + length > m_maxBodyBytes)
{
stop(
{NetErrorCode::TooLarge,
"the response body is larger than " +
std::to_string(m_maxBodyBytes) + " bytes"});
return;
}
m_body.insert(m_body.end(), m_chunk.begin(), m_chunk.begin() + length);
if (!WinHttpReadData(handle, m_chunk.data(), kReadChunkBytes, nullptr))
{
stop({NetErrorCode::Network, errorMessage(GetLastError())});
}
}
void onError(DWORD code)
{
// A reason recorded by stop() outranks the cancellation it caused.
switch (code)
{
case ERROR_WINHTTP_OPERATION_CANCELLED:
stop({NetErrorCode::Aborted, "the request was cancelled"});
break;
case ERROR_WINHTTP_TIMEOUT:
stop({NetErrorCode::Timeout, "no response before the timeout"});
break;
default:
stop({NetErrorCode::Network, errorMessage(code)});
break;
}
}
// The last callback for a hop's handle: send the next hop, or report.
void onClosing()
{
// The connection, a parent of the handle just closed, goes before the
// session, its own parent.
if (m_connection != nullptr)
{
WinHttpCloseHandle(m_connection);
m_connection = nullptr;
}
bool hop;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_request = nullptr;
m_closeSent = false;
hop = m_hopping && !m_closing;
m_hopping = false;
}
if (hop)
{
sendHop();
// WinHTTP's hold on the hop just closed, taken in sendHop().
unref();
return;
}
forget();
bool failed;
bool completed;
NetError error;
HttpResponse response;
{
std::lock_guard<std::mutex> lock(m_mutex);
failed = m_failed;
completed = m_completed;
error = std::move(m_error);
response = std::move(m_response);
}
if (failed)
{
fail(m_id, std::move(error));
}
else if (completed)
{
response.body = std::move(m_body);
complete(m_id, std::move(response));
}
else
{
fail(m_id, {NetErrorCode::Aborted, "the request was cancelled"});
}
unref();
}
rcp<Registry> m_registry;
RequestId m_id;
HINTERNET m_session = nullptr;
// The request as last sent: the script's, then each hop's.
HttpRequest m_hop;
uint32_t m_hops = 0;
Clock::time_point m_deadline;
uint32_t m_maxBodyBytes;
bool m_allowLocalAddresses;
// Only the callback thread of the moment touches these; WinHTTP never
// runs two callbacks for one handle at once, and hops run one at a time.
HINTERNET m_connection = nullptr;
// The hop's host as sent, and whether WinHTTP is reaching it through a
// proxy, which it resolves instead.
std::wstring m_hopHost;
bool m_proxied = false;
std::vector<uint8_t> m_chunk;
std::vector<uint8_t> m_body;
std::mutex m_mutex;
// The hop's handle, from its creation to its closing callback.
HINTERNET m_request = nullptr;
// Frames inside a WinHTTP call on m_request.
int m_calls = 0;
// The exchange has ended; what follows is reported from the closing
// callback.
bool m_closing = false;
// m_request is to close so the next hop can be sent.
bool m_hopping = false;
bool m_closeSent = false;
bool m_failed = false;
NetError m_error;
bool m_completed = false;
HttpResponse m_response;
};
class WindowsProvider : public Provider
{
public:
WindowsProvider() : m_registry(make_rcp<Registry>())
{
// The system's proxy settings, as the other transports use, found the
// way Windows 8.1 and later recommend; older WinHTTP falls back to its
// own machine-wide setting.
HINTERNET session = WinHttpOpen(
L"Rive",
WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
nullptr,
nullptr,
WINHTTP_FLAG_ASYNC);
if (session == nullptr)
{
session = WinHttpOpen(
L"Rive",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
nullptr,
nullptr,
WINHTTP_FLAG_ASYNC);
}
if (session != nullptr)
{
// Browsers and NSURLSession hand scripts decoded bodies too.
DWORD decompression = WINHTTP_DECOMPRESSION_FLAG_ALL;
WinHttpSetOption(
session,
WINHTTP_OPTION_DECOMPRESSION,
&decompression,
sizeof(decompression));
// HTTP/2 where the server offers it, as NSURLSession; older WinHTTP
// declines the option and stays on HTTP/1.1.
DWORD protocols = WINHTTP_PROTOCOL_FLAG_HTTP2;
WinHttpSetOption(
session,
WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
&protocols,
sizeof(protocols));
if (WinHttpSetStatusCallback(
session,
&Request::onStatus,
WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS |
WINHTTP_CALLBACK_FLAG_HANDLES |
WINHTTP_CALLBACK_STATUS_RESOLVING_NAME |
WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER,
0) == WINHTTP_INVALID_STATUS_CALLBACK)
{
WinHttpCloseHandle(session);
session = nullptr;
}
}
m_registry->session = session;
m_watchdog = std::thread([registry = m_registry] { watch(registry); });
}
~WindowsProvider() override
{
// Requests still out answer with a cancellation, which no one is
// listening for. Their closing callbacks keep the registry alive, and
// the last of them closes the session; with none out, it goes now.
std::vector<rcp<Request>> requests;
HINTERNET session = nullptr;
{
std::lock_guard<std::mutex> lock(m_registry->mutex);
m_registry->stopping = true;
for (auto& entry : m_registry->requests)
{
requests.push_back(entry.second);
}
session = m_registry->takeSessionIfDone();
}
if (session != nullptr)
{
WinHttpCloseHandle(session);
}
m_registry->wake.notify_all();
m_watchdog.join();
for (const rcp<Request>& request : requests)
{
request->abort();
}
}
void start(RequestId id, const HttpRequest& request) override
{
// The session is fixed from the constructor until the registry closes
// it, which only happens once this provider is gone.
HINTERNET session = m_registry->session;
if (session == nullptr)
{
fail(id, {NetErrorCode::Network, "WinHTTP is unavailable"});
return;
}
auto entry = make_rcp<Request>(
m_registry,
id,
request,
scriptnet::limits());
{
std::lock_guard<std::mutex> lock(m_registry->mutex);
m_registry->requests[id] = entry;
}
m_registry->wake.notify_all();
entry->start(session);
}
void abort(RequestId id) override
{
rcp<Request> entry;
{
std::lock_guard<std::mutex> lock(m_registry->mutex);
auto it = m_registry->requests.find(id);
if (it != m_registry->requests.end())
{
entry = it->second;
}
}
if (entry != nullptr)
{
entry->abort();
}
}
private:
// WinHTTP's timeouts are per phase; the script's covers the whole
// exchange, so a thread stops whatever outlives its deadline.
static void watch(rcp<Registry> registry)
{
std::unique_lock<std::mutex> lock(registry->mutex);
while (!registry->stopping)
{
Clock::time_point now = Clock::now();
Clock::time_point next = Clock::time_point::max();
std::vector<rcp<Request>> expired;
for (auto& entry : registry->requests)
{
const rcp<Request>& request = entry.second;
if (request->closing())
{
continue;
}
if (request->deadline() <= now)
{
expired.push_back(request);
}
else
{
next = std::min(next, request->deadline());
}
}
if (!expired.empty())
{
lock.unlock();
for (const rcp<Request>& request : expired)
{
request->stop(
{NetErrorCode::Timeout, "no response before the timeout"});
}
lock.lock();
continue;
}
if (next == Clock::time_point::max())
{
registry->wake.wait(lock);
}
else
{
registry->wake.wait_until(lock, next);
}
}
}
rcp<Registry> m_registry;
std::thread m_watchdog;
};
} // namespace
rcp<Provider> rive::scriptnet::makePlatformProvider()
{
return make_rcp<WindowsProvider>();
}
#endif // WITH_RIVE_SCRIPTNET && _WIN32