Files
ReSkate/Extension/Plugins/plugin_http.cpp
T
AyeZeeBB 5c0993afea Plugins: the wider set (phase 3), behind one Game interface the tests fake
- Skater: skater.state() and the skater_state and bail events (events.skater); noclip and boosts
  through the console's own queue, under the host's rules (skater.control); a save point of the
  plugin's own (trainer); the game's HUD hidden while any plugin asks (hud).
- World: park editor objects listed, placed, moved and removed through the editor's own edits and
  the session's object rules (world.objects); time of day, world controls and park rotation as
  the World menu sets them, refused where the host chooses (world.environment); fast travel and
  levels, refused for guests (travel); AI skaters, outside sessions only (world.ai).
- Session: other players' skaters as last heard (session.skaters, a feed the session publishes
  each frame, plugin_session_feed.h); a plugin's tag above a player's nametag (session.nametags);
  voice volume (voice.control); chat lines as events (chat.read) and sending one, one every 3 s
  (chat.send); throwdown state and events (throwdowns).
- Music from outside players (music), progress (progression.read) and game settings
  (settings.read), read only; named actions on keys or pad combos, pressed while the game has the
  input (input); full-screen screens with the input, closed by Esc (ui.screen).
- Libraries and events between plugins: expose, require_plugin, emit, on_plugin; plain data is
  copied across VMs and a plugin in a call cannot be called back into.
- net.http: HTTPS to the hosts reskate-plugin.json's new "domains" lists; no redirects, cookies
  or Windows sign-in; private and local addresses refused; 10 a minute, 4 waiting, 1 MB back.
- Left for engine work (listed in the design doc): respawn and freeze, manuals, the camera, trick
  animations, replays, starting throwdowns, the game's own soundtrack, overlay images, rebinding
  plugin keys in the Binds page, local-only chat lines.
- reskate-test has a wider module (ptest noclip, boost, hidehud, object, night, day, ai, screen,
  bind, music, chat, web by hand); reskate-hostile has chat-spam, edit-spam, travel-spam,
  web-elsewhere, bind-spam and screen-loop.

All host tests pass; not yet tried in game.
2026-10-19 13:49:29 -05:00

179 lines
7.9 KiB
C++

#include "plugin_http.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#include <winhttp.h>
#include <atomic>
#include <cstring>
#include <mutex>
#include <thread>
namespace dingosdk::plugins::http {
namespace {
constexpr std::size_t most_bytes = 1u << 20;
constexpr DWORD timeout_ms = 10000;
constexpr int most_going = 8;
std::mutex mutex;
std::vector<HttpResponse> answers; // under mutex
std::atomic<int> going{};
std::wstring wide(const std::string& text) { return std::wstring(text.begin(), text.end()); } // ASCII, checked by the API
// Loopback, private, link-local, shared (CGNAT), multicast, reserved and unspecified addresses:
// a plugin's request never reaches the player's own network.
bool reserved(const sockaddr* address) {
if (address->sa_family == AF_INET) {
const auto* v4 = reinterpret_cast<const sockaddr_in*>(address);
const auto ip = ntohl(v4->sin_addr.s_addr);
const auto a = ip >> 24, b = (ip >> 16) & 0xff;
return a == 0 || a == 10 || a == 127 || (a == 100 && b >= 64 && b < 128) || (a == 169 && b == 254) ||
(a == 172 && b >= 16 && b < 32) || (a == 192 && b == 168) || (a == 192 && b == 0) || (a == 198 && (b == 18 || b == 19)) ||
a >= 224;
}
if (address->sa_family == AF_INET6) {
const auto* v6 = reinterpret_cast<const sockaddr_in6*>(address);
const auto* bytes = v6->sin6_addr.s6_addr;
bool zero = true;
for (int i = 0; i < 15; ++i) zero = zero && bytes[i] == 0;
if (zero && (bytes[15] == 0 || bytes[15] == 1)) return true; // :: and ::1
if ((bytes[0] & 0xfe) == 0xfc) return true; // fc00::/7
if (bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80) return true; // fe80::/10
if (bytes[0] == 0xff) return true; // multicast
bool mapped = true;
for (int i = 0; i < 10; ++i) mapped = mapped && bytes[i] == 0;
if (mapped && bytes[10] == 0xff && bytes[11] == 0xff) { // ::ffff:a.b.c.d
sockaddr_in v4{};
v4.sin_family = AF_INET;
std::memcpy(&v4.sin_addr, bytes + 12, 4);
return reserved(reinterpret_cast<const sockaddr*>(&v4));
}
return false;
}
return true;
}
// Why the host may not be reached, or empty.
std::string check_host(const std::string& host) {
WSADATA data{};
if (WSAStartup(MAKEWORD(2, 2), &data) != 0) return "the network is unavailable";
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo* found = nullptr;
std::string error;
if (getaddrinfo(host.c_str(), "443", &hints, &found) != 0 || !found) error = "its host name could not be found";
for (auto* item = found; item && error.empty(); item = item->ai_next)
if (reserved(item->ai_addr)) error = "its host is on a private or local network";
if (found) freeaddrinfo(found);
WSACleanup();
return error;
}
HttpResponse run(const HttpRequest& request) {
HttpResponse response{request.id, request.plugin};
if (auto error = check_host(request.host); !error.empty()) {
response.error = error;
return response;
}
struct Handle {
HINTERNET value{};
~Handle() { if (value) WinHttpCloseHandle(value); }
};
const auto agent = wide("ReSkate-Plugin/" + request.plugin);
Handle session{WinHttpOpen(agent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)};
if (!session.value) session.value = WinHttpOpen(agent.c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!session.value) {
response.error = "web requests are unavailable";
return response;
}
WinHttpSetTimeouts(session.value, static_cast<int>(timeout_ms), static_cast<int>(timeout_ms), static_cast<int>(timeout_ms), static_cast<int>(timeout_ms));
DWORD protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
WinHttpSetOption(session.value, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols, sizeof(protocols));
Handle connection{WinHttpConnect(session.value, wide(request.host).c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0)};
if (!connection.value) {
response.error = "it could not connect";
return response;
}
Handle call{WinHttpOpenRequest(connection.value, wide(request.method).c_str(), wide(request.path).c_str(), nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)};
if (!call.value) {
response.error = "it could not be made";
return response;
}
DWORD never = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
WinHttpSetOption(call.value, WINHTTP_OPTION_REDIRECT_POLICY, &never, sizeof(never));
DWORD off = WINHTTP_DISABLE_COOKIES | WINHTTP_DISABLE_AUTHENTICATION | WINHTTP_DISABLE_KEEP_ALIVE;
WinHttpSetOption(call.value, WINHTTP_OPTION_DISABLE_FEATURE, &off, sizeof(off));
DWORD logon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
WinHttpSetOption(call.value, WINHTTP_OPTION_AUTOLOGON_POLICY, &logon, sizeof(logon));
std::wstring headers;
if (!request.content_type.empty()) headers = L"Content-Type: " + wide(request.content_type) + L"\r\n";
const BOOL sent = WinHttpSendRequest(call.value, headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS : headers.c_str(),
headers.empty() ? 0 : static_cast<DWORD>(-1L),
request.body.empty() ? WINHTTP_NO_REQUEST_DATA : const_cast<char*>(request.body.data()),
static_cast<DWORD>(request.body.size()), static_cast<DWORD>(request.body.size()), 0);
if (!sent || !WinHttpReceiveResponse(call.value, nullptr)) {
response.error = GetLastError() == ERROR_WINHTTP_TIMEOUT ? "it took longer than 10 s" : "it failed (no answer, or its certificate was not valid)";
return response;
}
DWORD status = 0, size = sizeof(status);
WinHttpQueryHeaders(call.value, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &size,
WINHTTP_NO_HEADER_INDEX);
response.status = static_cast<int>(status);
for (;;) {
DWORD available = 0;
if (!WinHttpQueryDataAvailable(call.value, &available)) {
response.error = "its answer broke off";
break;
}
if (!available) break;
if (response.body.size() + available > most_bytes) {
response.error = "its answer is larger than 1 MB";
response.body.clear();
break;
}
const auto at = response.body.size();
response.body.resize(at + available);
DWORD read = 0;
if (!WinHttpReadData(call.value, response.body.data() + at, available, &read)) {
response.error = "its answer broke off";
break;
}
response.body.resize(at + read);
}
return response;
}
}
bool start(HttpRequest request) noexcept {
if (going.fetch_add(1) >= most_going) {
going.fetch_sub(1);
return false;
}
try {
std::thread([request = std::move(request)] {
HttpResponse response;
try {
response = run(request);
} catch (const std::exception& error) {
response = {request.id, request.plugin, 0, {}, error.what()};
}
{
std::lock_guard lock(mutex);
if (answers.size() < 256) answers.push_back(std::move(response));
}
going.fetch_sub(1);
}).detach();
return true;
} catch (...) {
going.fetch_sub(1);
return false;
}
}
std::vector<HttpResponse> take() noexcept {
std::lock_guard lock(mutex);
return std::exchange(answers, {});
}
}