From d7dce8453da2e254e053687753723b9530fe9053 Mon Sep 17 00:00:00 2001 From: dostrikov Date: Sat, 8 Jun 2024 12:51:44 +0700 Subject: [PATCH] fix(repeater): msgpack protocol mapping (#171) closes #170 Co-authored-by: Artem Derevnjuk --- .../Bus/DefaultRepeaterBusFactory.cs | 3 +- src/SecTester.Repeater/Bus/IncomingRequest.cs | 81 ++++++++- .../Bus/OutgoingResponse.cs | 18 +- src/SecTester.Repeater/Bus/RepeaterError.cs | 3 +- src/SecTester.Repeater/Bus/RepeaterInfo.cs | 3 +- src/SecTester.Repeater/Bus/RepeaterVersion.cs | 3 +- .../Bus/SocketIoRepeaterBus.cs | 3 +- .../DefaultMessagePackSerializerOptions.cs | 17 ++ .../Internal/HttpMethods.cs | 30 ++++ .../MessagePackHttpHeadersFormatter.cs | 155 ++++++++++++++++++ .../MessagePackHttpMethodFormatter.cs | 49 ++++++ .../Internal/MessagePackNamingPolicy.cs | 8 + .../MessagePackSnakeCaseNamingPolicy.cs | 11 ++ .../MessagePackStringEnumMemberFormatter.cs | 53 ++++++ .../SecTester.Repeater.csproj | 4 +- src/SecTester.Repeater/packages.lock.json | 8 +- src/SecTester.Runner/packages.lock.json | 18 +- .../Bus/IncomingRequestTests.cs | 117 +++++++++++++ .../Bus/SocketIoRepeaterBusTests.cs | 5 +- .../MessagePackHttpHeadersFormatterTests.cs | 63 +++++++ .../MessagePackHttpMethodFormatterTests.cs | 62 +++++++ ...ssagePackStringEnumMemberFormatterTests.cs | 75 +++++++++ .../packages.lock.json | 12 +- .../SecTester.Runner.Tests/packages.lock.json | 24 +-- 24 files changed, 773 insertions(+), 52 deletions(-) create mode 100644 src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs create mode 100644 src/SecTester.Repeater/Internal/HttpMethods.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs create mode 100644 src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs create mode 100644 test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs create mode 100644 test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs create mode 100644 test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs diff --git a/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs b/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs index 45f6a56..db3ae74 100644 --- a/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs +++ b/src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs @@ -5,6 +5,7 @@ using SecTester.Core.Utils; using SocketIO.Serializer.MessagePack; using SocketIOClient; +using SocketIOClient.Transport; namespace SecTester.Repeater.Bus; @@ -37,7 +38,7 @@ public IRepeaterBus Create(string repeaterId) ReconnectionAttempts = options.ReconnectionAttempts, ReconnectionDelayMax = options.ReconnectionDelayMax, ConnectionTimeout = options.ConnectionTimeout, - AutoUpgrade = false, + Transport = TransportProtocol.WebSocket, Auth = new { token = _config.Credentials.Token, domain = repeaterId } }) { diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index da65e3b..ff859fd 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -1,19 +1,82 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using MessagePack; -using SecTester.Core.Bus; +using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] -public record IncomingRequest(Uri Url) : Event, IRequest +[MessagePackObject] +public record IncomingRequest(Uri Url) : IRequest { - public string? Body { get; set; } - public HttpMethod Method { get; set; } = HttpMethod.Get; - public Protocol Protocol { get; set; } = Protocol.Http; - public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); - public IEnumerable>> Headers { get; set; } = - new List>>(); + private const string UrlKey = "url"; + private const string MethodKey = "method"; + private const string HeadersKey = "headers"; + private const string BodyKey = "body"; + private const string ProtocolKey = "protocol"; + + [Key(ProtocolKey)] public Protocol Protocol { get; set; } = Protocol.Http; + + [Key(HeadersKey)] public IEnumerable>> Headers { get; set; } = new Dictionary>(); + + [Key(BodyKey)] public string? Body { get; set; } + + [Key(MethodKey)] public HttpMethod Method { get; set; } = HttpMethod.Get; + + [Key(UrlKey)] public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); + + public static IncomingRequest FromDictionary(Dictionary dictionary) + { + var protocol = GetProtocolFromDictionary(dictionary); + var headers = GetHeadersFromDictionary(dictionary); + var body = GetBodyFromDictionary(dictionary); + var method = GetMethodFromDictionary(dictionary); + var url = GetUrlFromDictionary(dictionary); + + return new IncomingRequest(url!) + { + Protocol = protocol, + Headers = headers, + Body = body, + Method = method + }; + } + + private static Protocol GetProtocolFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(ProtocolKey, out var protocolObj) && protocolObj is string protocolStr + ? (Protocol)Enum.Parse(typeof(Protocol), protocolStr, true) + : Protocol.Http; + + private static IEnumerable>> GetHeadersFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(HeadersKey, out var headersObj) && headersObj is Dictionary headersDict + ? ConvertToHeaders(headersDict) + : new Dictionary>(); + + private static string? GetBodyFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(BodyKey, out var bodyObj) ? bodyObj?.ToString() : null; + + private static HttpMethod GetMethodFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(MethodKey, out var methodObj) && methodObj is string methodStr + ? HttpMethods.Items.TryGetValue(methodStr, out var m) && m is not null + ? m + : HttpMethod.Get + : HttpMethod.Get; + + private static Uri? GetUrlFromDictionary(Dictionary dictionary) => + dictionary.TryGetValue(UrlKey, out var urlObj) && urlObj is string urlStr + ? new Uri(urlStr) + : null; + + private static IEnumerable>> ConvertToHeaders(Dictionary headers) => + headers.ToDictionary( + kvp => kvp.Key.ToString()!, + kvp => kvp.Value switch + { + IEnumerable list => list.Select(v => v.ToString()!), + string str => new[] { str }, + _ => Enumerable.Empty() + } + ); } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index 6a6dd3e..70a8658 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -4,14 +4,24 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public record OutgoingResponse : IResponse { + [Key("protocol")] + public Protocol Protocol { get; set; } = Protocol.Http; + + [Key("statusCode")] public int? StatusCode { get; set; } + + [Key("body")] public string? Body { get; set; } + + [Key("message")] public string? Message { get; set; } + + [Key("errorCode")] public string? ErrorCode { get; set; } - public Protocol Protocol { get; set; } = Protocol.Http; - public IEnumerable>> Headers { get; set; } = - new List>>(); + + [Key("headers")] + public IEnumerable>> Headers { get; set; } = new Dictionary>(); } diff --git a/src/SecTester.Repeater/Bus/RepeaterError.cs b/src/SecTester.Repeater/Bus/RepeaterError.cs index eabc9e6..ea76ab2 100644 --- a/src/SecTester.Repeater/Bus/RepeaterError.cs +++ b/src/SecTester.Repeater/Bus/RepeaterError.cs @@ -2,8 +2,9 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterError { + [Key("message")] public string Message { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/RepeaterInfo.cs b/src/SecTester.Repeater/Bus/RepeaterInfo.cs index b76257b..4671e90 100644 --- a/src/SecTester.Repeater/Bus/RepeaterInfo.cs +++ b/src/SecTester.Repeater/Bus/RepeaterInfo.cs @@ -2,8 +2,9 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterInfo { + [Key("repeaterId")] public string RepeaterId { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/RepeaterVersion.cs b/src/SecTester.Repeater/Bus/RepeaterVersion.cs index 915aa21..f3555a4 100644 --- a/src/SecTester.Repeater/Bus/RepeaterVersion.cs +++ b/src/SecTester.Repeater/Bus/RepeaterVersion.cs @@ -2,8 +2,9 @@ namespace SecTester.Repeater.Bus; -[MessagePackObject(true)] +[MessagePackObject] public sealed record RepeaterVersion { + [Key("version")] public string Version { get; set; } = null!; } diff --git a/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs b/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs index fa3fd44..db5e7fd 100644 --- a/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs +++ b/src/SecTester.Repeater/Bus/SocketIoRepeaterBus.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Timers; @@ -64,7 +65,7 @@ private void DelegateEvents() } var ct = new CancellationTokenSource(_options.AckTimeout); - var request = response.GetValue(); + var request = IncomingRequest.FromDictionary(response.GetValue>()); var result = await RequestReceived.Invoke(request).ConfigureAwait(false); await response.CallbackAsync(ct.Token, result).ConfigureAwait(false); }); diff --git a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs new file mode 100644 index 0000000..707b476 --- /dev/null +++ b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs @@ -0,0 +1,17 @@ +using MessagePack; +using MessagePack.Resolvers; + +namespace SecTester.Repeater.Internal; + +internal static class DefaultMessagePackSerializerOptions +{ + internal static readonly MessagePackSerializerOptions Instance = new( + CompositeResolver.Create( + CompositeResolver.Create( + new MessagePackHttpHeadersFormatter(), + new MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy.SnakeCase), + new MessagePackHttpMethodFormatter()), + StandardResolver.Instance + ) + ); +} diff --git a/src/SecTester.Repeater/Internal/HttpMethods.cs b/src/SecTester.Repeater/Internal/HttpMethods.cs new file mode 100644 index 0000000..1900399 --- /dev/null +++ b/src/SecTester.Repeater/Internal/HttpMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; + +namespace SecTester.Repeater.Internal; + +public class HttpMethods +{ + public static IDictionary Items { get; } = typeof(HttpMethod) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) + .Select(x => x.GetValue(null)) + .Cast() + .Concat(new List + { + new("PATCH"), + new("COPY"), + new("LINK"), + new("UNLINK"), + new("PURGE"), + new("LOCK"), + new("UNLOCK"), + new("PROPFIND"), + new("VIEW") + }) + .Distinct() + .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs new file mode 100644 index 0000000..cbd6db9 --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackHttpHeadersFormatter.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.Linq; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +// Headers formatter is to be supporting javascript `undefined` which is treated as null (0xC0) +// https://www.npmjs.com/package/@msgpack/msgpack#messagepack-mapping-table +// https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format + +internal class MessagePackHttpHeadersFormatter : IMessagePackFormatter< + IEnumerable>>? +> +{ + public void Serialize(ref MessagePackWriter writer, IEnumerable>>? value, + MessagePackSerializerOptions options) + { + if (value == null) + { + writer.WriteNil(); + } + else + { + var count = value.Count(); + + writer.WriteMapHeader(count); + + Serialize(ref writer, value); + } + } + + private static void Serialize(ref MessagePackWriter writer, IEnumerable>> value) + { + foreach (var item in value) + { + writer.Write(item.Key); + + Serialize(ref writer, item); + } + } + + private static void Serialize(ref MessagePackWriter writer, KeyValuePair> item) + { + var headersCount = item.Value.Count(); + + if (headersCount == 1) + { + writer.Write(item.Value.First()); + } + else + { + writer.WriteArrayHeader(headersCount); + + foreach (var subItem in item.Value) + { + writer.Write(subItem); + } + } + } + + public IEnumerable>>? Deserialize(ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + if (reader.NextMessagePackType == MessagePackType.Nil) + { + reader.ReadNil(); + return null; + } + + if (reader.NextMessagePackType != MessagePackType.Map) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be a map or null"); + } + + var length = reader.ReadMapHeader(); + + options.Security.DepthStep(ref reader); + + try + { + return DeserializeMap(ref reader, length, options); + } + finally + { + reader.Depth--; + } + } + + private static IEnumerable>> DeserializeMap(ref MessagePackReader reader, int length, + MessagePackSerializerOptions options) + { + var result = new List>>(length); + + for (var i = 0 ; i < length ; i++) + { + var key = DeserializeString(ref reader); + + result.Add(new KeyValuePair>( + key, + DeserializeValue(ref reader, options) + )); + } + + return result; + } + + private static IEnumerable DeserializeArray(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) + { + var result = new List(length); + + options.Security.DepthStep(ref reader); + + try + { + for (var i = 0 ; i < length ; i++) + { + result.Add(DeserializeString(ref reader)); + } + } + finally + { + reader.Depth--; + } + + return result; + } + + private static IEnumerable DeserializeValue(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + switch (reader.NextMessagePackType) + { + case MessagePackType.Nil: + reader.ReadNil(); + return new List(); + case MessagePackType.String: + return new List { DeserializeString(ref reader) }; + case MessagePackType.Array: + return DeserializeArray(ref reader, reader.ReadArrayHeader(), options); + default: + throw new MessagePackSerializationException( + $"Unrecognized code: 0x{reader.NextCode:X2} but expected to be either a string or an array."); + } + } + + private static string DeserializeString(ref MessagePackReader reader) + { + if (reader.NextMessagePackType != MessagePackType.String) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be a string."); + } + + return reader.ReadString() ?? string.Empty; + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs new file mode 100644 index 0000000..0fc6569 --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackHttpMethodFormatter : IMessagePackFormatter +{ + public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) + { + if (null == value) + { + writer.WriteNil(); + } + else + { + writer.Write(value.Method); + } + } + + public HttpMethod? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.NextMessagePackType == MessagePackType.Nil) + { + reader.ReadNil(); + return null; + } + + if (reader.NextMessagePackType != MessagePackType.String) + { + throw new MessagePackSerializationException($"Unrecognized code: 0x{reader.NextCode:X2} but expected to be either a string or null."); + } + + return Deserialize(ref reader); + } + + private static HttpMethod? Deserialize(ref MessagePackReader reader) + { + var token = reader.ReadString(); + + if (token is null || !HttpMethods.Items.TryGetValue(token, out var method)) + { + throw new MessagePackSerializationException( + $"Unexpected value {token} when parsing the {nameof(HttpMethod)}."); + } + + return method; + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs b/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs new file mode 100644 index 0000000..f7bdfa8 --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackNamingPolicy.cs @@ -0,0 +1,8 @@ +namespace SecTester.Repeater.Internal; + +internal abstract class MessagePackNamingPolicy +{ + public static MessagePackNamingPolicy SnakeCase { get; } = new MessagePackSnakeCaseNamingPolicy(); + + public abstract string ConvertName(string name); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs b/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs new file mode 100644 index 0000000..1effd1b --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackSnakeCaseNamingPolicy.cs @@ -0,0 +1,11 @@ +using SecTester.Core.Utils; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackSnakeCaseNamingPolicy : MessagePackNamingPolicy +{ + public override string ConvertName(string name) + { + return name.ToSnakeCase(); + } +} diff --git a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs new file mode 100644 index 0000000..064e40c --- /dev/null +++ b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Internal; + +internal class MessagePackStringEnumMemberFormatter : IMessagePackFormatter + where T : Enum +{ + private static readonly Dictionary EnumToString = typeof(T) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (T)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.Value, x => x.StringValue); + + private readonly Dictionary _casedStringToEnum; + private readonly Dictionary _casedEnumToString; + + public MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy namingPolicy) + { + this._casedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); + this._casedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); + } + + public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) + { + if (!_casedEnumToString.TryGetValue(value, out var stringValue)) + { + throw new MessagePackSerializationException($"No string representation found for {value}"); + } + + writer.Write(stringValue); + } + + public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + var stringValue = reader.ReadString(); + + if (null == stringValue || !_casedStringToEnum.TryGetValue(stringValue, out var enumValue)) + { + throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); + } + + return enumValue; + } +} diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 15eb7f1..1bbf68f 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -12,12 +12,12 @@ - + - + diff --git a/src/SecTester.Repeater/packages.lock.json b/src/SecTester.Repeater/packages.lock.json index f4301ee..149aa87 100644 --- a/src/SecTester.Repeater/packages.lock.json +++ b/src/SecTester.Repeater/packages.lock.json @@ -23,9 +23,9 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Direct", - "requested": "[3.1.1, )", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "requested": "[3.1.2, )", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -415,7 +415,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } diff --git a/src/SecTester.Runner/packages.lock.json b/src/SecTester.Runner/packages.lock.json index 0213610..ae4d9cd 100644 --- a/src/SecTester.Runner/packages.lock.json +++ b/src/SecTester.Runner/packages.lock.json @@ -231,8 +231,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -412,7 +412,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -429,9 +429,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } @@ -440,7 +440,7 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Scan": "[0.41.3, )" } }, "sectester.scan": { @@ -448,8 +448,8 @@ "dependencies": { "Macross.Json.Extensions": "[3.0.0, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", "System.Linq.Async": "[6.0.1, )", "System.Text.Json": "[6.0.0, )" } diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs new file mode 100644 index 0000000..672bcc3 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -0,0 +1,117 @@ +using MessagePack; +using SecTester.Repeater.Internal; +using SocketIO.Core; +using SocketIO.Serializer.MessagePack; + +namespace SecTester.Repeater.Tests.Bus; + +public class IncomingRequestTests +{ + private static readonly MessagePackSerializerOptions Options = DefaultMessagePackSerializerOptions.Instance; + + private static readonly IEnumerable ValidFixtures = new[] + { + new IncomingRequest(new Uri("http://foo.bar/1")) + { + Protocol = Protocol.Http, + Method = new HttpMethod("PROPFIND"), + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + }, + Body = "{\"foo\":\"bar\"}" + }, + new IncomingRequest(new Uri("http://foo.bar/2")) + { + Protocol = Protocol.Http, + Headers = new List>>() + } + }; + + public static IEnumerable CreateInstanceFixtures => ValidFixtures + .Select((x) => new object?[] + { + x + }); + + [Theory] + [MemberData(nameof(CreateInstanceFixtures))] + public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest input) + { + // arrange + var serializer = new SocketIOMessagePackSerializer(Options); + + var serialized = serializer.Serialize(EngineIO.V4, "request", 1, "/some", new object[] { input }).First(); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(serialized.Binary, Options); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(input); + } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsInvalid() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http:\",\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(Options); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var act = () => IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + act.Should().Throw(); + } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldAssignDefaultPropertyValues() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(Options); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1"))); + } + + [Fact] + public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() + { + // arrange + var packJson = + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + + var serializer = new SocketIOMessagePackSerializer(Options); + + var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); + + var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); + + // act + var result = IncomingRequest.FromDictionary(deserializedDictionary); + + // assert + result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")) { Protocol = Protocol.Http }); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs b/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs index 1dc7206..17351e8 100644 --- a/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/SocketIoRepeaterBusTests.cs @@ -36,7 +36,10 @@ public async Task RequestReceived_ExecutesHandler() StatusCode = 204 }; _connection.Connect().Returns(Task.CompletedTask); - _socketIoMessage.GetValue().Returns(new IncomingRequest(Url)); + _socketIoMessage.GetValue>().Returns(new Dictionary() + { + {"url", Url.ToString()} + }); _connection.On("request", Arg.Invoke(_socketIoMessage)); _sut.RequestReceived += _ => Task.FromResult(result); diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs new file mode 100644 index 0000000..68d29cf --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs @@ -0,0 +1,63 @@ +using MessagePack; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public sealed class MessagePackHttpHeadersFormatterTests +{ + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create(new MessagePackHttpHeadersFormatter()), + BuiltinResolver.Instance + ) + ); + + public static readonly IEnumerable Fixtures = new List() + { + new object?[] + { + null + }, + new object[] + { + Enumerable.Empty>>() + }, + new object?[] + { + new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } + } + }; + + public static readonly IEnumerable WrongValueFixtures = new List() + { + new object[] + { + "{\"headers\":5}" + }, + new object[] { "{\"headers\":[]}" }, + new object[] { "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}" }, + new object[] { "{\"headers\":{\"content-type\":1}}" }, + new object[] { "{\"headers\":{\"content-type\":[null]}}" }, + new object[] { "{\"headers\":{\"content-type\":[1]}}" } + }; + + [Theory] + [MemberData(nameof(Fixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( + IEnumerable>>? input) + { + // arrange + var serialized = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize>>>(serialized, Options); + + // assert + result.Should().BeEquivalentTo(input); + } +} diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs new file mode 100644 index 0000000..93a1420 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpMethodFormatterTests.cs @@ -0,0 +1,62 @@ +using MessagePack; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public sealed class MessagePackHttpMethodFormatterTests +{ + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create(new MessagePackHttpMethodFormatter()), + BuiltinResolver.Instance + ) + ); + + public static readonly IEnumerable Fixture = new List + { + new object[] { "DELETE", HttpMethod.Delete }, + new object[] { "GET", HttpMethod.Get }, + new object[] { "HEAD", HttpMethod.Head }, + new object[] { "OPTIONS", HttpMethod.Options }, + new object[] { "PATCH", HttpMethod.Patch }, + new object[] { "POST", HttpMethod.Post }, + new object[] { "PUT", HttpMethod.Put }, + new object[] { "TRACE", HttpMethod.Trace }, + new object[] { "COPY", new HttpMethod("COPY") }, + new object[] { "LINK", new HttpMethod("LINK") }, + new object[] { "UNLINK", new HttpMethod("UNLINK") }, + new object[] { "PURGE", new HttpMethod("PURGE") }, + new object[] { "LOCK", new HttpMethod("LOCK") }, + new object[] { "UNLOCK", new HttpMethod("UNLOCK") }, + new object[] { "PROPFIND", new HttpMethod("PROPFIND") }, + new object[] { "VIEW", new HttpMethod("VIEW") } + }; + + [Theory] + [MemberData(nameof(Fixture))] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializeHttpMethods( + string input, HttpMethod expected) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void HttpMethodMessagePackFormatter_Deserialize_ShouldHandleNull() + { + // arrange + var binary = MessagePackSerializer.Serialize(null, Options); + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + // assert + result.Should().BeNull(); + } +} diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs new file mode 100644 index 0000000..b2bfabf --- /dev/null +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackStringEnumMemberFormatterTests.cs @@ -0,0 +1,75 @@ +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Resolvers; +using SecTester.Repeater.Internal; + +namespace SecTester.Repeater.Tests.Internal; + +public enum Foo +{ + Foo = 0, + BAR = 1, + [EnumMember(Value = "baz_cux")] + BazCux = 2 +} + +public sealed class MessagePackStringEnumMemberFormatterTests +{ + private static readonly MessagePackSerializerOptions Options = new( + CompositeResolver.Create( + CompositeResolver.Create( + new MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy.SnakeCase) + ), + BuiltinResolver.Instance + ) + ); + + public static IEnumerable + Fixtures => + new List + { + new object[] { "foo", Foo.Foo }, + new object[] { "bar", Foo.BAR }, + new object[] { "baz_cux", Foo.BazCux } + }; + + public static IEnumerable + WrongValueFixtures => + new List + { + new object[] { null }, + new object[] { "5" }, + new object[] { "BazCux" } + }; + + [Theory] + [MemberData(nameof(Fixtures))] + public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize( + string input, Foo expected) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + + // act + var result = MessagePackSerializer.Deserialize(binary, Options); + + // assert + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(WrongValueFixtures))] + public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input) + { + // arrange + var binary = MessagePackSerializer.Serialize(input, Options); + + // act + var act = () => MessagePackSerializer.Deserialize(binary, Options); + + // assert + act.Should().Throw().WithMessage( + "Failed to deserialize*"); + } +} diff --git a/test/SecTester.Repeater.Tests/packages.lock.json b/test/SecTester.Repeater.Tests/packages.lock.json index d7051cc..4765948 100644 --- a/test/SecTester.Repeater.Tests/packages.lock.json +++ b/test/SecTester.Repeater.Tests/packages.lock.json @@ -695,8 +695,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -1589,7 +1589,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -1606,9 +1606,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } diff --git a/test/SecTester.Runner.Tests/packages.lock.json b/test/SecTester.Runner.Tests/packages.lock.json index 5ac24be..50f3db7 100644 --- a/test/SecTester.Runner.Tests/packages.lock.json +++ b/test/SecTester.Runner.Tests/packages.lock.json @@ -508,8 +508,8 @@ }, "SocketIO.Serializer.MessagePack": { "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "lOAZs6AUCDhfMFa4Vu40LeeK/9fP+iMUerI3qzmToDaSa+A2/YQ7D0nvYa1atwTvzvhDZklBKNV5dIzQWWwPJg==", + "resolved": "3.1.2", + "contentHash": "eYSPq7aKP11crDqGA5XlcGtQBqbeydbzgKb6FgySAlUqgGWZ2foXuQLIiBbPJPuH6ygaqycvQimw+oz5328AeA==", "dependencies": { "MessagePack": "2.5.124", "SocketIO.Core": "3.1.1", @@ -1404,7 +1404,7 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Http": "[6.0.0, )", "RabbitMQ.Client": "[6.4.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Core": "[0.41.3, )", "System.Text.Json": "[6.0.0, )", "System.Threading.RateLimiting": "[7.0.0, )" } @@ -1421,9 +1421,9 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", - "SocketIO.Serializer.MessagePack": "[3.1.1, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", + "SocketIO.Serializer.MessagePack": "[3.1.2, )", "SocketIOClient": "[3.1.1, )", "System.Linq.Async": "[6.0.1, )" } @@ -1432,16 +1432,16 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Scan": "[0.41.3, )" } }, "sectester.runner": { "type": "Project", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Repeater": "[0.40.0, )", - "SecTester.Reporter": "[0.40.0, )", - "SecTester.Scan": "[0.40.0, )" + "SecTester.Repeater": "[0.41.3, )", + "SecTester.Reporter": "[0.41.3, )", + "SecTester.Scan": "[0.41.3, )" } }, "sectester.scan": { @@ -1449,8 +1449,8 @@ "dependencies": { "Macross.Json.Extensions": "[3.0.0, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )", - "SecTester.Bus": "[0.40.0, )", - "SecTester.Core": "[0.40.0, )", + "SecTester.Bus": "[0.41.3, )", + "SecTester.Core": "[0.41.3, )", "System.Linq.Async": "[6.0.1, )", "System.Text.Json": "[6.0.0, )" }