216 lines
10 KiB
C++
216 lines
10 KiB
C++
// In-game news / version check (docs/multiplayer/news.md).
|
|
//
|
|
// The game asks http://motd.gamespy.com/motd/vercheck.asp?... through WinHTTP (not WSOCK32, so the
|
|
// gethostbyname redirect does not see it) once per start, and only when the registry value DistroId is
|
|
// non-zero. This file:
|
|
// - sends those requests to [Network] NewsUrl (default https://dsii.uk; http://127.0.0.x:port for a test server;
|
|
// empty = leave GameSpy's address alone) by hooking WinHttpConnect (server name and port) and
|
|
// WinHttpOpenRequest (adds WINHTTP_FLAG_SECURE for an https base). The path and query stay as the game
|
|
// built them, so the server sees /motd/vercheck.asp?productid=...
|
|
// - makes DS2Multiplayer's virtual product key serve a non-zero DistroId (SetupVirtualProductKey) so the check
|
|
// runs on Steam. Nothing else in the exe reads DistroId.
|
|
#include "multiplayer.h"
|
|
#include <shellapi.h>
|
|
#include <winhttp.h>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cwchar>
|
|
#include <mutex>
|
|
|
|
namespace {
|
|
|
|
using WinHttpConnect_t = HINTERNET(WINAPI*)(HINTERNET, LPCWSTR, INTERNET_PORT, DWORD);
|
|
using WinHttpOpenRequest_t = HINTERNET(WINAPI*)(HINTERNET, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR*, DWORD);
|
|
WinHttpConnect_t g_connect;
|
|
WinHttpOpenRequest_t g_openRequest;
|
|
|
|
wchar_t g_host[256];
|
|
INTERNET_PORT g_port;
|
|
bool g_secure;
|
|
|
|
constexpr size_t kMaxTracked = 16;
|
|
std::mutex g_lock;
|
|
size_t g_next;
|
|
|
|
bool HasSuffix(LPCWSTR name, const wchar_t* suffix) {
|
|
size_t n = name ? wcslen(name) : 0, k = wcslen(suffix);
|
|
return n > k && _wcsicmp(name + n - k, suffix) == 0;
|
|
}
|
|
|
|
bool g_newsScreen = true; // [Network] NewsScreen: also Broken World's News screen (www.gaspowered.com/gasservices)
|
|
|
|
enum Kind : unsigned char { kNone = 0, kGamespy = 1, kGas = 2 }; // what a redirected connection was meant for
|
|
|
|
// motd.gamespy.com (the version check) and, with NewsScreen, www.gaspowered.com (Broken World's News screen).
|
|
Kind HostKind(LPCWSTR name) {
|
|
if (HasSuffix(name, L".gamespy.com")) return kGamespy;
|
|
if (g_newsScreen && HasSuffix(name, L".gaspowered.com")) return kGas;
|
|
return kNone;
|
|
}
|
|
|
|
struct Tracked {
|
|
HINTERNET h;
|
|
Kind kind;
|
|
};
|
|
Tracked g_redirected[kMaxTracked];
|
|
|
|
void Track(HINTERNET h, Kind kind) {
|
|
std::lock_guard<std::mutex> g(g_lock);
|
|
for (auto& e : g_redirected)
|
|
if (e.h == h) e = Tracked{}; // a recycled handle value must not keep an old meaning
|
|
if (kind != kNone && h) {
|
|
g_redirected[g_next] = Tracked{h, kind};
|
|
g_next = (g_next + 1) % kMaxTracked;
|
|
}
|
|
}
|
|
|
|
Kind KindOf(HINTERNET h) {
|
|
std::lock_guard<std::mutex> g(g_lock);
|
|
for (auto& e : g_redirected)
|
|
if (e.h == h) return e.kind;
|
|
return kNone;
|
|
}
|
|
|
|
HINTERNET WINAPI HookConnect(HINTERNET session, LPCWSTR server, INTERNET_PORT port, DWORD reserved) {
|
|
Kind kind = HostKind(server);
|
|
if (kind) ds2::log().Write("news: %ls:%u -> %ls:%u%s", server, port, g_host, g_port, g_secure ? " (https)" : "");
|
|
HINTERNET h = g_connect(session, kind ? g_host : server, kind ? g_port : port, reserved);
|
|
Track(h, kind);
|
|
return h;
|
|
}
|
|
|
|
// Broken World's News screen asks /gasservices?func=getCurrentServer (the news), and its Registration tab sends the
|
|
// player's e-mail address and postal code to the same host (func=regNewsletter). Only the news request is ever
|
|
// forwarded: anything else on that connection fails to open, as it does today with the host gone, so those details
|
|
// never leave the PC (and never reach our server).
|
|
bool GasRequestAllowed(LPCWSTR object) {
|
|
static const wchar_t kNews[] = L"/gasservices?func=getCurrentServer";
|
|
static const wchar_t kMessages[] = L"/gasservices?func=currentMessages";
|
|
return object && (wcsncmp(object, kNews, wcslen(kNews)) == 0 || wcsncmp(object, kMessages, wcslen(kMessages)) == 0);
|
|
}
|
|
|
|
HINTERNET WINAPI HookOpenRequest(HINTERNET connect, LPCWSTR verb, LPCWSTR object, LPCWSTR version, LPCWSTR referrer,
|
|
LPCWSTR* accept, DWORD flags) {
|
|
wchar_t path[600];
|
|
Kind kind = KindOf(connect);
|
|
if (kind == kGas && !GasRequestAllowed(object)) {
|
|
ds2::log().Write("news: refused a request to the old Gas Powered Games host (only the news is forwarded; nothing is sent): path starts %.24ls", object ? object : L"(none)");
|
|
SetLastError(ERROR_WINHTTP_CANNOT_CONNECT);
|
|
return nullptr;
|
|
}
|
|
if (kind != kNone) {
|
|
// https base: TLS for everything that goes there; http base (a test server): none, even if the game asked for it
|
|
flags = g_secure ? (flags | WINHTTP_FLAG_SECURE) : (flags & ~static_cast<DWORD>(WINHTTP_FLAG_SECURE));
|
|
// Mark the check as the plugin's (a hosts-file player's game would start its own downloader on the news link)
|
|
// and say which item was read; the query is the game's own, so the marker is appended with '&'.
|
|
if (kind == kGamespy && object && wcsncmp(object, L"/motd/vercheck.asp?", 19) == 0 && wcslen(object) < 400) {
|
|
char seen[48];
|
|
mp::ReadSeen(seen, sizeof(seen));
|
|
wchar_t id[48] = {};
|
|
for (int i = 0; seen[i] && i < 40; ++i)
|
|
id[i] = (seen[i] >= '0' && seen[i] <= '9') || (seen[i] >= 'a' && seen[i] <= 'z') || seen[i] == '-' ? seen[i] : L'_';
|
|
swprintf_s(path, L"%s&rv=1&seen=%s", object, id);
|
|
object = path;
|
|
}
|
|
}
|
|
return g_openRequest(connect, verb, object, version, referrer, accept, flags);
|
|
}
|
|
|
|
// Private test instances (DS2DEV_DOCS): never start a browser or another program on the PC of whoever runs the test.
|
|
bool Harness() { return GetEnvironmentVariableA("DS2DEV_DOCS", nullptr, 0) != 0; }
|
|
|
|
HINSTANCE WINAPI GuardShellExecuteA(HWND, LPCSTR, LPCSTR file, LPCSTR params, LPCSTR, INT) {
|
|
ds2::log().Write("news: [test instance] ShellExecute blocked: %s %s", file ? file : "", params ? params : "");
|
|
return reinterpret_cast<HINSTANCE>(33);
|
|
}
|
|
|
|
BOOL WINAPI GuardShellExecuteExA(SHELLEXECUTEINFOA* info) {
|
|
ds2::log().Write("news: [test instance] ShellExecuteEx blocked: %s %s", info && info->lpFile ? info->lpFile : "",
|
|
info && info->lpParameters ? info->lpParameters : "");
|
|
if (info) { info->hInstApp = reinterpret_cast<HINSTANCE>(33); info->hProcess = nullptr; }
|
|
return TRUE;
|
|
}
|
|
|
|
using CreateProcessA_t = BOOL(WINAPI*)(LPCSTR, LPSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCSTR,
|
|
LPSTARTUPINFOA, LPPROCESS_INFORMATION);
|
|
CreateProcessA_t g_createProcess;
|
|
|
|
BOOL WINAPI GuardCreateProcessA(LPCSTR app, LPSTR cmd, LPSECURITY_ATTRIBUTES pa, LPSECURITY_ATTRIBUTES ta, BOOL inherit,
|
|
DWORD flags, LPVOID env, LPCSTR dir, LPSTARTUPINFOA si, LPPROCESS_INFORMATION pi) {
|
|
const char* what = cmd ? cmd : (app ? app : "");
|
|
if (strstr(what, "rundll32") || strstr(what, "url.dll") || strstr(what, "http")) {
|
|
ds2::log().Write("news: [test instance] CreateProcess blocked: %s", what);
|
|
SetLastError(ERROR_ACCESS_DENIED);
|
|
return FALSE;
|
|
}
|
|
return g_createProcess(app, cmd, pa, ta, inherit, flags, env, dir, si, pi);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace mp {
|
|
|
|
// "https://dsii.uk", "http://127.0.0.91:8091", optional trailing slash. False if off or unusable.
|
|
bool ParseNewsUrl(const char* url, wchar_t* host, size_t cap, unsigned short* port, bool* secure) {
|
|
if (!url || !*url) return false;
|
|
const char* p;
|
|
if (_strnicmp(url, "https://", 8) == 0) { p = url + 8; *secure = true; }
|
|
else if (_strnicmp(url, "http://", 7) == 0) { p = url + 7; *secure = false; }
|
|
else return false;
|
|
size_t n = 0;
|
|
while (p[n] && p[n] != '/' && p[n] != ':') ++n;
|
|
if (!n || n >= cap) return false;
|
|
for (size_t i = 0; i < n; ++i) host[i] = static_cast<unsigned char>(p[i]);
|
|
host[n] = 0;
|
|
long v = *secure ? 443 : 80;
|
|
if (p[n] == ':') {
|
|
v = strtol(p + n + 1, nullptr, 10);
|
|
if (v < 1 || v > 65535) return false;
|
|
}
|
|
*port = static_cast<unsigned short>(v);
|
|
return true;
|
|
}
|
|
|
|
bool NewsEnabled(const ds2::Ini& ini) {
|
|
char url[300];
|
|
ini.String("Network", "NewsUrl", "https://dsii.uk", url, sizeof(url));
|
|
wchar_t host[256];
|
|
unsigned short port;
|
|
bool secure;
|
|
return ParseNewsUrl(url, host, 256, &port, &secure);
|
|
}
|
|
|
|
void InstallNews(const ds2::Ini& ini, ds2::Exe exe) {
|
|
if (Harness()) { // private test instances: Broken World's "More" button must not start a browser
|
|
ds2::HookImport("SHELL32.dll", "ShellExecuteA", reinterpret_cast<void*>(GuardShellExecuteA), "ShellExecuteA (test instance guard)");
|
|
ds2::HookImport("SHELL32.dll", "ShellExecuteExA", reinterpret_cast<void*>(GuardShellExecuteExA), "ShellExecuteExA (test instance guard)");
|
|
g_createProcess = static_cast<CreateProcessA_t>(ds2::HookImport("KERNEL32.dll", "CreateProcessA", reinterpret_cast<void*>(GuardCreateProcessA),
|
|
"CreateProcessA (test instance guard)"));
|
|
}
|
|
char url[300];
|
|
ini.String("Network", "NewsUrl", "https://dsii.uk", url, sizeof(url));
|
|
if (!url[0]) {
|
|
ds2::log().Write("news: NewsUrl empty - the version check keeps GameSpy's address");
|
|
return;
|
|
}
|
|
g_newsScreen = ini.Bool("Network", "NewsScreen", true);
|
|
if (!ParseNewsUrl(url, g_host, 256, &g_port, &g_secure)) {
|
|
ds2::log().Write("news: NewsUrl '%s' is not an http:// or https:// address - ignored", url);
|
|
return;
|
|
}
|
|
// The request hook first: a redirected connection without it would speak plain HTTP to a TLS port.
|
|
g_openRequest = static_cast<WinHttpOpenRequest_t>(
|
|
ds2::HookImport("WINHTTP.dll", "WinHttpOpenRequest", reinterpret_cast<void*>(HookOpenRequest), "WinHttpOpenRequest (news redirect)"));
|
|
if (!g_openRequest) return;
|
|
g_connect = static_cast<WinHttpConnect_t>(
|
|
ds2::HookImport("WINHTTP.dll", "WinHttpConnect", reinterpret_cast<void*>(HookConnect), "WinHttpConnect (news redirect)"));
|
|
if (!g_connect) return;
|
|
ds2::log().Write("news: version check goes to %ls:%u (%s)", g_host, g_port, g_secure ? "https" : "http");
|
|
NewsTarget target;
|
|
wcscpy_s(target.host, g_host);
|
|
target.port = g_port;
|
|
target.secure = g_secure;
|
|
InstallNewsUi(ini, exe, target);
|
|
}
|
|
|
|
} // namespace mp
|