Files
Racoon-MinecraftConsoles/Minecraft.Client/Windows64/Windows64_LceLiveSignaling.cpp
veroxsity de125e5275 Add TCP-over-WebSocket relay client for Minecraft
- Implemented Win64LceLiveRelay.h and Win64LceLiveSignaling.cpp to facilitate TCP-over-WebSocket communication for Minecraft, allowing game traffic to route through the LCELive relay server when direct TCP is blocked.
- Introduced signaling mechanisms for host and joiner connections, including session management and candidate exchange.
- Added logging functionality in Windows_Log.cpp and Windows_Log.h for better debugging and session tracking.
- Created build-release.bat script for streamlined build and deployment process, including exclusion of unnecessary files.
2026-04-17 23:47:32 +01:00

725 lines
21 KiB
C++

#include "stdafx.h"
#ifdef _WINDOWS64
#include <winsock2.h>
#include <ws2tcpip.h>
#include <winhttp.h>
#include <objbase.h> // CoCreateGuid
#include "Windows64_LceLiveSignaling.h"
#include "Windows64_LceLive.h"
#include "Windows64_LceLiveP2P.h"
#include "Windows64_Log.h"
#include "../../../Minecraft.Server/vendor/nlohmann/json.hpp"
#include <cstdio>
#include <string>
#include <vector>
#pragma comment(lib, "Winhttp.lib")
// ============================================================================
// Internal implementation
// ============================================================================
namespace
{
using Json = nlohmann::json;
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
std::wstring Utf8ToWideLocal(const std::string& s)
{
if (s.empty()) return L"";
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
if (n <= 0) return L"";
std::wstring out(static_cast<size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], n);
if (!out.empty() && out.back() == L'\0') out.pop_back();
return out;
}
std::string WideToUtf8Local(const std::wstring& s)
{
if (s.empty()) return "";
const int n = WideCharToMultiByte(CP_UTF8, 0, s.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (n <= 0) return "";
std::string out(static_cast<size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, s.c_str(), -1, &out[0], n, nullptr, nullptr);
if (!out.empty() && out.back() == '\0') out.pop_back();
return out;
}
// Generate a lowercase hyphenated UUID string (e.g. "550e8400-e29b-41d4-a716-446655440000").
std::string GenerateUuid()
{
GUID guid = {};
CoCreateGuid(&guid);
char buf[40] = {};
snprintf(buf, sizeof(buf),
"%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
guid.Data1, guid.Data2, guid.Data3,
guid.Data4[0], guid.Data4[1],
guid.Data4[2], guid.Data4[3], guid.Data4[4],
guid.Data4[5], guid.Data4[6], guid.Data4[7]);
return buf;
}
// Read the API base URL the same way Windows64_LceLive.cpp does:
// LCELIVE_API_BASE_URL env var → lcelive.properties → localhost:5187.
std::string GetBaseUrl()
{
char envValue[512] = {};
const DWORD len = GetEnvironmentVariableA("LCELIVE_API_BASE_URL", envValue,
static_cast<DWORD>(sizeof(envValue)));
if (len > 0 && len < sizeof(envValue))
return std::string(envValue);
// Try lcelive.properties next to the .exe
char exePath[MAX_PATH] = {};
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
std::string props(exePath);
const size_t lastSlash = props.find_last_of("\\/");
if (lastSlash != std::string::npos)
props = props.substr(0, lastSlash + 1);
props += "lcelive.properties";
FILE* f = nullptr;
if (fopen_s(&f, props.c_str(), "rb") == 0 && f != nullptr)
{
char line[512] = {};
while (fgets(line, sizeof(line), f) != nullptr)
{
std::string s(line);
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
if (s.substr(0, 12) == "api_base_url")
{
const size_t eq = s.find('=');
if (eq != std::string::npos) { fclose(f); return s.substr(eq + 1); }
}
}
fclose(f);
}
return "http://localhost:5187";
}
// URL-percent-encode a string (for safe use in query parameters).
std::string UrlEncode(const std::string& s)
{
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s)
{
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
out += static_cast<char>(c);
else
{
char hex[4] = {};
snprintf(hex, sizeof(hex), "%%%02X", c);
out += hex;
}
}
return out;
}
// Build the JSON candidate message we send over the signaling channel.
std::string BuildCandidateJson(const std::string& ip, int port,
Win64LceLiveP2P::EConnMethod method)
{
const char* methodStr =
(method == Win64LceLiveP2P::EConnMethod::UPnP) ? "upnp" : "stun";
Json j;
j["type"] = "candidate";
j["ip"] = ip;
j["port"] = port;
j["method"] = methodStr;
return j.dump();
}
// -------------------------------------------------------------------------
// Worker context — passed to the background thread
// -------------------------------------------------------------------------
struct WorkerContext
{
bool isHost;
std::string sessionId;
std::string ourIp;
int ourPort;
Win64LceLiveP2P::EConnMethod ourMethod;
std::string accessToken;
std::string baseUrl;
// Written by worker to signal results back to main thread.
volatile bool workerDone;
bool wsConnected; // WebSocket opened successfully
bool peerReceived; // Peer candidate decoded
std::string peerIp;
int peerPort;
bool peerNeedsHolePunch;
std::string errorMessage;
// Handle the main thread can close to unblock a stuck WinHttpWebSocketReceive.
volatile HINTERNET wsHandle;
};
// -------------------------------------------------------------------------
// Runtime state
// -------------------------------------------------------------------------
struct SignalingState
{
bool initialized;
CRITICAL_SECTION lock;
Win64LceLiveSignaling::ESignalingState state;
std::string sessionId;
std::string peerIp;
int peerPort;
bool peerNeedsHolePunch;
std::string lastError;
// Set by PrepareJoin(); cleared when JoinerConnect() fires.
std::string pendingJoinerSessionId;
HANDLE workerThread;
WorkerContext* workerCtx;
};
static SignalingState g_sig = {};
static INIT_ONCE g_initOnce = INIT_ONCE_STATIC_INIT;
BOOL CALLBACK InitSignalingState(PINIT_ONCE, PVOID, PVOID*)
{
InitializeCriticalSection(&g_sig.lock);
g_sig.state = Win64LceLiveSignaling::ESignalingState::Idle;
g_sig.initialized = true;
return TRUE;
}
void EnsureInitialized()
{
InitOnceExecuteOnce(&g_initOnce, &InitSignalingState, nullptr, nullptr);
}
// -------------------------------------------------------------------------
// WebSocket worker thread
// -------------------------------------------------------------------------
DWORD WINAPI SignalingWorkerProc(LPVOID param)
{
WorkerContext* ctx = static_cast<WorkerContext*>(param);
// ---- Parse the base URL ----
const std::wstring baseUrlW = Utf8ToWideLocal(ctx->baseUrl);
std::vector<wchar_t> hostBuf(256, 0);
std::vector<wchar_t> pathBuf(2048, 0);
URL_COMPONENTSW components = {};
components.dwStructSize = sizeof(components);
components.lpszHostName = hostBuf.data();
components.dwHostNameLength = static_cast<DWORD>(hostBuf.size());
components.lpszUrlPath = pathBuf.data();
components.dwUrlPathLength = static_cast<DWORD>(pathBuf.size());
if (!WinHttpCrackUrl(baseUrlW.c_str(), static_cast<DWORD>(baseUrlW.size()), 0, &components))
{
ctx->errorMessage = "Signaling: WinHttpCrackUrl failed for base URL";
ctx->workerDone = true;
return 0;
}
const bool secure = (components.nScheme == INTERNET_SCHEME_HTTPS);
const std::wstring hostW(components.lpszHostName, components.dwHostNameLength);
// Build the WebSocket path including query params.
// Auth goes in the Authorization header (server prefers that over ?token=).
const std::wstring basePath = components.lpszUrlPath
? std::wstring(components.lpszUrlPath, components.dwUrlPathLength)
: L"";
const std::wstring wsPath = basePath + L"/api/signaling/ws"
+ L"?sessionId=" + Utf8ToWideLocal(ctx->sessionId)
+ L"&role=" + Utf8ToWideLocal(ctx->isHost ? "host" : "joiner");
LCELOG("SIG", "connecting to %s%ls (role=%s sessionId=%s)",
secure ? "wss://" : "ws://",
(hostW + wsPath).c_str(),
ctx->isHost ? "host" : "joiner",
ctx->sessionId.c_str());
// ---- Open WinHTTP session ----
HINTERNET hSession = WinHttpOpen(
L"MCLCE-LceLive/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (hSession == nullptr)
{
ctx->errorMessage = "Signaling: WinHttpOpen failed";
ctx->workerDone = true;
return 0;
}
WinHttpSetTimeouts(hSession, 10000, 10000, 30000, 30000);
HINTERNET hConnect = WinHttpConnect(hSession, hostW.c_str(), components.nPort, 0);
if (hConnect == nullptr)
{
WinHttpCloseHandle(hSession);
ctx->errorMessage = "Signaling: WinHttpConnect failed";
ctx->workerDone = true;
return 0;
}
// ---- Open GET request (will be upgraded to WebSocket) ----
const DWORD flags = secure ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(
hConnect,
L"GET",
wsPath.c_str(),
nullptr,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
flags);
if (hRequest == nullptr)
{
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->errorMessage = "Signaling: WinHttpOpenRequest failed";
ctx->workerDone = true;
return 0;
}
// Mark this request for WebSocket upgrade BEFORE sending.
WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0);
// Add Authorization header.
if (!ctx->accessToken.empty())
{
const std::wstring authHeader =
L"Authorization: Bearer " + Utf8ToWideLocal(ctx->accessToken);
WinHttpAddRequestHeaders(hRequest,
authHeader.c_str(),
static_cast<DWORD>(authHeader.size()),
WINHTTP_ADDREQ_FLAG_ADD);
}
// Send the upgrade request.
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
{
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->errorMessage = "Signaling: WinHttpSendRequest failed (WSA "
+ std::to_string(GetLastError()) + ")";
ctx->workerDone = true;
return 0;
}
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->errorMessage = "Signaling: WinHttpReceiveResponse failed — server may be down";
ctx->workerDone = true;
return 0;
}
// ---- Complete WebSocket upgrade ----
HINTERNET hWs = WinHttpWebSocketCompleteUpgrade(hRequest, 0);
WinHttpCloseHandle(hRequest);
if (hWs == nullptr)
{
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->errorMessage = "Signaling: WebSocket upgrade failed — server returned non-101";
ctx->workerDone = true;
return 0;
}
// Make the handle visible to the main thread so Close() can abort the receive.
ctx->wsHandle = hWs;
ctx->wsConnected = true;
LCELOG("SIG", "WebSocket connected (session %s)", ctx->sessionId.c_str());
// ---- Send our P2P candidate ----
const std::string candidateJson =
BuildCandidateJson(ctx->ourIp, ctx->ourPort, ctx->ourMethod);
DWORD sendResult = WinHttpWebSocketSend(
hWs,
WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE,
const_cast<PVOID>(reinterpret_cast<const void*>(candidateJson.c_str())),
static_cast<DWORD>(candidateJson.size()));
if (sendResult != ERROR_SUCCESS)
{
WinHttpWebSocketClose(hWs, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
WinHttpCloseHandle(hWs);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->wsHandle = nullptr;
ctx->errorMessage = "Signaling: failed to send candidate";
ctx->workerDone = true;
return 0;
}
LCELOG("SIG", "candidate sent %s", candidateJson.c_str());
// ---- Receive loop: wait for peer's candidate ----
// We also pass through joiner_connected notifications (host only) so the
// log shows when the joiner arrives.
std::vector<BYTE> recvBuf(8192);
bool done = false;
while (!done)
{
DWORD bytesRead = 0;
WINHTTP_WEB_SOCKET_BUFFER_TYPE bufType = WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE;
const DWORD recvErr = WinHttpWebSocketReceive(
hWs,
recvBuf.data(),
static_cast<DWORD>(recvBuf.size() - 1),
&bytesRead,
&bufType);
if (recvErr != ERROR_SUCCESS)
{
// Closed from main thread (Close() called) or network error.
LCELOG("SIG", "receive ended (%lu)", recvErr);
break;
}
if (bufType != WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE &&
bufType != WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE)
continue; // binary or close frame — ignore
recvBuf[bytesRead] = 0;
const std::string msg(reinterpret_cast<char*>(recvBuf.data()), bytesRead);
LCELOG("SIG", "recv %s", msg.c_str());
try
{
const Json j = Json::parse(msg);
const std::string type = j.value("type", "");
if (type == "joiner_connected")
{
// Host-only: the joiner has arrived on the signaling channel.
// Their candidate will follow shortly.
LCELOG("SIG", "joiner connected on session %s", ctx->sessionId.c_str());
}
else if (type == "candidate")
{
const std::string peerIp = j.value("ip", "");
const int peerPort = j.value("port", 0);
const std::string methodStr = j.value("method", "stun");
const bool holePunch = (methodStr == "stun");
if (!peerIp.empty() && peerPort > 0)
{
ctx->peerIp = peerIp;
ctx->peerPort = peerPort;
ctx->peerNeedsHolePunch = holePunch;
ctx->peerReceived = true;
LCELOG("SIG", "peer endpoint %s:%d (method=%s)",
peerIp.c_str(), peerPort, methodStr.c_str());
done = true; // Both candidates exchanged — we're done here.
}
}
}
catch (...) {} // Malformed JSON — skip
}
// ---- Clean up ----
WinHttpWebSocketClose(hWs, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
WinHttpCloseHandle(hWs);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
ctx->wsHandle = nullptr;
ctx->workerDone = true;
return 0;
}
// -------------------------------------------------------------------------
// Common start helper (host & joiner share 90% of setup)
// -------------------------------------------------------------------------
bool StartWorker(bool isHost,
const std::string& sessionId,
const std::string& externalIp, int externalPort,
Win64LceLiveP2P::EConnMethod method)
{
EnsureInitialized();
EnterCriticalSection(&g_sig.lock);
if (g_sig.state != Win64LceLiveSignaling::ESignalingState::Idle)
{
LeaveCriticalSection(&g_sig.lock);
return false;
}
WorkerContext* ctx = new WorkerContext();
ctx->isHost = isHost;
ctx->sessionId = sessionId;
ctx->ourIp = externalIp;
ctx->ourPort = externalPort;
ctx->ourMethod = method;
ctx->accessToken = Win64LceLive::GetAccessToken();
ctx->baseUrl = GetBaseUrl();
ctx->workerDone = false;
ctx->wsConnected = false;
ctx->peerReceived = false;
ctx->peerPort = 0;
ctx->peerNeedsHolePunch = false;
ctx->wsHandle = nullptr;
g_sig.state = Win64LceLiveSignaling::ESignalingState::Connecting;
g_sig.sessionId = sessionId;
g_sig.peerPort = 0;
g_sig.lastError.clear();
g_sig.peerIp.clear();
g_sig.workerCtx = ctx;
g_sig.workerThread = CreateThread(nullptr, 0, &SignalingWorkerProc, ctx, 0, nullptr);
if (g_sig.workerThread == nullptr)
{
g_sig.state = Win64LceLiveSignaling::ESignalingState::Failed;
g_sig.lastError = "Signaling: failed to create worker thread";
delete ctx;
g_sig.workerCtx = nullptr;
LeaveCriticalSection(&g_sig.lock);
return false;
}
LeaveCriticalSection(&g_sig.lock);
return true;
}
} // anonymous namespace
// ============================================================================
// Public API
// ============================================================================
namespace Win64LceLiveSignaling
{
bool HostConnect(const std::string& externalIp, int externalPort,
Win64LceLiveP2P::EConnMethod method)
{
const std::string sessionId = GenerateUuid();
LCELOG("SIG", "hosting session %s endpoint %s:%d",
sessionId.c_str(), externalIp.c_str(), externalPort);
return StartWorker(true, sessionId, externalIp, externalPort, method);
}
bool JoinerConnect(const std::string& sessionId,
const std::string& externalIp, int externalPort,
Win64LceLiveP2P::EConnMethod method)
{
// Clear the pending ID — we're acting on it now.
EnterCriticalSection(&g_sig.lock);
g_sig.pendingJoinerSessionId.clear();
LeaveCriticalSection(&g_sig.lock);
LCELOG("SIG", "joining session %s our endpoint %s:%d",
sessionId.c_str(), externalIp.c_str(), externalPort);
return StartWorker(false, sessionId, externalIp, externalPort, method);
}
void PrepareJoin(const std::string& sessionId)
{
EnsureInitialized();
EnterCriticalSection(&g_sig.lock);
g_sig.pendingJoinerSessionId = sessionId;
LeaveCriticalSection(&g_sig.lock);
LCELOG("SIG", "joiner session ID stored (%s) — waiting for P2P", sessionId.c_str());
}
std::string GetPendingJoinerSessionId()
{
EnsureInitialized();
EnterCriticalSection(&g_sig.lock);
const std::string id = g_sig.pendingJoinerSessionId;
LeaveCriticalSection(&g_sig.lock);
return id;
}
void Tick()
{
EnsureInitialized();
EnterCriticalSection(&g_sig.lock);
if (g_sig.workerCtx == nullptr ||
(g_sig.state != ESignalingState::Connecting &&
g_sig.state != ESignalingState::Connected))
{
LeaveCriticalSection(&g_sig.lock);
return;
}
WorkerContext* ctx = g_sig.workerCtx;
// Promote Connecting → Connected once WebSocket is open.
if (g_sig.state == ESignalingState::Connecting && ctx->wsConnected)
g_sig.state = ESignalingState::Connected;
// Integrate completed peer exchange.
if (ctx->workerDone)
{
HANDLE t = g_sig.workerThread;
g_sig.workerThread = nullptr;
LeaveCriticalSection(&g_sig.lock);
if (t != nullptr)
{
WaitForSingleObject(t, INFINITE);
CloseHandle(t);
}
EnterCriticalSection(&g_sig.lock);
if (ctx->peerReceived)
{
g_sig.state = ESignalingState::PeerKnown;
g_sig.peerIp = ctx->peerIp;
g_sig.peerPort = ctx->peerPort;
g_sig.peerNeedsHolePunch = ctx->peerNeedsHolePunch;
LCELOG("SIG", "peer known %s:%d (holePunch=%d)",
ctx->peerIp.c_str(), ctx->peerPort, ctx->peerNeedsHolePunch ? 1 : 0);
}
else
{
g_sig.state = ESignalingState::Failed;
g_sig.lastError = ctx->errorMessage.empty()
? "Signaling: connection closed before peer candidate received"
: ctx->errorMessage;
LCELOG("SIG", "failed — %s", g_sig.lastError.c_str());
}
delete ctx;
g_sig.workerCtx = nullptr;
}
LeaveCriticalSection(&g_sig.lock);
}
SignalingSnapshot GetSnapshot()
{
EnsureInitialized();
Tick();
SignalingSnapshot snap = {};
EnterCriticalSection(&g_sig.lock);
snap.state = g_sig.state;
snap.sessionId = g_sig.sessionId;
snap.peerIp = g_sig.peerIp;
snap.peerPort = g_sig.peerPort;
snap.peerNeedsHolePunch = g_sig.peerNeedsHolePunch;
switch (g_sig.state)
{
case ESignalingState::Idle:
snap.statusMessage = L"Signaling: idle.";
break;
case ESignalingState::Connecting:
snap.statusMessage = L"Signaling: connecting to relay...";
break;
case ESignalingState::Connected:
snap.statusMessage = L"Signaling: waiting for peer...";
break;
case ESignalingState::PeerKnown:
{
wchar_t buf[256] = {};
swprintf_s(buf, L"Signaling: peer at %hs:%d (%s)",
g_sig.peerIp.c_str(), g_sig.peerPort,
g_sig.peerNeedsHolePunch ? L"hole punch" : L"direct");
snap.statusMessage = buf;
break;
}
case ESignalingState::Failed:
snap.statusMessage = L"Signaling: failed.";
snap.errorMessage = std::wstring(g_sig.lastError.begin(), g_sig.lastError.end());
break;
case ESignalingState::Closed:
snap.statusMessage = L"Signaling: closed.";
break;
}
LeaveCriticalSection(&g_sig.lock);
return snap;
}
void Close()
{
EnsureInitialized();
// Unblock any in-progress WinHttpWebSocketReceive by closing the handle.
HINTERNET wsToClose = nullptr;
EnterCriticalSection(&g_sig.lock);
if (g_sig.workerCtx != nullptr)
wsToClose = g_sig.workerCtx->wsHandle;
LeaveCriticalSection(&g_sig.lock);
if (wsToClose != nullptr)
{
// Sending a Close frame unblocks the receive on the worker thread.
WinHttpWebSocketClose(wsToClose,
WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
}
// Now wait for the worker to finish.
HANDLE t = nullptr;
EnterCriticalSection(&g_sig.lock);
t = g_sig.workerThread;
LeaveCriticalSection(&g_sig.lock);
if (t != nullptr)
{
WaitForSingleObject(t, 5000);
CloseHandle(t);
}
EnterCriticalSection(&g_sig.lock);
if (g_sig.workerCtx != nullptr)
{
delete g_sig.workerCtx;
g_sig.workerCtx = nullptr;
}
g_sig.workerThread = nullptr;
g_sig.state = ESignalingState::Closed;
g_sig.sessionId.clear();
g_sig.peerIp.clear();
g_sig.peerPort = 0;
g_sig.peerNeedsHolePunch = false;
g_sig.lastError.clear();
g_sig.pendingJoinerSessionId.clear();
LeaveCriticalSection(&g_sig.lock);
LCELOG("SIG", "closed");
}
} // namespace Win64LceLiveSignaling
#endif // _WINDOWS64