mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
511 lines
15 KiB
C#
511 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using BestHTTP;
|
|
using HarmonyLib;
|
|
using Il2CppInterop.Runtime;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal static class HileGate
|
|
{
|
|
internal static bool Enabled;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal static int DroppedCount;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static bool DropEnabled { get; } = IsDropEnabled();
|
|
|
|
private static bool IsDropEnabled()
|
|
{
|
|
try
|
|
{
|
|
var raw = System.Environment.GetEnvironmentVariable("RADICADE_HILE_GATE");
|
|
if (string.IsNullOrWhiteSpace(raw)) return false;
|
|
return raw.Trim().ToLowerInvariant() == "drop";
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static bool _dumped;
|
|
|
|
public static bool ShouldDrop(HTTPRequest request)
|
|
{
|
|
if (!Enabled || request?.Uri == null) return false;
|
|
|
|
var path = request.Uri.AbsolutePath;
|
|
if (path == null ||
|
|
!path.EndsWith("/api/PlayerReporting/v1/hile", System.StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
|
|
Dump(request);
|
|
return DropEnabled;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void Dump(HTTPRequest request)
|
|
{
|
|
if (_dumped) return;
|
|
_dumped = true;
|
|
|
|
try
|
|
{
|
|
var body = "<unreadable>";
|
|
|
|
try
|
|
{
|
|
var raw = request.RawData;
|
|
if (raw != null && raw.Length > 0)
|
|
{
|
|
body = System.Text.Encoding.UTF8.GetString(raw);
|
|
if (body.Length > 2000) body = body.Substring(0, 2000) + "...[truncated]";
|
|
}
|
|
else
|
|
{
|
|
var entity = request.GetEntityBody();
|
|
if (entity != null && entity.Length > 0)
|
|
{
|
|
var text = System.Text.Encoding.UTF8.GetString(entity);
|
|
body = text + " (" + entity.Length + " bytes in the entity body)";
|
|
}
|
|
else
|
|
{
|
|
body = "<empty body>";
|
|
}
|
|
}
|
|
}
|
|
catch (System.Exception bodyEx)
|
|
{
|
|
body = "<body read failed: " + bodyEx.Message + ">";
|
|
}
|
|
|
|
var caller = "<unavailable>";
|
|
try { caller = System.Environment.StackTrace; } catch { }
|
|
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(
|
|
"[HileGate] anti-cheat report captured (full wire form):\n " + request.MethodType
|
|
+ " " + request.Uri + "\n body: " + body
|
|
+ "\n managed caller stack:" + caller);
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] capture failed: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HarmonyPatch(typeof(HTTPManager), "SendRequest", new[] { typeof(HTTPRequest) })]
|
|
internal static class NameserverConnectionPatches
|
|
{
|
|
|
|
[HarmonyPriority(Priority.Last)]
|
|
private static bool Prefix(HTTPRequest request, ref HTTPRequest __result)
|
|
{
|
|
if (RequestFunnel.Handle(request)) return true;
|
|
|
|
__result = request;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static class RequestFunnel
|
|
{
|
|
private static readonly HashSet<long> Seen = new();
|
|
|
|
private static bool IsSocketScheme(string url) =>
|
|
url.StartsWith("ws:", StringComparison.OrdinalIgnoreCase) ||
|
|
url.StartsWith("wss:", StringComparison.OrdinalIgnoreCase);
|
|
|
|
internal static bool Handle(HTTPRequest request)
|
|
{
|
|
if (HileGate.ShouldDrop(request))
|
|
{
|
|
HileGate.DroppedCount++;
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[HileGate] dropped anti-cheat report POST (no response will arrive).");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var uri = request?.Uri?.ToString();
|
|
if (!string.IsNullOrEmpty(uri))
|
|
Diag.Trace("[Net] " + request.MethodType + " " + uri);
|
|
}
|
|
catch
|
|
{
|
|
|
|
}
|
|
|
|
if (request?.Uri == null) return true;
|
|
|
|
if (!Seen.Add(request.Pointer.ToInt64())) return true;
|
|
|
|
var original = request.Uri.ToString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var rewritten = IsSocketScheme(original)
|
|
? NameserverRedirect.RewriteWebSocket(original, RadicadeEacBypassPlugin.ServerUrl.Value)
|
|
: NameserverRedirect.Rewrite(original, RadicadeEacBypassPlugin.ServerUrl.Value);
|
|
var withKeyId = ImageSignatureRequest.AddKeyId(rewritten, RadicadeEacBypassPlugin.ServerUrl.Value,
|
|
RadicadeEacBypassPlugin.ImageKeyId);
|
|
if (withKeyId != rewritten)
|
|
{
|
|
|
|
|
|
Diag.Trace("[ImageSig] stated key id on " + withKeyId);
|
|
}
|
|
rewritten = withKeyId;
|
|
|
|
if (rewritten != original)
|
|
{
|
|
request.Uri = new Il2CppSystem.Uri(rewritten);
|
|
AlignHostHeader(request, rewritten);
|
|
Diag.Trace("[Connection] Redirected nameserver request to the configured Radicade server.");
|
|
}
|
|
|
|
if (RealtimeDiagnostics.Verbose && IsSocketScheme(original))
|
|
{
|
|
try
|
|
{
|
|
var outgoing = new System.Text.StringBuilder("[Realtime] socket request: " + original);
|
|
var headers = request.Headers;
|
|
if (headers != null)
|
|
{
|
|
foreach (var header in headers)
|
|
{
|
|
outgoing.Append(" | ").Append(header.Key).Append("=");
|
|
var values = header.Value;
|
|
if (values != null)
|
|
foreach (var value in values) outgoing.Append(value).Append(",");
|
|
}
|
|
}
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(outgoing.ToString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket request dump failed: "
|
|
+ ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
|
|
HubSocketAuth.Capture(request);
|
|
HubSocketAuth.CaptureFromLoginResponse(request, request.Uri.ToString());
|
|
|
|
|
|
|
|
HubSocketAuth.AuthenticateHubRequest(request, request.Uri.ToString());
|
|
|
|
HubSocketAuth.AuthenticateServerRequest(request, request.Uri.ToString());
|
|
|
|
if (Diag.Verbose) AuthGrantTrace.Report(request);
|
|
HttpOutcomeTrace.Wrap(request);
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void AlignHostHeader(HTTPRequest request, string url)
|
|
{
|
|
if (request == null || string.IsNullOrEmpty(url)) return;
|
|
|
|
Uri target;
|
|
if (!Uri.TryCreate(url, UriKind.Absolute, out target) || string.IsNullOrEmpty(target.Host)) return;
|
|
|
|
var authority = target.IsDefaultPort ? target.Host : target.Host + ":" + target.Port;
|
|
try
|
|
{
|
|
var stated = request.GetHeaderValues("Host");
|
|
if (stated != null && stated.Count > 0)
|
|
{
|
|
if (stated.Count == 1 && stated[0] == authority) return;
|
|
|
|
request.RemoveHeader("Host");
|
|
}
|
|
|
|
request.SetHeader("Host", authority);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[Connection] could not align the Host header on " + url +
|
|
": " + ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class AuthGrantTrace
|
|
{
|
|
private static readonly HashSet<long> Reported = new();
|
|
|
|
private static readonly string[] Fields =
|
|
{
|
|
"account_id", "username", "password", "birthday", "avatar", "avatarJson",
|
|
"email", "platform_auth", "device_id", "device_class", "client_id", "ver"
|
|
};
|
|
|
|
internal static void Report(HTTPRequest request)
|
|
{
|
|
try
|
|
{
|
|
if (request?.Uri == null) return;
|
|
if (!request.Uri.ToString().EndsWith("/auth/connect/token", StringComparison.OrdinalIgnoreCase)) return;
|
|
if (!Reported.Add(request.Pointer.ToInt64())) return;
|
|
|
|
var body = ReadBody(request);
|
|
if (string.IsNullOrEmpty(body))
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] connect/token body was unreadable; the grant is unknown for this request.");
|
|
return;
|
|
}
|
|
|
|
string Value(string name)
|
|
{
|
|
foreach (var pair in body.Split('&'))
|
|
{
|
|
var separator = pair.IndexOf('=');
|
|
if (separator <= 0) continue;
|
|
if (!string.Equals(pair.Substring(0, separator), name, StringComparison.OrdinalIgnoreCase)) continue;
|
|
return pair.Substring(separator + 1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool Present(string name)
|
|
{
|
|
var value = Value(name);
|
|
return !string.IsNullOrWhiteSpace(value);
|
|
}
|
|
|
|
var grant = Value("grant_type") ?? "<absent>";
|
|
var present = new List<string>();
|
|
foreach (var field in Fields)
|
|
if (Present(field)) present.Add(field);
|
|
|
|
RadicadeEacBypassPlugin.LogSource.LogInfo("[AuthGrant] connect/token grant_type=" + grant
|
|
+ "; fields present: " + (present.Count == 0 ? "<none>" : string.Join(", ", present))
|
|
+ " (values omitted)");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[AuthGrant] could not read the grant: " + ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static string ReadBody(HTTPRequest request)
|
|
{
|
|
try
|
|
{
|
|
var raw = request.RawData;
|
|
if (raw != null && raw.Length > 0) return System.Text.Encoding.UTF8.GetString(raw);
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
var entity = request.GetEntityBody();
|
|
if (entity != null && entity.Length > 0) return System.Text.Encoding.UTF8.GetString(entity);
|
|
}
|
|
catch { }
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch]
|
|
internal static class SendRequestImplFunnelPatch
|
|
{
|
|
private static IEnumerable<MethodBase> TargetMethods()
|
|
{
|
|
var impl = AccessTools.Method(typeof(HTTPManager), "SendRequestImpl", new[] { typeof(HTTPRequest) });
|
|
return impl == null ? Enumerable.Empty<MethodBase>() : new MethodBase[] { impl };
|
|
}
|
|
|
|
private static void Prefix(object[] __args)
|
|
{
|
|
try
|
|
{
|
|
if (__args == null || __args.Length == 0) return;
|
|
RequestFunnel.Handle(__args[0] as HTTPRequest);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Diag.Warn("[Net] funnel trace failed: " + ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class HttpOutcomeTrace
|
|
{
|
|
private static readonly HashSet<long> Wrapped = new();
|
|
private static int _completed;
|
|
|
|
internal static void Wrap(HTTPRequest request)
|
|
{
|
|
if (request == null) return;
|
|
|
|
string url;
|
|
string method;
|
|
string path;
|
|
string query;
|
|
try
|
|
{
|
|
if (request.Uri == null) return;
|
|
url = Shorten(request.Uri.ToString());
|
|
method = request.MethodType.ToString();
|
|
path = request.Uri.AbsolutePath;
|
|
query = request.Uri.Query;
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!Wrapped.Add(request.Pointer.ToInt64())) return;
|
|
|
|
var original = request.Callback;
|
|
request.Callback = DelegateSupport.ConvertDelegate<OnRequestFinishedDelegate>(
|
|
new Action<HTTPRequest, HTTPResponse>((req, response) =>
|
|
{
|
|
try
|
|
{
|
|
HttpTolerancePatches.RepairResponse(response, path, query);
|
|
|
|
var completed = System.Threading.Interlocked.Increment(ref _completed);
|
|
var status = -1;
|
|
var bytes = 0;
|
|
var reason = string.Empty;
|
|
if (response != null)
|
|
{
|
|
status = response.StatusCode;
|
|
try { var data = response.Data; if (data != null) bytes = data.Length; } catch { }
|
|
try { if (!response.IsSuccess && !string.IsNullOrEmpty(response.Message)) reason = " reason=" + response.Message; } catch { }
|
|
}
|
|
|
|
Diag.Trace("[HttpStatus] #" + completed + " " + method + " " + url + " -> "
|
|
+ (response == null ? "NO RESPONSE" : status.ToString())
|
|
+ " bytes=" + bytes + reason);
|
|
|
|
if (RealtimeDiagnostics.Verbose && url.IndexOf("/notify/hub/v1", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
try
|
|
{
|
|
var answer = new System.Text.StringBuilder();
|
|
answer.Append("[Realtime] socket answer: status=").Append(response == null ? -1 : status)
|
|
.Append(" message=").Append(response == null ? "<none>" : response.Message);
|
|
var answerHeaders = response?.Headers;
|
|
if (answerHeaders != null)
|
|
{
|
|
foreach (var header in answerHeaders)
|
|
{
|
|
answer.Append(" | ").Append(header.Key).Append("=");
|
|
var values = header.Value;
|
|
if (values != null)
|
|
foreach (var value in values) answer.Append(value).Append(",");
|
|
}
|
|
}
|
|
string body = null;
|
|
try { body = response?.DataAsText; } catch { }
|
|
if (!string.IsNullOrEmpty(body))
|
|
answer.Append(" | body=").Append(body.Length > 200 ? body.Substring(0, 200) : body);
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning(answer.ToString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] socket answer dump failed: "
|
|
+ ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
|
|
}
|
|
finally
|
|
{
|
|
try { original?.Invoke(req, response); } catch { }
|
|
}
|
|
}));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Diag.Warn("[HttpStatus] could not instrument " + method + " " + url + ": "
|
|
+ ex.GetType().Name + ": " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static string Shorten(string url)
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri(url);
|
|
var path = uri.AbsolutePath;
|
|
if (path.StartsWith("/auth/cachedlogin/forplatformid/", StringComparison.OrdinalIgnoreCase))
|
|
return "/auth/cachedlogin/forplatformid/{redacted}";
|
|
return path + (string.IsNullOrEmpty(uri.Query) ? string.Empty : "?" + uri.Query.Length + " chars");
|
|
}
|
|
catch
|
|
{
|
|
return "<unparsable>";
|
|
}
|
|
}
|
|
}
|