diff --git a/FxEvents/FiveMMsgPack/MsgPack.dll b/FxEvents/FiveMMsgPack/MsgPack.dll new file mode 100644 index 0000000..2cbc628 Binary files /dev/null and b/FxEvents/FiveMMsgPack/MsgPack.dll differ diff --git a/FxEvents/FxEvents.Client/EventSystem/ClientGateway.cs b/FxEvents/FxEvents.Client/EventSystem/ClientGateway.cs index 44f68b6..7854f7d 100644 --- a/FxEvents/FxEvents.Client/EventSystem/ClientGateway.cs +++ b/FxEvents/FxEvents.Client/EventSystem/ClientGateway.cs @@ -21,7 +21,7 @@ public class ClientGateway : BaseGateway public ClientGateway() { SnowflakeGenerator.Create((short)new Random().Next(1, 199)); - Serialization = new BinarySerialization(); + Serialization = new MsgPackSerialization(); DelayDelegate = async delay => await BaseScript.Delay(delay); PrepareDelegate = PrepareAsync; PushDelegate = Push; diff --git a/FxEvents/FxEvents.Client/FxEvents.Client.csproj b/FxEvents/FxEvents.Client/FxEvents.Client.csproj index 7db017a..0dcbb86 100644 --- a/FxEvents/FxEvents.Client/FxEvents.Client.csproj +++ b/FxEvents/FxEvents.Client/FxEvents.Client.csproj @@ -31,6 +31,9 @@ 4 + + ..\FiveMMsgPack\MsgPack.dll + @@ -40,16 +43,11 @@ $(PkgNewtonsoft_Json)\lib\portable-net40+sl5+win8+wp8+wpa81\Newtonsoft.Json.dll - - - - - - + diff --git a/FxEvents/FxEvents.Server/EventSystem/ServerGateway.cs b/FxEvents/FxEvents.Server/EventSystem/ServerGateway.cs index 9e3f561..ff8da68 100644 --- a/FxEvents/FxEvents.Server/EventSystem/ServerGateway.cs +++ b/FxEvents/FxEvents.Server/EventSystem/ServerGateway.cs @@ -23,7 +23,7 @@ public class ServerGateway : BaseGateway public ServerGateway() { SnowflakeGenerator.Create((short)new Random().Next(200, 399)); - Serialization = new BinarySerialization(); + Serialization = new MsgPackSerialization(); DelayDelegate = async delay => await BaseScript.Delay(delay); PushDelegate = Push; _signatures = new(); diff --git a/FxEvents/FxEvents.Server/FxEvents.Server.csproj b/FxEvents/FxEvents.Server/FxEvents.Server.csproj index 4881f7c..a47e4ee 100644 --- a/FxEvents/FxEvents.Server/FxEvents.Server.csproj +++ b/FxEvents/FxEvents.Server/FxEvents.Server.csproj @@ -9,12 +9,12 @@ x64 SERVER - embedded + portable SERVER - embedded + portable @@ -22,9 +22,12 @@ - + + + ..\FiveMMsgPack\MsgPack.dll + $(PkgNewtonsoft_Json)\lib\portable-net40+sl5+win8+wp8+wpa81\Newtonsoft.Json.dll diff --git a/FxEvents/Shared/BinaryHelper.cs b/FxEvents/Shared/BinaryHelper.cs index 085e8d8..1848af1 100644 --- a/FxEvents/Shared/BinaryHelper.cs +++ b/FxEvents/Shared/BinaryHelper.cs @@ -9,11 +9,12 @@ namespace FxEvents.Shared { public static class BinaryHelper { - private static BinarySerialization _serialization = new(); + private static BinarySerialization binarySerialization = new(); + private static MsgPackSerialization msgpackSerialization = new(); public static byte[] ToBytes(this T obj) { - using SerializationContext context = new("BinaryHelper", "ToBytes", _serialization); + using SerializationContext context = new("BinaryHelper", "ToBytes", msgpackSerialization); context.Serialize(typeof(T), obj); return context.GetData(); } @@ -39,7 +40,7 @@ public static string BytesToString(this byte[] ba, bool separator = false, bool public static T FromBytes(this byte[] data) { - using SerializationContext context = new(data.ToString(), "FromBytes", _serialization, data); + using SerializationContext context = new(data.ToString(), "FromBytes", msgpackSerialization, data); return context.Deserialize(); } } diff --git a/FxEvents/Shared/EventSubsystem/Message/EventParameter.cs b/FxEvents/Shared/EventSubsystem/Message/EventParameter.cs index 747a2bd..7582e7c 100644 --- a/FxEvents/Shared/EventSubsystem/Message/EventParameter.cs +++ b/FxEvents/Shared/EventSubsystem/Message/EventParameter.cs @@ -7,6 +7,7 @@ public class EventParameter { public byte[] Data { get; set; } + public EventParameter() { } public EventParameter(byte[] data) { Data = data; diff --git a/FxEvents/Shared/EventSubsystem/Message/EventResponseMessage.cs b/FxEvents/Shared/EventSubsystem/Message/EventResponseMessage.cs index 9709ab5..30c98f9 100644 --- a/FxEvents/Shared/EventSubsystem/Message/EventResponseMessage.cs +++ b/FxEvents/Shared/EventSubsystem/Message/EventResponseMessage.cs @@ -12,6 +12,7 @@ public class EventResponseMessage : IMessage public string? Signature { get; set; } public byte[]? Data { get; set; } + public EventResponseMessage() { } public EventResponseMessage(Snowflake id, string endpoint, string? signature, byte[]? data) { Id = id; diff --git a/FxEvents/Shared/EventSubsystem/Serialization/Implementations/BinarySerialization.cs b/FxEvents/Shared/EventSubsystem/Serialization/Implementations/BinarySerialization.cs index 1ee6bd0..3a329ee 100644 --- a/FxEvents/Shared/EventSubsystem/Serialization/Implementations/BinarySerialization.cs +++ b/FxEvents/Shared/EventSubsystem/Serialization/Implementations/BinarySerialization.cs @@ -15,6 +15,7 @@ namespace FxEvents.Shared.Serialization.Implementations public delegate T DeserializationObjectActivator(BinaryReader reader); + [Obsolete("This implementations is obsolete now that MsgPack is available")] public class BinarySerialization : ISerialization { public const string PackMethod = "PackSerializedBytes"; diff --git a/FxEvents/Shared/EventSubsystem/Serialization/Implementations/MsgPackSerialization.cs b/FxEvents/Shared/EventSubsystem/Serialization/Implementations/MsgPackSerialization.cs new file mode 100644 index 0000000..0f85a99 --- /dev/null +++ b/FxEvents/Shared/EventSubsystem/Serialization/Implementations/MsgPackSerialization.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using FxEvents.Shared.Diagnostics; +using FxEvents.Shared.Exceptions; +using FxEvents.Shared.TypeExtensions; +using Logger; +using MsgPack.Serialization; + +namespace FxEvents.Shared.Serialization.Implementations +{ + public class MsgPackSerialization : ISerialization + { + private delegate T ObjectActivator(); + private delegate void VoidMethod(); + private Log logger = new(); + private MsgPack.Serialization.SerializationContext _context = new(MsgPack.PackerCompatibilityOptions.None) { SerializationMethod = SerializationMethod.Map }; + public MsgPackSerialization() + { + } + + private bool CanCreateInstanceUsingDefaultConstructor(Type t) => t.IsValueType || !t.IsAbstract && t.GetConstructor(Type.EmptyTypes) != null; + public void Serialize(Type type, object value, SerializationContext context) + { + var typeIdentifier = GetTypeIdentifier(type); + if (typeIdentifier == "System.Collections.Generic.KeyValuePair`2") + { + var generics = type.GetGenericArguments(); + var method = GetType().GetMethod("Serialize", + new[] { typeof(Type), typeof(object), typeof(SerializationContext) }); + var instanceParam = Expression.Parameter(typeof(MsgPackSerialization), "instance"); + var typeParam = Expression.Parameter(typeof(Type), "type"); + var contextParam = Expression.Parameter(typeof(SerializationContext), "context"); + var pairParam = Expression.Parameter(type, "pair"); + var valueParam = Expression.Parameter(typeof(object), "value"); + var call = Expression.Call(instanceParam, method!, typeParam, valueParam, contextParam); + + void CallSerialization(Type genericType, string property) + { + var action = (Action)Expression.Lambda(typeof(Action), Expression.Block(new[] + { + instanceParam, + typeParam, + contextParam, + pairParam, + valueParam + }, + Expression.Assign(instanceParam, Expression.Constant(this, typeof(MsgPackSerialization))), + Expression.Assign(contextParam, Expression.Constant(context, typeof(SerializationContext))), + Expression.Assign(typeParam, Expression.Constant(genericType, typeof(Type))), + Expression.Assign(pairParam, Expression.Constant(value, type)), + Expression.Assign(valueParam, + Expression.Convert(Expression.Property(pairParam, property), typeof(object))), + call + )).Compile(); + + action.Invoke(); + } + + CallSerialization(generics[0], "Key"); + CallSerialization(generics[1], "Value"); + } + else if (typeIdentifier.StartsWith("System.Tuple`")) + { + var generics = type.GetGenericArguments(); + var method = GetType().GetMethod("Serialize", + new[] { typeof(Type), typeof(object), typeof(SerializationContext) }); + var instanceParam = Expression.Parameter(typeof(MsgPackSerialization), "instance"); + var typeParam = Expression.Parameter(typeof(Type), "type"); + var valueParam = Expression.Parameter(type, "value"); + var contextParam = Expression.Parameter(typeof(SerializationContext), "context"); + + for (var idx = 0; idx < generics.Length; idx++) + { + var generic = generics[idx]; + var call = Expression.Call(instanceParam, method!, typeParam, + Expression.Convert(Expression.Property(valueParam, $"Item{idx + 1}"), typeof(object)), + contextParam); + var action = (Action)Expression.Lambda(typeof(Action), Expression.Block(new[] + { + instanceParam, + typeParam, + contextParam, + valueParam + }, + Expression.Assign(instanceParam, Expression.Constant(this, typeof(MsgPackSerialization))), + Expression.Assign(contextParam, Expression.Constant(context, typeof(SerializationContext))), + Expression.Assign(typeParam, Expression.Constant(generic, typeof(Type))), + Expression.Assign(valueParam, Expression.Constant(value, type)), + call + )).Compile(); + + action.Invoke(); + } + } + else + { + var ser = MessagePackSerializer.Get(type, _context); + ser.Pack(context.Writer.BaseStream, value); + } + } + + public void Serialize(T value, SerializationContext context) + { + Serialize(typeof(T), value, context); + } + + public object Deserialize(Type type, SerializationContext context) + { + var ser = MessagePackSerializer.Get(type, _context); + return ser.Unpack(context.Reader.BaseStream); + } + + public T Deserialize(SerializationContext context) => Deserialize(typeof(T), context); + + public T Deserialize(Type type, SerializationContext context) + { + var canInstance = CanCreateInstanceUsingDefaultConstructor(type); + var typeIdentifier = GetTypeIdentifier(type); + + if (TypeCache.IsSimpleType) + { + var primitive = Deserialize(type, context); + if (primitive != null) return (T)primitive; + } + if (typeIdentifier == "System.Collections.Generic.KeyValuePair`2") + { + var generics = type.GetGenericArguments(); + var constructor = type.GetConstructor(generics) ?? + throw new SerializationException(context, type, + $"Could not find suitable constructor for type: {type.Name}"); + + var key = DeserializeAnonymously(generics[0], context); + var value = DeserializeAnonymously(generics[1], context); + var keyParam = Expression.Parameter(generics[0], "key"); + var valueParam = Expression.Parameter(generics[1], "value"); + var block = Expression.Block( + new[] { keyParam, valueParam }, + Expression.Assign(keyParam, Expression.Constant(key, generics[0])), + Expression.Assign(valueParam, Expression.Constant(value, generics[1])), + Expression.New(constructor, keyParam, valueParam) + ); + + if (typeof(T) == typeof(object)) + { + var generic = typeof(ObjectActivator<>).MakeGenericType(type); + var activator = Expression.Lambda(generic, block).Compile(); + + return (T)activator.DynamicInvoke(); + } + else + { + var activator = + (ObjectActivator)Expression.Lambda(typeof(ObjectActivator), block).Compile(); + + return activator.Invoke(); + } + } + else if (typeIdentifier.StartsWith("System.Tuple`")) + { + var generics = type.GetGenericArguments(); + var constructor = type.GetConstructor(generics) ?? + throw new SerializationException(context, type, + $"Could not find suitable constructor for type: {type.Name}"); + var parameters = new List(); + + foreach (var generic in generics) + { + var entry = Deserialize(generic, context); + + parameters.Add(Expression.Constant(entry, generic)); + } + + var expression = Expression.New(constructor, parameters); + + if (typeof(T) == typeof(object)) + { + var generic = typeof(ObjectActivator<>).MakeGenericType(type); + var activator = Expression.Lambda(generic, expression).Compile(); + + return (T)activator.DynamicInvoke(); + } + else + { + var activator = + (ObjectActivator)Expression.Lambda(typeof(ObjectActivator), expression).Compile(); + + return activator.Invoke(); + } + } + else + { + if (!canInstance) + { + throw new SerializationException(context, type, $"Type {type.Name} is missing its emtpy constructor"); + } + var ser = MessagePackSerializer.Get(_context); + return ser.Unpack(context.Reader.BaseStream); + } + } + + public object DeserializeAnonymously(Type type, SerializationContext context) => + Deserialize(type, context); + + private static string GetTypeIdentifier(Type type) + { + var builder = new StringBuilder(); + var declaring = type; + + builder.Append(type.Namespace); + builder.Append("."); + + var idx = builder.Length; + + while ((declaring = declaring.DeclaringType) != null) + { + builder.Insert(idx, declaring.Name + "."); + } + + builder.Append(type.Name); + + return builder.ToString(); + } + public object? DeserializePrimitive(Type type, SerializationContext context) + { + try + { + if (context.Reader == null) throw new Exception("SerializationContext.Reader is null."); + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + return context.Reader.ReadBoolean(); + case TypeCode.Byte: + return context.Reader.ReadByte(); + case TypeCode.Char: + return context.Reader.ReadChar(); + case TypeCode.Double: + return context.Reader.ReadDouble(); + case TypeCode.Int16: + return context.Reader.ReadInt16(); + case TypeCode.Int32: + return context.Reader.ReadInt32(); + case TypeCode.Int64: + return context.Reader.ReadInt64(); + case TypeCode.Single: + return context.Reader.ReadSingle(); + case TypeCode.String: + return context.Reader.ReadString(); + case TypeCode.SByte: + return context.Reader.ReadSByte(); + case TypeCode.UInt16: + return context.Reader.ReadUInt16(); + case TypeCode.UInt32: + return context.Reader.ReadUInt32(); + case TypeCode.UInt64: + return context.Reader.ReadUInt64(); + } + + return default; + } + catch (Exception ex) + { + throw new SerializationException(context, type, $"Could not deserialize primitive: {type}", ex); + } + } + } +} \ No newline at end of file diff --git a/FxEvents/Shared/Snowflake/Snowflake.cs b/FxEvents/Shared/Snowflake/Snowflake.cs index 0e7661f..878decf 100644 --- a/FxEvents/Shared/Snowflake/Snowflake.cs +++ b/FxEvents/Shared/Snowflake/Snowflake.cs @@ -8,8 +8,10 @@ public struct Snowflake : IEquatable { public static readonly Snowflake Empty = new Snowflake(0); - private readonly ulong _value; + private ulong _value; + public ulong Value { get => _value; set => _value = value; } + public Snowflake() { _value = 0; } public static Snowflake Next() { return SnowflakeGenerator.Instance.Next(); diff --git a/FxEvents/Shared/TypeExtensions/TypeCache.cs b/FxEvents/Shared/TypeExtensions/TypeCache.cs new file mode 100644 index 0000000..c8545dc --- /dev/null +++ b/FxEvents/Shared/TypeExtensions/TypeCache.cs @@ -0,0 +1,32 @@ +using System; +/// +/// A simple type cache to alleviate the reflection lookups. Checking for simple types is done once per type +/// encountered. Rather than every time. (Saves many CPU cycles. Reflection is slow) +/// +/// + +namespace FxEvents.Shared.TypeExtensions +{ + public static class TypeCache + { + static TypeCache() + { + Type = typeof(T); + IsSimpleType = true; + switch (Type.GetTypeCode(Type)) + { + case TypeCode.Object: + case TypeCode.DBNull: + case TypeCode.Empty: + case TypeCode.DateTime: + IsSimpleType = false; + break; + } + } + + // ReSharper disable StaticMemberInGenericType + public static bool IsSimpleType { get; } + public static Type Type { get; } + // ReSharper restore StaticMemberInGenericType + } +} \ No newline at end of file diff --git a/README.md b/README.md index e30b334..880f011 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # FxEvents an advanced event subsystem for FiveM With FxEvents you can send and get values between client and server using an advanced event handling process. -Signatures are encrypted per each client and using the proviced binary serialization you can hide contents from malicious clients! -To work you'll have to implement the Serialization Generator to be found in https://github.com/manups4e/fx-events/tree/main/src/generator into your project. +Signatures are encrypted per each client and using the proviced MsgPack binary serialization you can hide contents from malicious clients! +To work you only need to add `FXEvents.Client.dll` or `FXEvents.Server.dll` and `Newtonsoft.Json.dll` (In case of json serialization) to your resource. +No need of any external library for MsgPack, the event system uses the internal MsgPack dll provided with fivem itself!! Usage examples: @@ -53,13 +54,16 @@ Callbacks can be called serverside too because it might happen that the server n The library comes with some goodies to help with customization and debugging serialization printing. -## ToJson() +## ToJson() ![image](https://user-images.githubusercontent.com/4005518/188593550-48891947-fb41-4ec1-894c-b429ca890361.png) + +⚠️ You need Newtonsoft.Json to make this work!! ```c# string text = param.ToJson(); ``` ## FromJson() +⚠️ You need Newtonsoft.Json to make this work!! ```c# type value = jsonText.FromJson(); ```