// 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); } } }