mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
RateControl
This commit is contained in:
@@ -47,5 +47,9 @@ public enum ExceptionCode : ushort
|
|||||||
NotSupported,
|
NotSupported,
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
NotAllowed,
|
NotAllowed,
|
||||||
RateLimitExceeded
|
RateLimitExceeded,
|
||||||
|
ParserLimitExceeded,
|
||||||
|
AttachmentLimitExceeded,
|
||||||
|
AlreadyAttached,
|
||||||
|
ConnectionLimitExceeded
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ public class AutoList<T, ST> : IEnumerable<T>, ICollection, ICollection<T>
|
|||||||
/// <returns>Array</returns>
|
/// <returns>Array</returns>
|
||||||
public T[] ToArray()
|
public T[] ToArray()
|
||||||
{
|
{
|
||||||
// list.OrderBy()
|
lock (syncRoot)
|
||||||
return list.ToArray();
|
return list.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -242,7 +242,11 @@ public class AutoList<T, ST> : IEnumerable<T>, ICollection, ICollection<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Count
|
public int Count
|
||||||
{
|
{
|
||||||
get { return list.Count; }
|
get
|
||||||
|
{
|
||||||
|
lock (syncRoot)
|
||||||
|
return list.Count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSynchronized => (list as ICollection).IsSynchronized;
|
public bool IsSynchronized => (list as ICollection).IsSynchronized;
|
||||||
@@ -256,7 +260,8 @@ public class AutoList<T, ST> : IEnumerable<T>, ICollection, ICollection<T>
|
|||||||
/// <param name="value">Item to check if exists</param>
|
/// <param name="value">Item to check if exists</param>
|
||||||
public bool Contains(T value)
|
public bool Contains(T value)
|
||||||
{
|
{
|
||||||
return list.Contains(value);
|
lock (syncRoot)
|
||||||
|
return list.Contains(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ public static class DataDeserializer
|
|||||||
{
|
{
|
||||||
internal static readonly AsyncLocal<ulong[]> TypeDefRequestSequence = new();
|
internal static readonly AsyncLocal<ulong[]> TypeDefRequestSequence = new();
|
||||||
|
|
||||||
|
private static void EnsureTypedArrayBudget(ParsedTdu tdu, Warehouse warehouse, int elementSize)
|
||||||
|
=> ParserGuard.EnsureAllocation(
|
||||||
|
warehouse,
|
||||||
|
ParserGuard.MultiplySaturated(tdu.PayloadLength, (ulong)elementSize),
|
||||||
|
"typed array");
|
||||||
|
|
||||||
|
private static T[] GuardDecodedArray<T>(Warehouse warehouse, T[] values, int elementSize)
|
||||||
|
{
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, values.Length, elementSize);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
public static async AsyncReply<object> TypeDefInfoParserAsync(
|
public static async AsyncReply<object> TypeDefInfoParserAsync(
|
||||||
ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
@@ -324,6 +336,10 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
public static object ResourceLinkParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static object ResourceLinkParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(
|
||||||
|
connection?.ParsingWarehouse,
|
||||||
|
ParserGuard.MultiplySaturated(tdu.PayloadLength, 2),
|
||||||
|
"resource link");
|
||||||
var link = tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
var link = tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
if (connection == null)
|
if (connection == null)
|
||||||
{
|
{
|
||||||
@@ -337,6 +353,10 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
public static object ResourceLinkParser(ParsedTdu tdu, Warehouse warehouse)
|
public static object ResourceLinkParser(ParsedTdu tdu, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(
|
||||||
|
warehouse,
|
||||||
|
ParserGuard.MultiplySaturated(tdu.PayloadLength, 2),
|
||||||
|
"resource link");
|
||||||
var link = tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
var link = tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
return new ResourceLink(link);
|
return new ResourceLink(link);
|
||||||
}
|
}
|
||||||
@@ -432,27 +452,41 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
public static unsafe object RawDataParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static unsafe object RawDataParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(connection?.ParsingWarehouse, tdu.PayloadLength, "raw data");
|
||||||
return tdu.Data.Clip(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
return tdu.Data.Clip(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe object RawDataParser(ParsedTdu tdu, Warehouse warehouse)
|
public static unsafe object RawDataParser(ParsedTdu tdu, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(warehouse, tdu.PayloadLength, "raw data");
|
||||||
return tdu.Data.Clip(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
return tdu.Data.Clip(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static unsafe object StringParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static unsafe object StringParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(
|
||||||
|
connection?.ParsingWarehouse,
|
||||||
|
ParserGuard.MultiplySaturated(tdu.PayloadLength, 2),
|
||||||
|
"string");
|
||||||
return tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
return tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe object StringParser(ParsedTdu tdu, Warehouse warehouse)
|
public static unsafe object StringParser(ParsedTdu tdu, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureAllocation(
|
||||||
|
warehouse,
|
||||||
|
ParserGuard.MultiplySaturated(tdu.PayloadLength, 2),
|
||||||
|
"string");
|
||||||
return tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
return tdu.Data.GetString(tdu.PayloadOffset, (uint)tdu.PayloadLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async AsyncReply<IRecord> RecordParserAsync(ParsedTdu tdu, TypeDef recordTypeDef, EpConnection connection, uint[] requestSequence)
|
public static async AsyncReply<IRecord> RecordParserAsync(ParsedTdu tdu, TypeDef recordTypeDef, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
|
ParserGuard.EnsureCollectionCount(
|
||||||
|
connection?.ParsingWarehouse,
|
||||||
|
recordTypeDef.Properties.Length,
|
||||||
|
IntPtr.Size);
|
||||||
//if (tdu.Metadata.TypeDefId == null)
|
//if (tdu.Metadata.TypeDefId == null)
|
||||||
// throw new Exception("TypeDefId metadata is required for record parsing.");
|
// throw new Exception("TypeDefId metadata is required for record parsing.");
|
||||||
|
|
||||||
@@ -618,6 +652,8 @@ public static class DataDeserializer
|
|||||||
"TypeDef not found for record.");
|
"TypeDef not found for record.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, recordTypeDef.Properties.Length, IntPtr.Size);
|
||||||
|
|
||||||
var list = new List<object>();
|
var list = new List<object>();
|
||||||
|
|
||||||
ParsedTdu current;
|
ParsedTdu current;
|
||||||
@@ -789,6 +825,7 @@ public static class DataDeserializer
|
|||||||
public static async AsyncReply<object> RecordListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static async AsyncReply<object> RecordListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
var rt = new AsyncBag<IRecord>();
|
var rt = new AsyncBag<IRecord>();
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
var length = tdu.PayloadLength;
|
var length = tdu.PayloadLength;
|
||||||
var offset = tdu.PayloadOffset;
|
var offset = tdu.PayloadOffset;
|
||||||
@@ -798,6 +835,7 @@ public static class DataDeserializer
|
|||||||
//var (cs, reply)
|
//var (cs, reply)
|
||||||
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
|
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
|
||||||
rt.Add(pr.Value);
|
rt.Add(pr.Value);
|
||||||
|
|
||||||
if (pr.Size > 0)
|
if (pr.Size > 0)
|
||||||
@@ -827,6 +865,7 @@ public static class DataDeserializer
|
|||||||
{
|
{
|
||||||
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
|
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
|
||||||
rt.Add(reply as IRecord);
|
rt.Add(reply as IRecord);
|
||||||
|
|
||||||
if (cs > 0)
|
if (cs > 0)
|
||||||
@@ -845,6 +884,7 @@ public static class DataDeserializer
|
|||||||
public static async AsyncReply<object> ResourceListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static async AsyncReply<object> ResourceListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
var rt = new AsyncBag<IResource>();
|
var rt = new AsyncBag<IResource>();
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
var length = tdu.PayloadLength;
|
var length = tdu.PayloadLength;
|
||||||
var offset = tdu.PayloadOffset;
|
var offset = tdu.PayloadOffset;
|
||||||
@@ -854,6 +894,7 @@ public static class DataDeserializer
|
|||||||
//var (cs, reply)
|
//var (cs, reply)
|
||||||
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
|
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
|
||||||
rt.Add(pr.Value);// reply);
|
rt.Add(pr.Value);// reply);
|
||||||
|
|
||||||
if (pr.Size > 0)
|
if (pr.Size > 0)
|
||||||
@@ -884,6 +925,7 @@ public static class DataDeserializer
|
|||||||
{
|
{
|
||||||
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
|
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
|
||||||
rt.Add(reply as IResource);
|
rt.Add(reply as IResource);
|
||||||
|
|
||||||
if (cs > 0)
|
if (cs > 0)
|
||||||
@@ -931,6 +973,7 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
|
|
||||||
var rt = new AsyncBag<object>();
|
var rt = new AsyncBag<object>();
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
//var list = new List<object>();
|
//var list = new List<object>();
|
||||||
|
|
||||||
@@ -962,6 +1005,7 @@ public static class DataDeserializer
|
|||||||
//var (cs, reply)
|
//var (cs, reply)
|
||||||
var value = Codec.ParseAsync(current, connection, requestSequence);
|
var value = Codec.ParseAsync(current, connection, requestSequence);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
|
||||||
rt.Add(value);
|
rt.Add(value);
|
||||||
|
|
||||||
if (current.TotalLength > 0)
|
if (current.TotalLength > 0)
|
||||||
@@ -1012,6 +1056,7 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
var reply = Codec.ParseSync(current, warehouse);
|
var reply = Codec.ParseSync(current, warehouse);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, list.Count + 1, IntPtr.Size);
|
||||||
list.Add(reply);
|
list.Add(reply);
|
||||||
|
|
||||||
if (current.TotalLength > 0)
|
if (current.TotalLength > 0)
|
||||||
@@ -1040,6 +1085,7 @@ public static class DataDeserializer
|
|||||||
{
|
{
|
||||||
var (cs, reply) = Codec.ParseSync(data, offset, warehouse);
|
var (cs, reply) = Codec.ParseSync(data, offset, warehouse);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
|
||||||
rt.Add(reply);
|
rt.Add(reply);
|
||||||
|
|
||||||
if (cs > 0)
|
if (cs > 0)
|
||||||
@@ -1151,26 +1197,37 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
public static Array TypedArrayParser(ParsedTdu tdu, Tru elementTru, Warehouse warehouse)
|
public static Array TypedArrayParser(ParsedTdu tdu, Tru elementTru, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
|
var primitiveElementSize = elementTru.Identifier switch
|
||||||
|
{
|
||||||
|
TruIdentifier.Int16 or TruIdentifier.UInt16 => 2,
|
||||||
|
TruIdentifier.Int32 or TruIdentifier.UInt32 => 4,
|
||||||
|
TruIdentifier.Int64 or TruIdentifier.UInt64 => 8,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (primitiveElementSize > 0)
|
||||||
|
EnsureTypedArrayBudget(tdu, warehouse, primitiveElementSize);
|
||||||
|
|
||||||
switch (elementTru.Identifier)
|
switch (elementTru.Identifier)
|
||||||
{
|
{
|
||||||
case TruIdentifier.Int32:
|
case TruIdentifier.Int32:
|
||||||
return GroupInt32Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupInt32Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4);
|
||||||
case TruIdentifier.Int64:
|
case TruIdentifier.Int64:
|
||||||
return GroupInt64Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupInt64Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8);
|
||||||
case TruIdentifier.Int16:
|
case TruIdentifier.Int16:
|
||||||
return GroupInt16Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupInt16Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2);
|
||||||
case TruIdentifier.UInt32:
|
case TruIdentifier.UInt32:
|
||||||
return GroupUInt32Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupUInt32Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4);
|
||||||
case TruIdentifier.UInt64:
|
case TruIdentifier.UInt64:
|
||||||
return GroupUInt64Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupUInt64Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8);
|
||||||
case TruIdentifier.UInt16:
|
case TruIdentifier.UInt16:
|
||||||
return GroupUInt16Codec.Decode(tdu.Data.AsSpan(
|
return GuardDecodedArray(warehouse, GroupUInt16Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2);
|
||||||
//case TruIdentifier.Enum:
|
//case TruIdentifier.Enum:
|
||||||
|
|
||||||
// var enumType = tru.GetRuntimeType(warehouse);
|
// var enumType = tru.GetRuntimeType(warehouse);
|
||||||
@@ -1224,6 +1281,7 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
var value = Codec.ParseSync(current, warehouse);
|
var value = Codec.ParseSync(current, warehouse);
|
||||||
|
|
||||||
|
ParserGuard.EnsureCollectionCount(warehouse, list.Count + 1, IntPtr.Size);
|
||||||
list.Add(value);
|
list.Add(value);
|
||||||
|
|
||||||
if (current.TotalLength > 0)
|
if (current.TotalLength > 0)
|
||||||
@@ -1249,26 +1307,38 @@ public static class DataDeserializer
|
|||||||
// Non-async/await version of TypedArrayParserAsync using continuations
|
// Non-async/await version of TypedArrayParserAsync using continuations
|
||||||
public static AsyncReply TypedArrayParserAsync2(ParsedTdu tdu, Tru elementTru, EpConnection connection, uint[] requestSequence)
|
public static AsyncReply TypedArrayParserAsync2(ParsedTdu tdu, Tru elementTru, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
|
var warehouse = connection?.ParsingWarehouse;
|
||||||
|
var primitiveElementSize = elementTru.Identifier switch
|
||||||
|
{
|
||||||
|
TruIdentifier.Int16 or TruIdentifier.UInt16 => 2,
|
||||||
|
TruIdentifier.Int32 or TruIdentifier.UInt32 => 4,
|
||||||
|
TruIdentifier.Int64 or TruIdentifier.UInt64 => 8,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (primitiveElementSize > 0)
|
||||||
|
EnsureTypedArrayBudget(tdu, warehouse, primitiveElementSize);
|
||||||
|
|
||||||
switch (elementTru.Identifier)
|
switch (elementTru.Identifier)
|
||||||
{
|
{
|
||||||
case TruIdentifier.Int32:
|
case TruIdentifier.Int32:
|
||||||
return new AsyncReply(GroupInt32Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt32Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4));
|
||||||
case TruIdentifier.Int64:
|
case TruIdentifier.Int64:
|
||||||
return new AsyncReply(GroupInt64Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt64Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8));
|
||||||
case TruIdentifier.Int16:
|
case TruIdentifier.Int16:
|
||||||
return new AsyncReply(GroupInt16Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt16Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2));
|
||||||
case TruIdentifier.UInt32:
|
case TruIdentifier.UInt32:
|
||||||
return new AsyncReply(GroupUInt32Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt32Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4));
|
||||||
case TruIdentifier.UInt64:
|
case TruIdentifier.UInt64:
|
||||||
return new AsyncReply(GroupUInt64Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt64Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8));
|
||||||
case TruIdentifier.UInt16:
|
case TruIdentifier.UInt16:
|
||||||
return new AsyncReply(GroupUInt16Codec.Decode(tdu.Data.AsSpan(
|
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt16Codec.Decode(tdu.Data.AsSpan(
|
||||||
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
|
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
var rt = new AsyncReply();
|
var rt = new AsyncReply();
|
||||||
@@ -1285,6 +1355,7 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
// Recursive processor using continuations
|
// Recursive processor using continuations
|
||||||
Action<uint, uint, ParsedTdu?> processNext = null;
|
Action<uint, uint, ParsedTdu?> processNext = null;
|
||||||
|
var itemCount = 0;
|
||||||
|
|
||||||
processNext = (curOffset, curLength, previous) =>
|
processNext = (curOffset, curLength, previous) =>
|
||||||
{
|
{
|
||||||
@@ -1333,6 +1404,19 @@ public static class DataDeserializer
|
|||||||
|
|
||||||
var reply = Codec.ParseAsync(current, connection, requestSequence);
|
var reply = Codec.ParseAsync(current, connection, requestSequence);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ParserGuard.EnsureCollectionCount(
|
||||||
|
warehouse,
|
||||||
|
++itemCount,
|
||||||
|
IntPtr.Size);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
rt.TriggerError(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
list.Add(reply);
|
list.Add(reply);
|
||||||
|
|
||||||
if (current.TotalLength > 0)
|
if (current.TotalLength > 0)
|
||||||
|
|||||||
@@ -62,14 +62,16 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
|
|
||||||
public T Take(KT key)
|
public T Take(KT key)
|
||||||
{
|
{
|
||||||
if (dic.ContainsKey(key))
|
lock (syncRoot)
|
||||||
{
|
{
|
||||||
var v = dic[key];
|
if (dic.TryGetValue(key, out var value))
|
||||||
Remove(key);
|
{
|
||||||
return v;
|
Remove(key);
|
||||||
}
|
return value;
|
||||||
else
|
}
|
||||||
|
|
||||||
return default(T);
|
return default(T);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Sort(Func<KeyValuePair<KT, T>, object> keySelector)
|
public void Sort(Func<KeyValuePair<KT, T>, object> keySelector)
|
||||||
@@ -79,9 +81,12 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
|
|
||||||
public T[] ToArray()
|
public T[] ToArray()
|
||||||
{
|
{
|
||||||
var a = new T[Count];
|
lock (syncRoot)
|
||||||
dic.Values.CopyTo(a, 0);
|
{
|
||||||
return a;
|
var a = new T[dic.Count];
|
||||||
|
dic.Values.CopyTo(a, 0);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(KT key, T value)
|
public void Add(KT key, T value)
|
||||||
@@ -132,10 +137,8 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (dic.ContainsKey(key))
|
lock (syncRoot)
|
||||||
return dic[key];
|
return dic.TryGetValue(key, out var value) ? value : default(T);
|
||||||
else
|
|
||||||
return default(T);
|
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
@@ -157,13 +160,15 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
if (removableList)
|
|
||||||
foreach (IDestructible v in dic.Values)
|
|
||||||
if (v != null)
|
|
||||||
v.OnDestroy -= ItemDestroyed;
|
|
||||||
|
|
||||||
lock (syncRoot)
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
if (removableList)
|
||||||
|
foreach (IDestructible v in dic.Values)
|
||||||
|
if (v != null)
|
||||||
|
v.OnDestroy -= ItemDestroyed;
|
||||||
|
|
||||||
dic.Clear();
|
dic.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (OnCleared != null)
|
if (OnCleared != null)
|
||||||
OnCleared(this);
|
OnCleared(this);
|
||||||
@@ -184,17 +189,18 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
|
|
||||||
public void Remove(KT key)
|
public void Remove(KT key)
|
||||||
{
|
{
|
||||||
if (!dic.ContainsKey(key))
|
T value;
|
||||||
return;
|
|
||||||
|
|
||||||
var value = dic[key];
|
|
||||||
|
|
||||||
if (removableList)
|
|
||||||
if (value != null)
|
|
||||||
((IDestructible)value).OnDestroy -= ItemDestroyed;
|
|
||||||
|
|
||||||
lock (syncRoot)
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
if (!dic.TryGetValue(key, out value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (removableList)
|
||||||
|
if (value != null)
|
||||||
|
((IDestructible)value).OnDestroy -= ItemDestroyed;
|
||||||
|
|
||||||
dic.Remove(key);
|
dic.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
if (OnRemoved != null)
|
if (OnRemoved != null)
|
||||||
OnRemoved(key, value, this);
|
OnRemoved(key, value, this);
|
||||||
@@ -208,19 +214,26 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
|
|||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
{
|
{
|
||||||
get { return dic.Count; }
|
get
|
||||||
|
{
|
||||||
|
lock (syncRoot)
|
||||||
|
return dic.Count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public bool Contains(KT Key)
|
public bool Contains(KT Key)
|
||||||
{
|
{
|
||||||
return dic.ContainsKey(Key);
|
lock (syncRoot)
|
||||||
|
return dic.ContainsKey(Key);
|
||||||
}
|
}
|
||||||
public bool ContainsKey(KT Key)
|
public bool ContainsKey(KT Key)
|
||||||
{
|
{
|
||||||
return dic.ContainsKey(Key);
|
lock (syncRoot)
|
||||||
|
return dic.ContainsKey(Key);
|
||||||
}
|
}
|
||||||
public bool ContainsValue(T Value)
|
public bool ContainsValue(T Value)
|
||||||
{
|
{
|
||||||
return dic.ContainsValue(Value);
|
lock (syncRoot)
|
||||||
|
return dic.ContainsValue(Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ namespace Esiur.Data
|
|||||||
|
|
||||||
public static async AsyncReply<ParsedTdu> ParseAsync(byte[] data, uint offset, uint ends, EpConnection connection)
|
public static async AsyncReply<ParsedTdu> ParseAsync(byte[] data, uint offset, uint ends, EpConnection connection)
|
||||||
{
|
{
|
||||||
// @TODO: add protection against memory allocation attacks by checking the length of the data before parsing it.
|
|
||||||
|
|
||||||
var h = data[offset++];
|
var h = data[offset++];
|
||||||
|
|
||||||
var cls = (TduClass)(h >> 6);
|
var cls = (TduClass)(h >> 6);
|
||||||
@@ -91,6 +89,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
@@ -102,6 +102,10 @@ namespace Esiur.Data
|
|||||||
//offset += data[offset] + (uint)1;
|
//offset += data[offset] + (uint)1;
|
||||||
|
|
||||||
var metaDataTru = await Tru.ParseAsync(data, offset, connection, null);
|
var metaDataTru = await Tru.ParseAsync(data, offset, connection, null);
|
||||||
|
|
||||||
|
if (metaDataTru.Size > cl)
|
||||||
|
throw new ParserLimitException("Typed TDU metadata exceeds its declared payload length.");
|
||||||
|
|
||||||
offset += metaDataTru.Size;
|
offset += metaDataTru.Size;
|
||||||
|
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
@@ -133,6 +137,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
@@ -158,8 +164,6 @@ namespace Esiur.Data
|
|||||||
|
|
||||||
public static object Parse(byte[] data, uint offset, uint ends, EpConnection connection)
|
public static object Parse(byte[] data, uint offset, uint ends, EpConnection connection)
|
||||||
{
|
{
|
||||||
// @TODO: add protection against memory allocation attacks by checking the length of the data before parsing it.
|
|
||||||
|
|
||||||
var h = data[offset++];
|
var h = data[offset++];
|
||||||
|
|
||||||
var cls = (TduClass)(h >> 6);
|
var cls = (TduClass)(h >> 6);
|
||||||
@@ -222,6 +226,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
@@ -235,6 +241,13 @@ namespace Esiur.Data
|
|||||||
|
|
||||||
Tru.ParseAsync(data, offset, connection, null).Then(metaDataTru =>
|
Tru.ParseAsync(data, offset, connection, null).Then(metaDataTru =>
|
||||||
{
|
{
|
||||||
|
if (metaDataTru.Size > cl)
|
||||||
|
{
|
||||||
|
rt.TriggerError(new ParserLimitException(
|
||||||
|
"Typed TDU metadata exceeds its declared payload length."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
offset += metaDataTru.Size;
|
offset += metaDataTru.Size;
|
||||||
|
|
||||||
rt.Trigger(new ParsedTdu()
|
rt.Trigger(new ParsedTdu()
|
||||||
@@ -269,6 +282,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
@@ -347,8 +362,6 @@ namespace Esiur.Data
|
|||||||
|
|
||||||
public static ParsedTdu ParseSync(byte[] data, uint offset, uint ends, Warehouse warehouse)
|
public static ParsedTdu ParseSync(byte[] data, uint offset, uint ends, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
// @TODO: add protection against memory allocation attacks by checking the length of the data before parsing it.
|
|
||||||
|
|
||||||
var h = data[offset++];
|
var h = data[offset++];
|
||||||
|
|
||||||
var cls = (TduClass)(h >> 6);
|
var cls = (TduClass)(h >> 6);
|
||||||
@@ -411,6 +424,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(warehouse, cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
@@ -422,6 +437,10 @@ namespace Esiur.Data
|
|||||||
//offset += data[offset] + (uint)1;
|
//offset += data[offset] + (uint)1;
|
||||||
|
|
||||||
var metaDataTru = Tru.Parse(data, offset, warehouse);
|
var metaDataTru = Tru.Parse(data, offset, warehouse);
|
||||||
|
|
||||||
|
if (metaDataTru.Size > cl)
|
||||||
|
throw new ParserLimitException("Typed TDU metadata exceeds its declared payload length.");
|
||||||
|
|
||||||
offset += metaDataTru.Size;
|
offset += metaDataTru.Size;
|
||||||
|
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
@@ -453,6 +472,8 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
ParserGuard.EnsurePacketSize(warehouse, cl);
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new ParsedTdu()
|
return new ParsedTdu()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Esiur.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when untrusted input exceeds a configured parser budget.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ParserLimitException : Exception
|
||||||
|
{
|
||||||
|
public ParserLimitException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ParserGuard
|
||||||
|
{
|
||||||
|
internal static Warehouse? GetWarehouse(EpConnection? connection)
|
||||||
|
=> connection?.ParsingWarehouse;
|
||||||
|
|
||||||
|
internal static void EnsurePacketSize(Warehouse? warehouse, ulong size)
|
||||||
|
{
|
||||||
|
var limit = warehouse?.Configuration.Parser.MaximumPacketSize ?? 0;
|
||||||
|
if (limit > 0 && size > limit)
|
||||||
|
throw new ParserLimitException(
|
||||||
|
$"Declared packet payload of {size} bytes exceeds the {limit}-byte limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EnsureAllocation(Warehouse? warehouse, ulong size, string kind)
|
||||||
|
{
|
||||||
|
var limit = warehouse?.Configuration.Parser.MaximumAllocationSize ?? 0;
|
||||||
|
if (limit > 0 && size > limit)
|
||||||
|
throw new ParserLimitException(
|
||||||
|
$"Decoded {kind} allocation of {size} bytes exceeds the {limit}-byte limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EnsureCollectionCount(
|
||||||
|
Warehouse? warehouse,
|
||||||
|
int count,
|
||||||
|
int estimatedBytesPerItem = 0)
|
||||||
|
{
|
||||||
|
var configuration = warehouse?.Configuration.Parser;
|
||||||
|
if (configuration == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (configuration.MaximumCollectionItems > 0 && count > configuration.MaximumCollectionItems)
|
||||||
|
throw new ParserLimitException(
|
||||||
|
$"Decoded collection count of {count} exceeds the {configuration.MaximumCollectionItems}-item limit.");
|
||||||
|
|
||||||
|
if (estimatedBytesPerItem > 0)
|
||||||
|
EnsureAllocation(
|
||||||
|
warehouse,
|
||||||
|
(ulong)count * (ulong)estimatedBytesPerItem,
|
||||||
|
"collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ulong MultiplySaturated(ulong value, ulong multiplier)
|
||||||
|
=> value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier;
|
||||||
|
}
|
||||||
@@ -21,7 +21,11 @@ namespace Esiur.Data
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static PlainTdu Parse(byte[] data, uint offset, uint ends)
|
public static PlainTdu Parse(
|
||||||
|
byte[] data,
|
||||||
|
uint offset,
|
||||||
|
uint ends,
|
||||||
|
ulong maximumPayloadLength = ulong.MaxValue)
|
||||||
{
|
{
|
||||||
var oOffset = offset;
|
var oOffset = offset;
|
||||||
|
|
||||||
@@ -91,6 +95,10 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
if (cl > maximumPayloadLength)
|
||||||
|
throw new ParserLimitException(
|
||||||
|
$"Declared packet payload of {cl} bytes exceeds the {maximumPayloadLength}-byte limit.");
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new PlainTdu()
|
return new PlainTdu()
|
||||||
{
|
{
|
||||||
@@ -128,6 +136,10 @@ namespace Esiur.Data
|
|||||||
for (uint i = 0; i < cll; i++)
|
for (uint i = 0; i < cll; i++)
|
||||||
cl = cl << 8 | data[offset++];
|
cl = cl << 8 | data[offset++];
|
||||||
|
|
||||||
|
if (cl > maximumPayloadLength)
|
||||||
|
throw new ParserLimitException(
|
||||||
|
$"Declared packet payload of {cl} bytes exceeds the {maximumPayloadLength}-byte limit.");
|
||||||
|
|
||||||
if (ends - offset < cl)
|
if (ends - offset < cl)
|
||||||
return new PlainTdu()
|
return new PlainTdu()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
|||||||
c.Assign(s);
|
c.Assign(s);
|
||||||
Add(c);
|
Add(c);
|
||||||
|
|
||||||
|
// A derived server can reject admission (for example, due to a per-peer
|
||||||
|
// connection quota) by not adding the connection and closing its socket.
|
||||||
|
if (!Connections.Contains(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ClientConnected(c);
|
ClientConnected(c);
|
||||||
|
|||||||
@@ -185,7 +185,12 @@ public class EpAuthPacket : Packet
|
|||||||
if (NotEnough(offset, ends, 1))
|
if (NotEnough(offset, ends, 1))
|
||||||
return -dataLengthNeeded;
|
return -dataLengthNeeded;
|
||||||
|
|
||||||
Tdu = PlainTdu.Parse(data, offset, ends);//, _warehouse);
|
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||||
|
Tdu = PlainTdu.Parse(
|
||||||
|
data,
|
||||||
|
offset,
|
||||||
|
ends,
|
||||||
|
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||||
|
|
||||||
if (Tdu.Value.Class == TduClass.Invalid)
|
if (Tdu.Value.Class == TduClass.Invalid)
|
||||||
return -(int)Tdu.Value.TotalLength;
|
return -(int)Tdu.Value.TotalLength;
|
||||||
|
|||||||
@@ -135,7 +135,12 @@ class EpPacket : Packet
|
|||||||
if (NotEnough(offset, ends, 1))
|
if (NotEnough(offset, ends, 1))
|
||||||
return -dataLengthNeeded;
|
return -dataLengthNeeded;
|
||||||
|
|
||||||
Tdu = PlainTdu.Parse(data, offset, ends);
|
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||||
|
Tdu = PlainTdu.Parse(
|
||||||
|
data,
|
||||||
|
offset,
|
||||||
|
ends,
|
||||||
|
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||||
|
|
||||||
if (Tdu.Value.Class == TduClass.Invalid)
|
if (Tdu.Value.Class == TduClass.Invalid)
|
||||||
return -(int)Tdu.Value.TotalLength;
|
return -(int)Tdu.Value.TotalLength;
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
Warehouse _serverWarehouse;
|
Warehouse _serverWarehouse;
|
||||||
|
|
||||||
public EpServer Server => _server;
|
public EpServer Server => _server;
|
||||||
|
internal Warehouse ParsingWarehouse => Instance?.Warehouse ?? _serverWarehouse ?? Warehouse.Default;
|
||||||
//public EpServer Server
|
//public EpServer Server
|
||||||
//{
|
//{
|
||||||
// get => _server;
|
// get => _server;
|
||||||
@@ -1857,6 +1858,11 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
offset = processPacket(msg, offset, ends, data, chunkId);
|
offset = processPacket(msg, offset, ends, data, chunkId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (ParserLimitException ex)
|
||||||
|
{
|
||||||
|
Global.Log("EpConnection:ParserLimit", LogType.Warning, ex.Message);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Global.Log(ex);
|
Global.Log(ex);
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ partial class EpConnection
|
|||||||
// Global.Counters suffer from). Used by the deadlock experiments.
|
// Global.Counters suffer from). Used by the deadlock experiments.
|
||||||
/// <summary>Number of resources fully attached on this connection (a monotonic progress signal).</summary>
|
/// <summary>Number of resources fully attached on this connection (a monotonic progress signal).</summary>
|
||||||
public long AttachedResourceCount { get; private set; }
|
public long AttachedResourceCount { get; private set; }
|
||||||
|
/// <summary>Number of resource attach or reattach requests sent by this connection.</summary>
|
||||||
|
public long ResourceAttachRequestCount { get; private set; }
|
||||||
/// <summary>Number of wait-for-cycle breaks (placeholders returned to break a cycle) on this connection.</summary>
|
/// <summary>Number of wait-for-cycle breaks (placeholders returned to break a cycle) on this connection.</summary>
|
||||||
public long CycleBreakCount { get; private set; }
|
public long CycleBreakCount { get; private set; }
|
||||||
/// <summary>Number of placeholders returned where no genuine cycle existed (legacy resolver only).</summary>
|
/// <summary>Number of placeholders returned where no genuine cycle existed (legacy resolver only).</summary>
|
||||||
@@ -235,6 +237,7 @@ partial class EpConnection
|
|||||||
volatile int _callbackCounter = 0;
|
volatile int _callbackCounter = 0;
|
||||||
|
|
||||||
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
|
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
|
||||||
|
readonly HashSet<uint> _peerAttachmentRequests = new HashSet<uint>();
|
||||||
|
|
||||||
// resources might get attached by the client
|
// resources might get attached by the client
|
||||||
internal KeyList<IResource, DateTime> _cache = new();
|
internal KeyList<IResource, DateTime> _cache = new();
|
||||||
@@ -928,38 +931,55 @@ partial class EpConnection
|
|||||||
|
|
||||||
var resourceId = Convert.ToUInt32(value);
|
var resourceId = Convert.ToUInt32(value);
|
||||||
|
|
||||||
|
if (!TryBeginPeerAttachment(resourceId, out var exceptionCode, out var exceptionMessage))
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)exceptionCode, exceptionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Instance.Warehouse.GetById(resourceId).Then((res) =>
|
Instance.Warehouse.GetById(resourceId).Then((res) =>
|
||||||
{
|
{
|
||||||
if (res != null)
|
try
|
||||||
{
|
{
|
||||||
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (res != null)
|
||||||
{
|
{
|
||||||
SendError(ErrorType.Management, callback, 6);
|
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
||||||
return;
|
{
|
||||||
|
SendError(ErrorType.Management, callback, 6);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = res as IResource;
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
Unsubscribe(r);
|
||||||
|
|
||||||
|
// reply ok
|
||||||
|
SendReply(EpPacketReply.Completed, callback,
|
||||||
|
r.Instance.Definition.Id,
|
||||||
|
r.Instance.Age,
|
||||||
|
r.Instance.Link,
|
||||||
|
r.Instance.Hops,
|
||||||
|
r.Instance.Serialize());
|
||||||
|
|
||||||
|
// subscribe
|
||||||
|
Subscribe(r);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// reply failed
|
||||||
|
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
var r = res as IResource;
|
|
||||||
|
|
||||||
// unsubscribe
|
|
||||||
Unsubscribe(r);
|
|
||||||
|
|
||||||
// reply ok
|
|
||||||
SendReply(EpPacketReply.Completed, callback,
|
|
||||||
r.Instance.Definition.Id,
|
|
||||||
r.Instance.Age,
|
|
||||||
r.Instance.Link,
|
|
||||||
r.Instance.Hops,
|
|
||||||
r.Instance.Serialize());
|
|
||||||
|
|
||||||
// subscribe
|
|
||||||
Subscribe(r);
|
|
||||||
}
|
}
|
||||||
else
|
finally
|
||||||
{
|
{
|
||||||
// reply failed
|
EndPeerAttachment(resourceId);
|
||||||
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
|
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
|
||||||
}
|
}
|
||||||
|
}).Error(ex =>
|
||||||
|
{
|
||||||
|
EndPeerAttachment(resourceId);
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.GeneralFailure, ex.Message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,40 +994,57 @@ partial class EpConnection
|
|||||||
|
|
||||||
var age = Convert.ToUInt64(args[1]);
|
var age = Convert.ToUInt64(args[1]);
|
||||||
|
|
||||||
|
if (!TryBeginPeerAttachment(resourceId, out var exceptionCode, out var exceptionMessage))
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)exceptionCode, exceptionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Instance.Warehouse.GetById(resourceId).Then((res) =>
|
Instance.Warehouse.GetById(resourceId).Then((res) =>
|
||||||
{
|
{
|
||||||
if (res != null)
|
try
|
||||||
{
|
{
|
||||||
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
if (res != null)
|
||||||
{
|
{
|
||||||
SendError(ErrorType.Management, callback, 6);
|
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
|
||||||
return;
|
{
|
||||||
|
SendError(ErrorType.Management, callback, 6);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = res as IResource;
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
Unsubscribe(r);
|
||||||
|
|
||||||
|
|
||||||
|
// reply ok
|
||||||
|
SendReply(EpPacketReply.Completed, callback,
|
||||||
|
r.Instance.Definition.Id,
|
||||||
|
r.Instance.Age,
|
||||||
|
r.Instance.Link,
|
||||||
|
r.Instance.Hops,
|
||||||
|
r.Instance.SerializeAfter(age));
|
||||||
|
|
||||||
|
|
||||||
|
// subscribe
|
||||||
|
Subscribe(r);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// reply failed
|
||||||
|
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
var r = res as IResource;
|
|
||||||
|
|
||||||
// unsubscribe
|
|
||||||
Unsubscribe(r);
|
|
||||||
|
|
||||||
|
|
||||||
// reply ok
|
|
||||||
SendReply(EpPacketReply.Completed, callback,
|
|
||||||
r.Instance.Definition.Id,
|
|
||||||
r.Instance.Age,
|
|
||||||
r.Instance.Link,
|
|
||||||
r.Instance.Hops,
|
|
||||||
r.Instance.SerializeAfter(age));
|
|
||||||
|
|
||||||
|
|
||||||
// subscribe
|
|
||||||
Subscribe(r);
|
|
||||||
}
|
}
|
||||||
else
|
finally
|
||||||
{
|
{
|
||||||
// reply failed
|
EndPeerAttachment(resourceId);
|
||||||
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
|
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
|
|
||||||
}
|
}
|
||||||
|
}).Error(ex =>
|
||||||
|
{
|
||||||
|
EndPeerAttachment(resourceId);
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.GeneralFailure, ex.Message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2660,6 +2697,88 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AsyncReply<EpResource> AttachmentLimitError(string message)
|
||||||
|
{
|
||||||
|
var reply = new AsyncReply<EpResource>();
|
||||||
|
// AsyncReply throws when an error is triggered before any observer is attached.
|
||||||
|
// Install a sink so this already-failed reply can be returned and observed by Codec.
|
||||||
|
reply.Error(_ => { });
|
||||||
|
reply.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.AttachmentLimitExceeded,
|
||||||
|
message));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FailResourceAttachment(
|
||||||
|
uint id,
|
||||||
|
AsyncReply<EpResource> reply,
|
||||||
|
Exception exception)
|
||||||
|
{
|
||||||
|
_resourceRequests.Remove(id);
|
||||||
|
_neededResources.Remove(id);
|
||||||
|
ClearResourceFetchNode(id);
|
||||||
|
reply.TriggerError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReserveResourceAttachment(
|
||||||
|
uint id,
|
||||||
|
AsyncReply<EpResource> reply,
|
||||||
|
uint[] requestSequence,
|
||||||
|
out AsyncReply<EpResource> alternative)
|
||||||
|
{
|
||||||
|
lock (_resourceRequests.SyncRoot)
|
||||||
|
{
|
||||||
|
var existing = _resourceRequests[id];
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
alternative = existing.Reply;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration = ParsingWarehouse.Configuration.ResourceAttachments;
|
||||||
|
|
||||||
|
if (configuration.MaximumPendingAttachmentsPerConnection > 0
|
||||||
|
&& _resourceRequests.Count >= configuration.MaximumPendingAttachmentsPerConnection)
|
||||||
|
{
|
||||||
|
alternative = AttachmentLimitError(
|
||||||
|
"The pending resource attachment limit for this connection was reached.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachedCount = 0;
|
||||||
|
lock (_attachedResources.SyncRoot)
|
||||||
|
{
|
||||||
|
var stale = new List<uint>();
|
||||||
|
foreach (var pair in _attachedResources)
|
||||||
|
{
|
||||||
|
if (pair.Value != null && pair.Value.TryGetTarget(out _))
|
||||||
|
attachedCount++;
|
||||||
|
else
|
||||||
|
stale.Add(pair.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var staleId in stale)
|
||||||
|
_attachedResources.Remove(staleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.MaximumAttachedResourcesPerConnection > 0
|
||||||
|
&& attachedCount + _resourceRequests.Count
|
||||||
|
>= configuration.MaximumAttachedResourcesPerConnection)
|
||||||
|
{
|
||||||
|
alternative = AttachmentLimitError(
|
||||||
|
"The resource attachment limit for this connection was reached.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resourceRequests.Add(
|
||||||
|
id,
|
||||||
|
new FetchRequestInfo<EpResource, uint>(reply, requestSequence));
|
||||||
|
alternative = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AsyncReply<EpResource> FetchResource(uint id, uint[] requestSequence)
|
public AsyncReply<EpResource> FetchResource(uint id, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
//lock (fetchLock)
|
//lock (fetchLock)
|
||||||
@@ -2746,19 +2865,26 @@ partial class EpConnection
|
|||||||
var newSequence = requestSequence != null ? requestSequence.Concat(new uint[] { id }).ToArray() : new uint[] { id };
|
var newSequence = requestSequence != null ? requestSequence.Concat(new uint[] { id }).ToArray() : new uint[] { id };
|
||||||
|
|
||||||
var reply = new AsyncReply<EpResource>();
|
var reply = new AsyncReply<EpResource>();
|
||||||
_resourceRequests.Add(id, new FetchRequestInfo<EpResource, uint>(reply, newSequence));
|
if (!TryReserveResourceAttachment(id, reply, newSequence, out var alternative))
|
||||||
|
return alternative;
|
||||||
|
|
||||||
// This fetch's parent now waits on `id` until it attaches.
|
// This fetch's parent now waits on `id` until it attaches.
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
AddResourceFetchBlock(parent.Value, id);
|
AddResourceFetchBlock(parent.Value, id);
|
||||||
|
|
||||||
|
ResourceAttachRequestCount++;
|
||||||
SendRequest(EpPacketRequest.AttachResource, id)
|
SendRequest(EpPacketRequest.AttachResource, id)
|
||||||
.Then((result) =>
|
.Then((result) =>
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
reply.TriggerError(new AsyncException(ErrorType.Management,
|
FailResourceAttachment(
|
||||||
(ushort)ExceptionCode.ResourceNotFound, "Null response"));
|
id,
|
||||||
|
reply,
|
||||||
|
new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ResourceNotFound,
|
||||||
|
"Null response"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2801,11 +2927,7 @@ partial class EpConnection
|
|||||||
ClearResourceFetchNode(id);
|
ClearResourceFetchNode(id);
|
||||||
TryPublishDeliveredRoots();
|
TryPublishDeliveredRoots();
|
||||||
reply.Trigger(dr);
|
reply.Trigger(dr);
|
||||||
}).Error(ex => {
|
}).Error(ex => FailResourceAttachment(id, reply, ex));
|
||||||
_resourceRequests.Remove(id);
|
|
||||||
ClearResourceFetchNode(id);
|
|
||||||
reply.TriggerError(ex);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeDef == null)
|
if (typeDef == null)
|
||||||
@@ -2827,17 +2949,14 @@ partial class EpConnection
|
|||||||
_neededResources[id] = resource;
|
_neededResources[id] = resource;
|
||||||
Instance.Warehouse.Put(Instance.Link + "/" + id.ToString(), resource)
|
Instance.Warehouse.Put(Instance.Link + "/" + id.ToString(), resource)
|
||||||
.Then(initResource)
|
.Then(initResource)
|
||||||
.Error(ex => reply.TriggerError(ex));
|
.Error(ex => FailResourceAttachment(id, reply, ex));
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
initResource(resource);
|
initResource(resource);
|
||||||
}
|
}
|
||||||
}).Error((ex) =>
|
}).Error(ex => FailResourceAttachment(id, reply, ex));
|
||||||
{
|
|
||||||
reply.TriggerError(ex);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2854,7 +2973,8 @@ partial class EpConnection
|
|||||||
// references in the graph can resolve back to this instance.
|
// references in the graph can resolve back to this instance.
|
||||||
_neededResources[id] = resource;
|
_neededResources[id] = resource;
|
||||||
Instance.Warehouse.Put(this.Instance.Link + "/" + id.ToString(), resource)
|
Instance.Warehouse.Put(this.Instance.Link + "/" + id.ToString(), resource)
|
||||||
.Then(initResource).Error((ex) => reply.TriggerError(ex));
|
.Then(initResource)
|
||||||
|
.Error(ex => FailResourceAttachment(id, reply, ex));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2862,13 +2982,11 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}).Error((ex) =>
|
}).Error(ex =>
|
||||||
{
|
{
|
||||||
// Failed to attach: drop the in-flight request and wait-for edges so a
|
// Failed to attach: drop the in-flight request and wait-for edges so a
|
||||||
// later retry is not blocked by a stale entry.
|
// later retry is not blocked by a stale entry.
|
||||||
_resourceRequests.Remove(id);
|
FailResourceAttachment(id, reply, ex);
|
||||||
ClearResourceFetchNode(id);
|
|
||||||
reply.TriggerError(ex);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -2903,8 +3021,10 @@ partial class EpConnection
|
|||||||
|
|
||||||
var reply = new AsyncReply<EpResource>();
|
var reply = new AsyncReply<EpResource>();
|
||||||
var sequence = new uint[] { id };
|
var sequence = new uint[] { id };
|
||||||
_resourceRequests.Add(id, new FetchRequestInfo<EpResource, uint>(reply, sequence));
|
if (!TryReserveResourceAttachment(id, reply, sequence, out var alternative))
|
||||||
|
return alternative;
|
||||||
|
|
||||||
|
ResourceAttachRequestCount++;
|
||||||
SendRequest(EpPacketRequest.ReattachResource, id, age).Then(result =>
|
SendRequest(EpPacketRequest.ReattachResource, id, age).Then(result =>
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -3106,10 +3226,62 @@ partial class EpConnection
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryBeginPeerAttachment(
|
||||||
|
uint resourceId,
|
||||||
|
out ExceptionCode exceptionCode,
|
||||||
|
out string exceptionMessage)
|
||||||
|
{
|
||||||
|
lock (_subscriptionsLock)
|
||||||
|
{
|
||||||
|
var configuration = ParsingWarehouse.Configuration.ResourceAttachments;
|
||||||
|
var alreadyAttached = _subscriptions.Keys.Any(
|
||||||
|
resource => resource.Instance?.Id == resourceId);
|
||||||
|
|
||||||
|
if (configuration.RejectDuplicateAttachments
|
||||||
|
&& (alreadyAttached || _peerAttachmentRequests.Contains(resourceId)))
|
||||||
|
{
|
||||||
|
exceptionCode = ExceptionCode.AlreadyAttached;
|
||||||
|
exceptionMessage = $"Resource {resourceId} is already attached or being attached by this connection.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.MaximumPendingAttachmentsPerConnection > 0
|
||||||
|
&& _peerAttachmentRequests.Count >= configuration.MaximumPendingAttachmentsPerConnection)
|
||||||
|
{
|
||||||
|
exceptionCode = ExceptionCode.AttachmentLimitExceeded;
|
||||||
|
exceptionMessage = "The pending resource attachment limit for this connection was reached.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.MaximumAttachedResourcesPerConnection > 0
|
||||||
|
&& _subscriptions.Count + _peerAttachmentRequests.Count
|
||||||
|
>= configuration.MaximumAttachedResourcesPerConnection)
|
||||||
|
{
|
||||||
|
exceptionCode = ExceptionCode.AttachmentLimitExceeded;
|
||||||
|
exceptionMessage = "The resource attachment limit for this connection was reached.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_peerAttachmentRequests.Add(resourceId);
|
||||||
|
exceptionCode = default;
|
||||||
|
exceptionMessage = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndPeerAttachment(uint resourceId)
|
||||||
|
{
|
||||||
|
lock (_subscriptionsLock)
|
||||||
|
_peerAttachmentRequests.Remove(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
private void Subscribe(IResource resource)
|
private void Subscribe(IResource resource)
|
||||||
{
|
{
|
||||||
lock (_subscriptionsLock)
|
lock (_subscriptionsLock)
|
||||||
{
|
{
|
||||||
|
if (_subscriptions.ContainsKey(resource))
|
||||||
|
return;
|
||||||
|
|
||||||
resource.Instance.EventOccurred += Instance_EventOccurred;
|
resource.Instance.EventOccurred += Instance_EventOccurred;
|
||||||
resource.Instance.CustomEventOccurred += Instance_CustomEventOccurred;
|
resource.Instance.CustomEventOccurred += Instance_CustomEventOccurred;
|
||||||
resource.Instance.PropertyModified += Instance_PropertyModified;
|
resource.Instance.PropertyModified += Instance_PropertyModified;
|
||||||
@@ -3147,6 +3319,7 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
_subscriptions.Clear();
|
_subscriptions.Clear();
|
||||||
|
_peerAttachmentRequests.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ namespace Esiur.Protocol;
|
|||||||
|
|
||||||
public class EpServer : NetworkServer<EpConnection>, IResource
|
public class EpServer : NetworkServer<EpConnection>, IResource
|
||||||
{
|
{
|
||||||
|
readonly object _peerConnectionsLock = new object();
|
||||||
|
readonly Dictionary<IPAddress, int> _peerConnectionCounts = new Dictionary<IPAddress, int>();
|
||||||
|
readonly Dictionary<EpConnection, IPAddress> _admittedConnections = new Dictionary<EpConnection, IPAddress>();
|
||||||
|
|
||||||
|
|
||||||
//[Attribute]
|
//[Attribute]
|
||||||
@@ -167,19 +170,99 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
|||||||
|
|
||||||
public override void Add(EpConnection connection)
|
public override void Add(EpConnection connection)
|
||||||
{
|
{
|
||||||
connection.Handle(ResourceOperation.Configure,
|
if (!TryAdmitConnection(connection))
|
||||||
new EpServerConnectionContext() {
|
{
|
||||||
Server = this,
|
Global.Log(
|
||||||
Warehouse = Instance.Warehouse }
|
"EpServer:ConnectionLimit",
|
||||||
);
|
LogType.Warning,
|
||||||
|
$"Rejected connection from {connection.RemoteEndPoint?.Address}: per-IP limit reached.");
|
||||||
|
connection.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
connection.ExceptionLevel = ExceptionLevel;
|
try
|
||||||
base.Add(connection);
|
{
|
||||||
|
connection.Handle(ResourceOperation.Configure,
|
||||||
|
new EpServerConnectionContext()
|
||||||
|
{
|
||||||
|
Server = this,
|
||||||
|
Warehouse = Instance.Warehouse
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.ExceptionLevel = ExceptionLevel;
|
||||||
|
base.Add(connection);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ReleaseConnection(connection);
|
||||||
|
connection.Close();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Remove(EpConnection connection)
|
public override void Remove(EpConnection connection)
|
||||||
{
|
{
|
||||||
base.Remove(connection);
|
try
|
||||||
|
{
|
||||||
|
base.Remove(connection);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseConnection(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryAdmitConnection(EpConnection connection)
|
||||||
|
{
|
||||||
|
var address = NormalizeAddress(connection.RemoteEndPoint?.Address);
|
||||||
|
if (address == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
lock (_peerConnectionsLock)
|
||||||
|
{
|
||||||
|
var count = _peerConnectionCounts.TryGetValue(address, out var current)
|
||||||
|
? current
|
||||||
|
: 0;
|
||||||
|
var limit = Instance.Warehouse.Configuration.Connections.MaximumConnectionsPerIpAddress;
|
||||||
|
|
||||||
|
if (limit > 0 && count >= limit)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_peerConnectionCounts[address] = count + 1;
|
||||||
|
_admittedConnections[connection] = address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseConnection(EpConnection connection)
|
||||||
|
{
|
||||||
|
lock (_peerConnectionsLock)
|
||||||
|
{
|
||||||
|
if (!_admittedConnections.TryGetValue(connection, out var address))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_admittedConnections.Remove(connection);
|
||||||
|
|
||||||
|
if (!_peerConnectionCounts.TryGetValue(address, out var count))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (count <= 1)
|
||||||
|
_peerConnectionCounts.Remove(address);
|
||||||
|
else
|
||||||
|
_peerConnectionCounts[address] = count - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPAddress NormalizeAddress(IPAddress address)
|
||||||
|
=> address?.IsIPv4MappedToIPv6 == true ? address.MapToIPv4() : address;
|
||||||
|
|
||||||
|
internal int GetConnectionCount(IPAddress address)
|
||||||
|
{
|
||||||
|
address = NormalizeAddress(address);
|
||||||
|
lock (_peerConnectionsLock)
|
||||||
|
return address != null && _peerConnectionCounts.TryGetValue(address, out var count)
|
||||||
|
? count
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ClientDisconnected(EpConnection connection)
|
protected override void ClientDisconnected(EpConnection connection)
|
||||||
|
|||||||
@@ -8,6 +8,45 @@ namespace Esiur.Resource;
|
|||||||
public sealed class WarehouseConfiguration
|
public sealed class WarehouseConfiguration
|
||||||
{
|
{
|
||||||
public RateControlConfiguration RateControl { get; set; } = new RateControlConfiguration();
|
public RateControlConfiguration RateControl { get; set; } = new RateControlConfiguration();
|
||||||
|
public ParserConfiguration Parser { get; set; } = new ParserConfiguration();
|
||||||
|
public ResourceAttachmentConfiguration ResourceAttachments { get; set; } = new ResourceAttachmentConfiguration();
|
||||||
|
public ConnectionConfiguration Connections { get; set; } = new ConnectionConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limits memory and object amplification while parsing untrusted packets.
|
||||||
|
/// A value of zero disables the corresponding limit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ParserConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>Maximum declared TDU payload retained for one packet.</summary>
|
||||||
|
public uint MaximumPacketSize { get; set; } = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>Maximum allocation produced by one decoded value.</summary>
|
||||||
|
public uint MaximumAllocationSize { get; set; } = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>Maximum number of values decoded into one collection.</summary>
|
||||||
|
public int MaximumCollectionItems { get; set; } = 65_536;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limits resources imported from, or attached by, one remote connection.
|
||||||
|
/// A value of zero disables the corresponding limit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ResourceAttachmentConfiguration
|
||||||
|
{
|
||||||
|
public int MaximumAttachedResourcesPerConnection { get; set; } = 4_096;
|
||||||
|
public int MaximumPendingAttachmentsPerConnection { get; set; } = 128;
|
||||||
|
public bool RejectDuplicateAttachments { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures network connection admission limits.
|
||||||
|
/// A value of zero disables the corresponding limit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConnectionConfiguration
|
||||||
|
{
|
||||||
|
public int MaximumConnectionsPerIpAddress { get; set; } = 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ internal static class Program
|
|||||||
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
|
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
|
||||||
{
|
{
|
||||||
warehouse.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
warehouse.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
||||||
|
warehouse.Configuration.Parser.MaximumPacketSize = 8 * 1024 * 1024;
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 4 * 1024 * 1024;
|
||||||
|
warehouse.Configuration.Parser.MaximumCollectionItems = 65_536;
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumAttachedResourcesPerConnection = 4_096;
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumPendingAttachmentsPerConnection = 128;
|
||||||
|
warehouse.Configuration.Connections.MaximumConnectionsPerIpAddress = 64;
|
||||||
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 10;
|
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 10;
|
||||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||||
{
|
{
|
||||||
@@ -139,6 +145,8 @@ internal static class Program
|
|||||||
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
||||||
{
|
{
|
||||||
warehouse.RegisterAuthenticationProvider(new ClientAuthenticationProvider());
|
warehouse.RegisterAuthenticationProvider(new ClientAuthenticationProvider());
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumAttachedResourcesPerConnection = 4_096;
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumPendingAttachmentsPerConnection = 128;
|
||||||
|
|
||||||
return await warehouse.Get<EpConnection>($"ep://localhost:{port}", new EpConnectionContext
|
return await warehouse.Get<EpConnection>($"ep://localhost:{port}", new EpConnectionContext
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Coverage includes:
|
|||||||
- pull streams backed by `IAsyncEnumerable<T>`;
|
- pull streams backed by `IAsyncEnumerable<T>`;
|
||||||
- `TerminateExecution` through async-enumerator disposal;
|
- `TerminateExecution` through async-enumerator disposal;
|
||||||
- `HaltExecution` and `ResumeExecution` for a cooperative push stream.
|
- `HaltExecution` and `ResumeExecution` for a cooperative push stream.
|
||||||
|
- parser allocation and collection budgets, attachment quotas, and per-IP connection limits.
|
||||||
|
|
||||||
Run it from the repository root:
|
Run it from the repository root:
|
||||||
|
|
||||||
@@ -41,3 +42,17 @@ public void Call()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Security limits are configured per Warehouse. A value of zero disables an individual limit:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
warehouse.Configuration.Parser.MaximumPacketSize = 8 * 1024 * 1024;
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 4 * 1024 * 1024;
|
||||||
|
warehouse.Configuration.Parser.MaximumCollectionItems = 65_536;
|
||||||
|
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumAttachedResourcesPerConnection = 4_096;
|
||||||
|
warehouse.Configuration.ResourceAttachments.MaximumPendingAttachmentsPerConnection = 128;
|
||||||
|
warehouse.Configuration.ResourceAttachments.RejectDuplicateAttachments = true;
|
||||||
|
|
||||||
|
warehouse.Configuration.Connections.MaximumConnectionsPerIpAddress = 64;
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Esiur.Core;
|
||||||
|
using Esiur.Protocol;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit.Integration;
|
||||||
|
|
||||||
|
[Collection("Integration")]
|
||||||
|
public class AttachmentSecurityTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ConcurrentFetches_AreCoalescedIntoOneAttachRequest()
|
||||||
|
{
|
||||||
|
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
var fetches = Enumerable.Range(0, 16)
|
||||||
|
.Select(_ => Task.Run(async () => await cluster.Connection.Get("sys/first")))
|
||||||
|
.ToArray();
|
||||||
|
var resources = await Task.WhenAll(fetches).WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
Assert.All(resources, resource => Assert.Same(resources[0], resource));
|
||||||
|
Assert.Equal(1, cluster.Connection.ResourceAttachRequestCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Connection_RefusesResourcesBeyondConfiguredAttachmentLimit()
|
||||||
|
{
|
||||||
|
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
cluster.ClientWarehouse.Configuration.ResourceAttachments
|
||||||
|
.MaximumAttachedResourcesPerConnection = 1;
|
||||||
|
|
||||||
|
Assert.NotNull(await cluster.Connection.Get("sys/first"));
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||||
|
await cluster.Connection.Get("sys/second"));
|
||||||
|
|
||||||
|
Assert.Equal(ExceptionCode.AttachmentLimitExceeded, exception.Code);
|
||||||
|
Assert.Equal(1, cluster.Connection.ResourceAttachRequestCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Task<IntegrationCluster> StartCluster()
|
||||||
|
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||||
|
{
|
||||||
|
await warehouse.Put("sys/first", new RateLimitedResource());
|
||||||
|
await warehouse.Put("sys/second", new RateLimitedResource());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Net.Packets;
|
||||||
|
using Esiur.Resource;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class ParserSecurityTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PacketParser_RejectsOversizedDeclarationBeforePayloadArrives()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
warehouse.Configuration.Parser.MaximumPacketSize = 1_024;
|
||||||
|
var packet = new EpPacket(warehouse);
|
||||||
|
|
||||||
|
// EP notification with a RawData TDU whose four-byte length declares 1 MiB,
|
||||||
|
// without supplying that payload. The declaration itself must be rejected.
|
||||||
|
var data = new byte[] { 0x20, 0x60, 0x00, 0x10, 0x00, 0x00 };
|
||||||
|
|
||||||
|
Assert.Throws<ParserLimitException>(() =>
|
||||||
|
{
|
||||||
|
packet.Parse(data, 0, (uint)data.Length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RawDataParser_EnforcesAllocationBudget()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 10;
|
||||||
|
var data = Codec.Compose(new byte[20], warehouse, null);
|
||||||
|
|
||||||
|
Assert.Throws<ParserLimitException>(() => Codec.ParseSync(data, 0, warehouse));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringParser_AccountsForDecodedUtf16Allocation()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 10;
|
||||||
|
var data = Codec.Compose("123456", warehouse, null);
|
||||||
|
|
||||||
|
Assert.Throws<ParserLimitException>(() => Codec.ParseSync(data, 0, warehouse));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ListParser_EnforcesCollectionItemBudget()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var data = Codec.Compose(new object[] { true, false, true }, warehouse, null);
|
||||||
|
warehouse.Configuration.Parser.MaximumCollectionItems = 2;
|
||||||
|
|
||||||
|
Assert.Throws<ParserLimitException>(() => Codec.ParseSync(data, 0, warehouse));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Esiur.Core;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Net;
|
||||||
|
using Esiur.Net.Sockets;
|
||||||
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class PeerConnectionLimitTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Server_RejectsConnectionsAbovePerIpLimit_AndReleasesClosedSlot()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
warehouse.Configuration.Connections.MaximumConnectionsPerIpAddress = 1;
|
||||||
|
|
||||||
|
var server = new EpServer();
|
||||||
|
server.Instance = new Instance(warehouse, 1, "server", server, null);
|
||||||
|
server.Connections = new AutoList<EpConnection, NetworkServer<EpConnection>>(server);
|
||||||
|
|
||||||
|
var address = IPAddress.Parse("192.0.2.10");
|
||||||
|
var firstSocket = new TestSocket(address, 10001);
|
||||||
|
var first = new EpConnection();
|
||||||
|
first.Assign(firstSocket);
|
||||||
|
server.Add(first);
|
||||||
|
|
||||||
|
var rejectedSocket = new TestSocket(address, 10002);
|
||||||
|
var rejected = new EpConnection();
|
||||||
|
rejected.Assign(rejectedSocket);
|
||||||
|
server.Add(rejected);
|
||||||
|
|
||||||
|
Assert.Single(server.Connections);
|
||||||
|
Assert.Equal(SocketState.Closed, rejectedSocket.State);
|
||||||
|
Assert.Equal(1, server.GetConnectionCount(address));
|
||||||
|
|
||||||
|
firstSocket.Close();
|
||||||
|
Assert.Equal(0, server.GetConnectionCount(address));
|
||||||
|
|
||||||
|
var replacementSocket = new TestSocket(address, 10003);
|
||||||
|
var replacement = new EpConnection();
|
||||||
|
replacement.Assign(replacementSocket);
|
||||||
|
server.Add(replacement);
|
||||||
|
|
||||||
|
Assert.Equal(SocketState.Established, replacementSocket.State);
|
||||||
|
Assert.Equal(1, server.GetConnectionCount(address));
|
||||||
|
|
||||||
|
replacementSocket.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class TestSocket : ISocket
|
||||||
|
{
|
||||||
|
public event DestroyedEvent? OnDestroy;
|
||||||
|
|
||||||
|
public SocketState State { get; private set; } = SocketState.Established;
|
||||||
|
public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
|
||||||
|
public IPEndPoint RemoteEndPoint { get; }
|
||||||
|
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 10518);
|
||||||
|
|
||||||
|
public TestSocket(IPAddress address, int port)
|
||||||
|
=> RemoteEndPoint = new IPEndPoint(address, port);
|
||||||
|
|
||||||
|
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||||
|
=> new(true);
|
||||||
|
|
||||||
|
public void Send(byte[] message) { }
|
||||||
|
public void Send(byte[] message, int offset, int length) { }
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
if (State == SocketState.Closed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
State = SocketState.Closed;
|
||||||
|
Receiver?.NetworkClose(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncReply<bool> Connect(string hostname, ushort port) => new(true);
|
||||||
|
public bool Begin() => true;
|
||||||
|
public AsyncReply<bool> BeginAsync() => new(true);
|
||||||
|
public AsyncReply<ISocket> AcceptAsync() => new((ISocket)null!);
|
||||||
|
public ISocket Accept() => null!;
|
||||||
|
public void Hold() { }
|
||||||
|
public void Unhold() { }
|
||||||
|
|
||||||
|
public void Destroy()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
OnDestroy?.Invoke(this);
|
||||||
|
OnDestroy = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user