diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index e9d6960c71..c79660568c 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -161,9 +162,10 @@ namespace osu.Game.Online builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - // TODO: This should only be required to be `TypeNameHandling.Auto`. - // See usage in osu-server-spectator for further documentation as to why this is required. - options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All; + options.PayloadSerializerSettings.Converters = new List + { + new SignalRDerivedTypeWorkaroundJsonConverter(), + }; }); } diff --git a/osu.Game/Online/Multiplayer/MatchRoomState.cs b/osu.Game/Online/Multiplayer/MatchRoomState.cs index edd34fb5a3..30d948f878 100644 --- a/osu.Game/Online/Multiplayer/MatchRoomState.cs +++ b/osu.Game/Online/Multiplayer/MatchRoomState.cs @@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer [Serializable] [MessagePackObject] [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. - // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead). public abstract class MatchRoomState { } diff --git a/osu.Game/Online/Multiplayer/MatchUserState.cs b/osu.Game/Online/Multiplayer/MatchUserState.cs index 69245deba0..665b64a8b4 100644 --- a/osu.Game/Online/Multiplayer/MatchUserState.cs +++ b/osu.Game/Online/Multiplayer/MatchUserState.cs @@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer [Serializable] [MessagePackObject] [Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. - // TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead). public abstract class MatchUserState { } diff --git a/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs new file mode 100644 index 0000000000..55516d2223 --- /dev/null +++ b/osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using System; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Online +{ + /// + /// A type of that serializes a subset of types used in multiplayer/spectator communication that + /// derive from a known base type. This is a safe alternative to using or , + /// which are known to have security issues. + /// + public class SignalRDerivedTypeWorkaroundJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => + SignalRUnionWorkaroundResolver.BASE_TYPES.Contains(objectType) || + SignalRUnionWorkaroundResolver.DERIVED_TYPES.Contains(objectType); + + public override object? ReadJson(JsonReader reader, Type objectType, object? o, JsonSerializer jsonSerializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + JObject obj = JObject.Load(reader); + + string type = (string)obj[@"$dtype"]!; + + var resolvedType = SignalRUnionWorkaroundResolver.DERIVED_TYPES.Single(t => t.Name == type); + + object? instance = Activator.CreateInstance(resolvedType); + + jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance); + + return instance; + } + + public override void WriteJson(JsonWriter writer, object? o, JsonSerializer serializer) + { + if (o == null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartObject(); + + writer.WritePropertyName(@"$dtype"); + serializer.Serialize(writer, o.GetType().Name); + + writer.WritePropertyName(@"$value"); + writer.WriteRawValue(JsonConvert.SerializeObject(o)); + + writer.WriteEndObject(); + } + } +} diff --git a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs index e44da044cc..21413f8285 100644 --- a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs +++ b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs @@ -20,7 +20,22 @@ namespace osu.Game.Online public static readonly MessagePackSerializerOptions OPTIONS = MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver()); - private static readonly Dictionary formatter_map = new Dictionary + public static readonly IReadOnlyList BASE_TYPES = new[] + { + typeof(MatchServerEvent), + typeof(MatchUserRequest), + typeof(MatchRoomState), + typeof(MatchUserState), + }; + + public static readonly IReadOnlyList DERIVED_TYPES = new[] + { + typeof(ChangeTeamRequest), + typeof(TeamVersusRoomState), + typeof(TeamVersusUserState), + }; + + private static readonly IReadOnlyDictionary formatter_map = new Dictionary { { typeof(TeamVersusUserState), new TypeRedirectingFormatter() }, { typeof(TeamVersusRoomState), new TypeRedirectingFormatter() },