mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
165 lines
6.2 KiB
C#
165 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
|
|
/// <summary>
|
|
/// Pure string surgery for two responses the client cannot cope with as the
|
|
/// server sends them. No engine or BestHTTP types live here so the rules can be
|
|
/// exercised outside the game.
|
|
/// </summary>
|
|
internal static class ResponseRepair
|
|
{
|
|
/// <summary>Loopback probe target: any transport fails (or answers) instantly, so a probe phase can never stall.</summary>
|
|
internal const string LoopbackProbeAddress = "127.0.0.1:50000";
|
|
|
|
private const string QosPathSuffix = "/matchmaking/player/qos";
|
|
private const string UserSegmentPathSuffix = "/data/commonquery/usersegment";
|
|
private const string AddressKey = "\"address\"";
|
|
|
|
/// <summary>
|
|
/// BestHTTP's chunked reader throws exactly this when the chunk-size line is
|
|
/// empty, which is what a keep-alive connection the server had already closed
|
|
/// looks like. It is not a bad request, so it must not fail one.
|
|
/// </summary>
|
|
internal static bool IsTruncatedChunkFailure(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message)) return false;
|
|
return message.IndexOf("as a hex number", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rewrites a response body when the client would otherwise mis-handle it.
|
|
/// Returns null when the body is left alone; <paramref name="note"/> explains
|
|
/// what changed when it did.
|
|
/// </summary>
|
|
internal static string RepairBody(string path, string query, string body, out string note)
|
|
{
|
|
note = null;
|
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(body)) return null;
|
|
|
|
if (EndsWith(path, QosPathSuffix)) return RepairProbeList(body, ref note);
|
|
if (EndsWith(path, UserSegmentPathSuffix)) return RepairUserSegment(query, body, ref note);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The probe list points at retired GCP addresses: five of its six entries are
|
|
/// unreachable, so the client's region-probe phase waits out a ping timeout on
|
|
/// each boot and login sits there. Pinning every entry at loopback keeps the
|
|
/// list's shape but makes the probes answer (or refuse) immediately.
|
|
/// </summary>
|
|
private static string RepairProbeList(string body, ref string note)
|
|
{
|
|
var repaired = new StringBuilder(body.Length);
|
|
var index = 0;
|
|
var replaced = 0;
|
|
|
|
while (true)
|
|
{
|
|
var key = body.IndexOf(AddressKey, index, StringComparison.Ordinal);
|
|
if (key < 0)
|
|
{
|
|
repaired.Append(body, index, body.Length - index);
|
|
break;
|
|
}
|
|
|
|
repaired.Append(body, index, key - index + AddressKey.Length);
|
|
|
|
var cursor = key + AddressKey.Length;
|
|
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
|
|
if (cursor >= body.Length || body[cursor] != ':')
|
|
{
|
|
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
|
|
index = cursor;
|
|
continue;
|
|
}
|
|
|
|
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
|
|
repaired.Append(':');
|
|
cursor++;
|
|
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
|
|
if (cursor >= body.Length || body[cursor] != '"')
|
|
{
|
|
index = cursor;
|
|
continue;
|
|
}
|
|
|
|
var close = body.IndexOf('"', cursor + 1);
|
|
if (close < 0)
|
|
{
|
|
repaired.Append(body, cursor, body.Length - cursor);
|
|
break;
|
|
}
|
|
|
|
repaired.Append('"').Append(LoopbackProbeAddress).Append('"');
|
|
replaced++;
|
|
index = close + 1;
|
|
}
|
|
|
|
if (replaced == 0) return null;
|
|
|
|
note = replaced.ToString(CultureInfo.InvariantCulture) + " region probe address(es) pinned to "
|
|
+ LoopbackProbeAddress + " (the server's list points at retired hosts)";
|
|
return repaired.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The client's model for this route is a name -> row map
|
|
/// (Dictionary<string, ...>), so the flat member object the server answers
|
|
/// with makes its reader demand '{' where a string sits and throw
|
|
/// "expected:'{', actual:'\"none\"'" (a crash report per login). Wrapping the
|
|
/// flat object under the row key(s) the request named gives it the map it wants.
|
|
/// </summary>
|
|
private static string RepairUserSegment(string query, string body, ref string note)
|
|
{
|
|
var trimmed = body.TrimStart();
|
|
if (trimmed.Length == 0 || trimmed[0] != '{') return null;
|
|
|
|
var keys = new List<string>();
|
|
foreach (var value in QueryValues(query))
|
|
{
|
|
AddKey(keys, value);
|
|
AddKey(keys, "user_segment:" + value);
|
|
}
|
|
if (keys.Count == 0) AddKey(keys, "user_segment");
|
|
|
|
var repaired = new StringBuilder(body.Length * (keys.Count + 1) + 16);
|
|
repaired.Append('{');
|
|
for (var i = 0; i < keys.Count; i++)
|
|
{
|
|
if (i > 0) repaired.Append(',');
|
|
repaired.Append('"').Append(keys[i]).Append("\":").Append(body);
|
|
}
|
|
repaired.Append('}');
|
|
|
|
note = "userSegment answered as a name -> row map (" + string.Join(", ", keys) + ")";
|
|
return repaired.ToString();
|
|
}
|
|
|
|
private static void AddKey(List<string> keys, string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key)) return;
|
|
foreach (var existing in keys)
|
|
if (existing == key) return;
|
|
keys.Add(key);
|
|
}
|
|
|
|
internal static IEnumerable<string> QueryValues(string query)
|
|
{
|
|
if (string.IsNullOrEmpty(query)) yield break;
|
|
foreach (var pair in query.TrimStart('?').Split('&'))
|
|
{
|
|
if (pair.Length == 0) continue;
|
|
var separator = pair.IndexOf('=');
|
|
var value = separator < 0 ? pair : pair.Substring(separator + 1);
|
|
value = Uri.UnescapeDataString(value).Trim();
|
|
if (value.Length == 0) continue;
|
|
yield return value;
|
|
}
|
|
}
|
|
|
|
private static bool EndsWith(string value, string suffix) =>
|
|
value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
|
|
}
|