// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace EpicGames.AspNet
{
///
/// Global constants
///
public static partial class CustomMediaTypeNames
{
///
/// Media type for compact binary
///
public const string UnrealCompactBinary = "application/x-ue-cb";
///
/// Media type for compressed buffers
///
public const string UnrealCompressedBuffer = "application/x-ue-comp";
///
///
///
public const string JupiterInlinedPayload = "application/x-jupiter-inline";
///
/// Media type for compact binary packages
///
public const string UnrealCompactBinaryPackage = "application/x-ue-cbpkg";
}
///
/// Converter to allow reading compact binary objects as request bodies
///
public class CbInputFormatter : InputFormatter
{
///
/// Constructor
///
public CbInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(CustomMediaTypeNames.UnrealCompactBinary));
}
///
protected override bool CanReadType(Type type)
{
return true;
}
///
public override async Task ReadRequestBodyAsync(InputFormatterContext context)
{
// Buffer the data into an array
byte[] data;
try
{
using MemoryStream stream = new MemoryStream();
await context.HttpContext.Request.Body.CopyToAsync(stream);
data = stream.ToArray();
}
catch (BadHttpRequestException e)
{
ClientSendSlowExceptionUtil.MaybeThrowSlowSendException(e);
throw;
}
// Serialize the object
CbField field;
try
{
field = new CbField(data);
}
catch (Exception ex)
{
ILogger logger = context.HttpContext.RequestServices.GetRequiredService>();
logger.LogError(ex, "Unable to parse compact binary: {Dump}", FormatHexDump(data, 256));
foreach ((string name, StringValues values) in context.HttpContext.Request.Headers)
{
foreach (string? value in values)
{
logger.LogInformation("Header {Name}: {Value}", name, value);
}
}
throw new Exception($"Unable to parse compact binary request: {FormatHexDump(data, 256)}", ex);
}
return await InputFormatterResult.SuccessAsync(CbSerializer.Deserialize(new CbField(data), context.ModelType)!);
}
static string FormatHexDump(byte[] data, int maxLength)
{
ReadOnlySpan span = data.AsSpan(0, Math.Min(data.Length, maxLength));
string hexString = StringUtils.FormatHexString(span);
char[] hexDump = new char[span.Length * 3];
for (int idx = 0; idx < span.Length; idx++)
{
hexDump[(idx * 3) + 0] = ((idx & 15) == 0) ? '\n' : ' ';
hexDump[(idx * 3) + 1] = hexString[(idx * 2) + 0];
hexDump[(idx * 3) + 2] = hexString[(idx * 2) + 1];
}
return new string(hexDump);
}
}
///
/// Converter to allow writing compact binary objects as responses
///
public class CbOutputFormatter : OutputFormatter
{
///
/// Constructor
///
public CbOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(CustomMediaTypeNames.UnrealCompactBinary));
}
///
protected override bool CanWriteType(Type? type)
{
return true;
}
///
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
ReadOnlyMemory data;
if (context.Object is CbObject obj)
{
data = obj.GetView();
}
else
{
data = CbSerializer.Serialize(context.ObjectType!, context.Object!).GetView();
}
await context.HttpContext.Response.BodyWriter.WriteAsync(data);
}
}
///
/// Special version of which returns native CbObject encoded data. Can be
/// inserted at a high priority in the output formatter list to prevent transcoding to json.
///
public class CbPreferredOutputFormatter : CbOutputFormatter
{
///
protected override bool CanWriteType(Type? type)
{
return type == typeof(CbObject);
}
}
}