diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs index e3aba48d..2cb4d637 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs @@ -48,7 +48,7 @@ public class MatchController : ControllerBase if (bodyString.Length == 0 || bodyString[0] != '[') return this.BadRequest(); - Logger.Info("Received match data: " + bodyString, LogArea.Match); + Logger.Debug("Received match data: " + bodyString, LogArea.Match); IMatchCommand? matchData; try diff --git a/ProjectLighthouse/Extensions/ControllerExtensions.cs b/ProjectLighthouse/Extensions/ControllerExtensions.cs index 179587cd..b0db1c49 100644 --- a/ProjectLighthouse/Extensions/ControllerExtensions.cs +++ b/ProjectLighthouse/Extensions/ControllerExtensions.cs @@ -1,7 +1,10 @@ #nullable enable using System; +using System.Buffers; using System.IO; +using System.IO.Pipelines; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; @@ -12,7 +15,7 @@ using Microsoft.AspNetCore.Mvc; namespace LBPUnion.ProjectLighthouse.Extensions; -public static class ControllerExtensions +public static partial class ControllerExtensions { public static GameToken GetToken(this ControllerBase controller) @@ -23,25 +26,72 @@ public static class ControllerExtensions return token; } + private static void AddStringToBuilder(StringBuilder builder, in ReadOnlySequence readOnlySequence) + { + // Separate method because Span/ReadOnlySpan cannot be used in async methods + ReadOnlySpan span = readOnlySequence.IsSingleSegment + ? readOnlySequence.First.Span + : readOnlySequence.ToArray().AsSpan(); + builder.Append(Encoding.UTF8.GetString(span)); + } + public static async Task ReadBodyAsync(this ControllerBase controller) { - controller.Request.Body.Position = 0; + StringBuilder builder = new(); - using StreamReader bodyReader = new(controller.Request.Body); - return await bodyReader.ReadToEndAsync(); + while (true) + { + ReadResult readResult = await controller.Request.BodyReader.ReadAsync(); + ReadOnlySequence buffer = readResult.Buffer; + + SequencePosition? position; + + do + { + // Look for a EOL in the buffer + position = buffer.PositionOf((byte)'\n'); + if (position == null) continue; + + ReadOnlySequence readOnlySequence = buffer.Slice(0, position.Value); + AddStringToBuilder(builder, in readOnlySequence); + + // Skip the line + the \n character (basically position) + buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); + } + while (position != null); + + + if (readResult.IsCompleted && buffer.Length > 0) + { + AddStringToBuilder(builder, in buffer); + } + + controller.Request.BodyReader.AdvanceTo(buffer.Start, buffer.End); + + // At this point, buffer will be updated to point one byte after the last + // \n character. + if (readResult.IsCompleted) + { + break; + } + } + + return builder.ToString(); } + [GeneratedRegex("&(?!(amp|apos|quot|lt|gt);)")] + private static partial Regex CharacterEscapeRegex(); + public static async Task DeserializeBody(this ControllerBase controller, params string[] rootElements) { controller.Request.Body.Position = 0; - using StreamReader bodyReader = new(controller.Request.Body); - string bodyString = await bodyReader.ReadToEndAsync(); + string bodyString = await controller.ReadBodyAsync(); try { // Prevent unescaped ampersands from causing deserialization to fail - bodyString = Regex.Replace(bodyString, "&(?!(amp|apos|quot|lt|gt);)", "&"); + bodyString = CharacterEscapeRegex().Replace(bodyString, "&"); XmlRootAttribute? root = null; if (rootElements.Length > 0) diff --git a/ProjectLighthouse/Helpers/MatchHelper.cs b/ProjectLighthouse/Helpers/MatchHelper.cs index 45519705..348c35b2 100644 --- a/ProjectLighthouse/Helpers/MatchHelper.cs +++ b/ProjectLighthouse/Helpers/MatchHelper.cs @@ -2,14 +2,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using LBPUnion.ProjectLighthouse.Match.MatchCommands; namespace LBPUnion.ProjectLighthouse.Helpers; -public static class MatchHelper +public static partial class MatchHelper { public static readonly Dictionary UserLocations = new(); public static readonly Dictionary?> UserRecentlyDivedIn = new(); @@ -38,27 +37,23 @@ public static class MatchHelper public static bool ClearUserRecentDiveIns(int userId) => UserRecentlyDivedIn.Remove(userId); + [GeneratedRegex("^\\[([^,]*),\\[(.*)\\]\\]")] + private static partial Regex MatchJsonRegex(); + + [GeneratedRegex(@"0x[a-fA-F0-9]{7,8}")] + private static partial Regex LocationHexRegex(); + // This is the function used to show people how laughably awful LBP's protocol is. Beware. public static IMatchCommand? Deserialize(string data) { - string matchType = ""; + System.Text.RegularExpressions.Match match = MatchJsonRegex().Match(data); - int i = 1; - while (true) - { - if (data[i] == ',') break; - - matchType += data[i]; - i++; - } - - string matchData = $"{{{string.Concat(data.Skip(matchType.Length + 3).SkipLast(2))}}}"; // unfuck formatting so we can parse it as json + string matchType = match.Groups[1].Value; + // Wraps the actual match data in curly braces to parse it as a json object + string matchData = $"{{{match.Groups[2].Value}}}"; // JSON does not like the hex value that location comes in (0x7f000001) so, convert it to int - matchData = Regex.Replace(matchData, @"0x[a-fA-F0-9]{8}", m => Convert.ToInt32(m.Value, 16).ToString()); - // oh, but it gets better than that! LBP also likes to send hex values with an uneven amount of digits (0xa000064, 7 digits). in any case, we handle it here: - matchData = Regex.Replace(matchData, @"0x[a-fA-F0-9]{7}", m => Convert.ToInt32(m.Value, 16).ToString()); - // i'm actually crying about it. + matchData = LocationHexRegex().Replace(matchData, m => Convert.ToInt32(m.Value, 16).ToString()); return Deserialize(matchType, matchData); }