mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
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>";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user