using System; using System.Collections.Generic; using System.Globalization; using System.Text; /// /// 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. /// internal static class ResponseRepair { /// Loopback probe target: any transport fails (or answers) instantly, so a probe phase can never stall. 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\""; /// /// 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. /// internal static bool IsTruncatedChunkFailure(string message) { if (string.IsNullOrEmpty(message)) return false; return message.IndexOf("as a hex number", StringComparison.OrdinalIgnoreCase) >= 0; } /// /// Rewrites a response body when the client would otherwise mis-handle it. /// Returns null when the body is left alone; explains /// what changed when it did. /// 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; } /// /// 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. /// 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(); } /// /// 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. /// 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(); 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 keys, string key) { if (string.IsNullOrEmpty(key)) return; foreach (var existing in keys) if (existing == key) return; keys.Add(key); } internal static IEnumerable 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); }