Use pipelines to read request bodies instead of stream

This commit is contained in:
Slendy 2023-02-11 21:11:20 -06:00
parent 30cffef2f2
commit bff915e304
No known key found for this signature in database
GPG key ID: 7288D68361B91428
3 changed files with 70 additions and 25 deletions

View file

@ -48,7 +48,7 @@ public class MatchController : ControllerBase
if (bodyString.Length == 0 || bodyString[0] != '[') return this.BadRequest(); 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; IMatchCommand? matchData;
try try

View file

@ -1,7 +1,10 @@
#nullable enable #nullable enable
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
@ -12,7 +15,7 @@ using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Extensions; namespace LBPUnion.ProjectLighthouse.Extensions;
public static class ControllerExtensions public static partial class ControllerExtensions
{ {
public static GameToken GetToken(this ControllerBase controller) public static GameToken GetToken(this ControllerBase controller)
@ -23,25 +26,72 @@ public static class ControllerExtensions
return token; return token;
} }
private static void AddStringToBuilder(StringBuilder builder, in ReadOnlySequence<byte> readOnlySequence)
{
// Separate method because Span/ReadOnlySpan cannot be used in async methods
ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment
? readOnlySequence.First.Span
: readOnlySequence.ToArray().AsSpan();
builder.Append(Encoding.UTF8.GetString(span));
}
public static async Task<string> ReadBodyAsync(this ControllerBase controller) public static async Task<string> ReadBodyAsync(this ControllerBase controller)
{ {
controller.Request.Body.Position = 0; StringBuilder builder = new();
using StreamReader bodyReader = new(controller.Request.Body); while (true)
return await bodyReader.ReadToEndAsync(); {
ReadResult readResult = await controller.Request.BodyReader.ReadAsync();
ReadOnlySequence<byte> buffer = readResult.Buffer;
SequencePosition? position;
do
{
// Look for a EOL in the buffer
position = buffer.PositionOf((byte)'\n');
if (position == null) continue;
ReadOnlySequence<byte> 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<T?> DeserializeBody<T>(this ControllerBase controller, params string[] rootElements) public static async Task<T?> DeserializeBody<T>(this ControllerBase controller, params string[] rootElements)
{ {
controller.Request.Body.Position = 0; controller.Request.Body.Position = 0;
using StreamReader bodyReader = new(controller.Request.Body); string bodyString = await controller.ReadBodyAsync();
string bodyString = await bodyReader.ReadToEndAsync();
try try
{ {
// Prevent unescaped ampersands from causing deserialization to fail // Prevent unescaped ampersands from causing deserialization to fail
bodyString = Regex.Replace(bodyString, "&(?!(amp|apos|quot|lt|gt);)", "&amp;"); bodyString = CharacterEscapeRegex().Replace(bodyString, "&amp;");
XmlRootAttribute? root = null; XmlRootAttribute? root = null;
if (rootElements.Length > 0) if (rootElements.Length > 0)

View file

@ -2,14 +2,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using LBPUnion.ProjectLighthouse.Match.MatchCommands; using LBPUnion.ProjectLighthouse.Match.MatchCommands;
namespace LBPUnion.ProjectLighthouse.Helpers; namespace LBPUnion.ProjectLighthouse.Helpers;
public static class MatchHelper public static partial class MatchHelper
{ {
public static readonly Dictionary<int, string?> UserLocations = new(); public static readonly Dictionary<int, string?> UserLocations = new();
public static readonly Dictionary<int, List<int>?> UserRecentlyDivedIn = new(); public static readonly Dictionary<int, List<int>?> UserRecentlyDivedIn = new();
@ -38,27 +37,23 @@ public static class MatchHelper
public static bool ClearUserRecentDiveIns(int userId) => UserRecentlyDivedIn.Remove(userId); 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. // This is the function used to show people how laughably awful LBP's protocol is. Beware.
public static IMatchCommand? Deserialize(string data) public static IMatchCommand? Deserialize(string data)
{ {
string matchType = ""; System.Text.RegularExpressions.Match match = MatchJsonRegex().Match(data);
int i = 1; string matchType = match.Groups[1].Value;
while (true) // Wraps the actual match data in curly braces to parse it as a json object
{ string matchData = $"{{{match.Groups[2].Value}}}";
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
// JSON does not like the hex value that location comes in (0x7f000001) so, convert it to int // 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()); matchData = LocationHexRegex().Replace(matchData, 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.
return Deserialize(matchType, matchData); return Deserialize(matchType, matchData);
} }