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();
Logger.Info("Received match data: " + bodyString, LogArea.Match);
Logger.Debug("Received match data: " + bodyString, LogArea.Match);
IMatchCommand? matchData;
try

View file

@ -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<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)
{
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<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)
{
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);)", "&amp;");
bodyString = CharacterEscapeRegex().Replace(bodyString, "&amp;");
XmlRootAttribute? root = null;
if (rootElements.Length > 0)

View file

@ -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<int, string?> UserLocations = 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);
[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);
}