Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(repeater): resolve bridges service communication #171

Merged
merged 13 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/SecTester.Repeater/Bus/DefaultRepeaterBusFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SecTester.Core.Utils;
using SocketIO.Serializer.MessagePack;
using SocketIOClient;
using SocketIOClient.Transport;

namespace SecTester.Repeater.Bus;

Expand Down Expand Up @@ -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 }
})
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using MessagePack;
using MessagePack.Formatters;

namespace SecTester.Repeater.Bus.Formatters;

// Headers formatter is to be support 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

public class MessagePackHttpHeadersFormatter : IMessagePackFormatter<
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<KeyValuePair<string, IEnumerable<string>>>?
>
{
public void Serialize(ref MessagePackWriter writer, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? value,
MessagePackSerializerOptions options)
{
if (value == null)
{
writer.WriteNil();
}
else
{
var count = value.Count();

writer.WriteMapHeader(count);

SerializeMap(ref writer, value);
}
}

public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? Deserialize(ref MessagePackReader reader,
MessagePackSerializerOptions options)
{
switch (reader.NextMessagePackType)
{
case MessagePackType.Nil:
return null;
ostridm marked this conversation as resolved.
Show resolved Hide resolved
case MessagePackType.Map:
break;
default:
throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture,
"Unrecognized code: 0x{0:X2} but expected to be a map or null", reader.NextCode));
}
ostridm marked this conversation as resolved.
Show resolved Hide resolved

var length = reader.ReadMapHeader();

options.Security.DepthStep(ref reader);

try
{
return DeserializeMap(ref reader, length, options);
}
finally
{
reader.Depth--;
}
}

private static void SerializeMap(ref MessagePackWriter writer, IEnumerable<KeyValuePair<string, IEnumerable<string>>> value)
ostridm marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var item in value)
{
writer.Write(item.Key);

SerializeValue(ref writer, item);
}
}

private static void SerializeValue(ref MessagePackWriter writer, KeyValuePair<string, IEnumerable<string>> 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);
}
}
}

private static List<KeyValuePair<string, IEnumerable<string>>> DeserializeMap(ref MessagePackReader reader, int length,
MessagePackSerializerOptions options)
{
var result = new List<KeyValuePair<string, IEnumerable<string>>>(length);

for (var i = 0 ; i < length ; i++)
{
var key = DeserializeString(ref reader);

switch (reader.NextMessagePackType)
{
case MessagePackType.String:
result.Add(new KeyValuePair<string, IEnumerable<string>>(key, new List<string> { DeserializeString(ref reader) }));
break;
case MessagePackType.Array:
result.Add(new KeyValuePair<string, IEnumerable<string>>(key, DeserializeArray(ref reader, reader.ReadArrayHeader(), options)));
break;
ostridm marked this conversation as resolved.
Show resolved Hide resolved
default:
throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture,
"Unrecognized code: 0x{0:X2} but expected to be either a string or an array.", reader.NextCode));
}
}


return result;
}

private static IEnumerable<string> DeserializeArray(ref MessagePackReader reader, int length, MessagePackSerializerOptions options)
{
var result = new List<string>(length);

if (length == 0)
{
return result;
}
ostridm marked this conversation as resolved.
Show resolved Hide resolved

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 string DeserializeString(ref MessagePackReader reader)
ostridm marked this conversation as resolved.
Show resolved Hide resolved
{
if (reader.NextMessagePackType != MessagePackType.String)
{
throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture,
"Unrecognized code: 0x{0:X2} but expected to be a string.", reader.NextCode));
}

var value = reader.ReadString();

if (null == value)
{
throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture, "Nulls are not allowed."));
}

return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Globalization;
using System.Net.Http;
using MessagePack;
using MessagePack.Formatters;

namespace SecTester.Repeater.Bus.Formatters;

public class MessagePackHttpMethodFormatter : IMessagePackFormatter<HttpMethod?>
{
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)
{
switch (reader.NextMessagePackType)
{
case MessagePackType.Nil:
return null;
case MessagePackType.String:
var method = reader.ReadString();
return string.IsNullOrWhiteSpace(method) ? null : new HttpMethod(method);
ostridm marked this conversation as resolved.
Show resolved Hide resolved
default:
throw new MessagePackSerializationException(string.Format(CultureInfo.InvariantCulture,
"Unrecognized code: 0x{0:X2} but expected to be either a string or null.", reader.NextCode));
}
}
}
ostridm marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using MessagePack;
using MessagePack.Formatters;

namespace SecTester.Repeater.Bus.Formatters;

// ADHOC: MessagePack-CSharp prohibits declaration of IMessagePackFormatter<T> requesting to use System.Enum instead, refer to formatter interface argument type check
// https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/db2320b3338735c9266110bbbfffe63f17dfdf46/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Resolvers/DynamicObjectResolver.cs#L623

public class MessagePackStringEnumFormatter<T> : IMessagePackFormatter<Enum>
ostridm marked this conversation as resolved.
Show resolved Hide resolved
where T : Enum
{
private static readonly Dictionary<T, string> EnumToString = typeof(T)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(field => new
{
Value = (T)field.GetValue(null),
StringValue = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? field.Name
})
.ToDictionary(x => x.Value, x => x.StringValue);

private static readonly Dictionary<string, T> StringToEnum = EnumToString.ToDictionary(x => x.Value, x => x.Key);

public void Serialize(ref MessagePackWriter writer, Enum value, MessagePackSerializerOptions options)
{
if (!EnumToString.TryGetValue((T)value, out var stringValue))
{
throw new MessagePackSerializationException($"No string representation found for {value}");
}

writer.Write(stringValue);
}

public Enum Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{

var stringValue = reader.ReadString();

if (!StringToEnum.TryGetValue(stringValue, out var enumValue))

Check warning on line 43 in src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs

View workflow job for this annotation

GitHub Actions / windows-2019

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, T>.TryGetValue(string key, out T value)'.
ostridm marked this conversation as resolved.
Show resolved Hide resolved
{
throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}.");
}

return enumValue;
}
}
23 changes: 23 additions & 0 deletions src/SecTester.Repeater/Bus/HttpMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using MessagePack;
using SecTester.Repeater.Bus.Formatters;

namespace SecTester.Repeater.Bus;

public abstract record HttpMessage
ostridm marked this conversation as resolved.
Show resolved Hide resolved
{
public const string HeadersKey = "headers";
public const string BodyKey = "body";
public const string ProtocolKey = "protocol";

[Key(ProtocolKey)]
public Protocol Protocol { get; set; } = Protocol.Http;

[Key(HeadersKey)]
[MessagePackFormatter(typeof(MessagePackHttpHeadersFormatter))]
ostridm marked this conversation as resolved.
Show resolved Hide resolved
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> Headers { get; set; } =
new List<KeyValuePair<string, IEnumerable<string>>>();

[Key(BodyKey)]
public string? Body { get; set; }
}
88 changes: 81 additions & 7 deletions src/SecTester.Repeater/Bus/IncomingRequest.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Serialization;
using MessagePack;
using SecTester.Core.Bus;
using SecTester.Repeater.Bus.Formatters;
using SecTester.Repeater.Runners;

namespace SecTester.Repeater.Bus;

[MessagePackObject(true)]
public record IncomingRequest(Uri Url) : Event, IRequest
[MessagePackObject]
public record IncomingRequest(Uri Url) : HttpMessage, IRequest
{
public string? Body { get; set; }
private const string UrlKey = "url";
private const string MethodKey = "method";

private static readonly Dictionary<string, Protocol> ProtocolEntries = typeof(Protocol)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(field => new
{
Value = (Protocol)field.GetValue(null),
StringValue = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? field.Name
})
.ToDictionary(x => x.StringValue, x => x.Value);

[Key(MethodKey)]
[MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))]
public HttpMethod Method { get; set; } = HttpMethod.Get;
public Protocol Protocol { get; set; } = Protocol.Http;

[Key(UrlKey)]
public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url));
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> Headers { get; set; } =
new List<KeyValuePair<string, IEnumerable<string>>>();

public static IncomingRequest FromDictionary(Dictionary<object, object> dictionary)
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
{
var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && ProtocolEntries.TryGetValue(p.ToString(), out var e)
? e
: Protocol.Http;

var uri = dictionary.TryGetValue(UrlKey, out var u) && u is string
? new Uri(u.ToString())
: throw new InvalidDataException(FormatPropertyError(UrlKey));

var method = dictionary.TryGetValue(MethodKey, out var m) && m is string
? new HttpMethod(m.ToString())
: HttpMethod.Get;

var body = dictionary.TryGetValue(BodyKey, out var b) && b is string ? b.ToString() : null;

var headers = dictionary.TryGetValue(HeadersKey, out var h) && h is Dictionary<object, object> value
? MapHeaders(value)
: new List<KeyValuePair<string, IEnumerable<string>>>();

return new IncomingRequest(uri)
{
Protocol = protocol,
Body = body,
Method = method,
Headers = headers
};
}

private static IEnumerable<KeyValuePair<string, IEnumerable<string>>> MapHeaders(Dictionary<object, object> headers)
{
var result = new List<KeyValuePair<string, IEnumerable<string>>>(headers?.Count ?? 0);

foreach (var kvp in headers)

Check warning on line 69 in src/SecTester.Repeater/Bus/IncomingRequest.cs

View workflow job for this annotation

GitHub Actions / windows-2019

Dereference of a possibly null reference.
{
var key = kvp.Key.ToString();

switch (kvp.Value)
{
case null:
result.Add(new KeyValuePair<string, IEnumerable<string>>(key, new List<string>()));
continue;
case string:
result.Add(new KeyValuePair<string, IEnumerable<string>>(key, new List<string>
{ kvp.Value.ToString() }));
continue;
case object[] objects:
result.Add(new KeyValuePair<string, IEnumerable<string>>(key,
objects.OfType<string>().Select(value => value.ToString()).ToList()));
continue;
}
}

return result;
}

private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type";
}
Loading
Loading