RateControl

This commit is contained in:
2026-07-14 02:27:33 +03:00
parent 4f6d2d0801
commit 1ca9fc477b
19 changed files with 879 additions and 146 deletions
+5 -1
View File
@@ -47,5 +47,9 @@ public enum ExceptionCode : ushort
NotSupported,
NotImplemented,
NotAllowed,
RateLimitExceeded
RateLimitExceeded,
ParserLimitExceeded,
AttachmentLimitExceeded,
AlreadyAttached,
ConnectionLimitExceeded
}
+9 -4
View File
@@ -89,8 +89,8 @@ public class AutoList<T, ST> : IEnumerable<T>, ICollection, ICollection<T>
/// <returns>Array</returns>
public T[] ToArray()
{
// list.OrderBy()
return list.ToArray();
lock (syncRoot)
return list.ToArray();
}
/// <summary>
@@ -242,7 +242,11 @@ public class AutoList<T, ST> : IEnumerable<T>, ICollection, ICollection<T>
/// </summary>
public int Count
{
get { return list.Count; }
get
{
lock (syncRoot)
return list.Count;
}
}
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>
public bool Contains(T value)
{
return list.Contains(value);
lock (syncRoot)
return list.Contains(value);
}
/// <summary>
+108 -24
View File
@@ -23,6 +23,18 @@ public static class DataDeserializer
{
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(
ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
{
@@ -324,6 +336,10 @@ public static class DataDeserializer
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);
if (connection == null)
{
@@ -337,6 +353,10 @@ public static class DataDeserializer
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);
return new ResourceLink(link);
}
@@ -432,27 +452,41 @@ public static class DataDeserializer
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);
}
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);
}
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);
}
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);
}
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)
// throw new Exception("TypeDefId metadata is required for record parsing.");
@@ -618,6 +652,8 @@ public static class DataDeserializer
"TypeDef not found for record.");
}
ParserGuard.EnsureCollectionCount(warehouse, recordTypeDef.Properties.Length, IntPtr.Size);
var list = new List<object>();
ParsedTdu current;
@@ -789,6 +825,7 @@ public static class DataDeserializer
public static async AsyncReply<object> RecordListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
{
var rt = new AsyncBag<IRecord>();
var count = 0;
var length = tdu.PayloadLength;
var offset = tdu.PayloadOffset;
@@ -798,6 +835,7 @@ public static class DataDeserializer
//var (cs, reply)
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
rt.Add(pr.Value);
if (pr.Size > 0)
@@ -827,6 +865,7 @@ public static class DataDeserializer
{
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
rt.Add(reply as IRecord);
if (cs > 0)
@@ -845,6 +884,7 @@ public static class DataDeserializer
public static async AsyncReply<object> ResourceListParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
{
var rt = new AsyncBag<IResource>();
var count = 0;
var length = tdu.PayloadLength;
var offset = tdu.PayloadOffset;
@@ -854,6 +894,7 @@ public static class DataDeserializer
//var (cs, reply)
var pr = await Codec.ParseAsync(tdu.Data, offset, connection, requestSequence);
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
rt.Add(pr.Value);// reply);
if (pr.Size > 0)
@@ -884,6 +925,7 @@ public static class DataDeserializer
{
var (cs, reply) = Codec.ParseSync(tdu.Data, offset, warehouse);
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
rt.Add(reply as IResource);
if (cs > 0)
@@ -931,6 +973,7 @@ public static class DataDeserializer
var rt = new AsyncBag<object>();
var count = 0;
//var list = new List<object>();
@@ -962,6 +1005,7 @@ public static class DataDeserializer
//var (cs, reply)
var value = Codec.ParseAsync(current, connection, requestSequence);
ParserGuard.EnsureCollectionCount(connection?.ParsingWarehouse, ++count, IntPtr.Size);
rt.Add(value);
if (current.TotalLength > 0)
@@ -1012,6 +1056,7 @@ public static class DataDeserializer
var reply = Codec.ParseSync(current, warehouse);
ParserGuard.EnsureCollectionCount(warehouse, list.Count + 1, IntPtr.Size);
list.Add(reply);
if (current.TotalLength > 0)
@@ -1040,6 +1085,7 @@ public static class DataDeserializer
{
var (cs, reply) = Codec.ParseSync(data, offset, warehouse);
ParserGuard.EnsureCollectionCount(warehouse, rt.Count + 1, IntPtr.Size);
rt.Add(reply);
if (cs > 0)
@@ -1151,26 +1197,37 @@ public static class DataDeserializer
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)
{
case TruIdentifier.Int32:
return GroupInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4);
case TruIdentifier.Int64:
return GroupInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8);
case TruIdentifier.Int16:
return GroupInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2);
case TruIdentifier.UInt32:
return GroupUInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupUInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4);
case TruIdentifier.UInt64:
return GroupUInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupUInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8);
case TruIdentifier.UInt16:
return GroupUInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength));
return GuardDecodedArray(warehouse, GroupUInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2);
//case TruIdentifier.Enum:
// var enumType = tru.GetRuntimeType(warehouse);
@@ -1224,6 +1281,7 @@ public static class DataDeserializer
var value = Codec.ParseSync(current, warehouse);
ParserGuard.EnsureCollectionCount(warehouse, list.Count + 1, IntPtr.Size);
list.Add(value);
if (current.TotalLength > 0)
@@ -1249,26 +1307,38 @@ public static class DataDeserializer
// Non-async/await version of TypedArrayParserAsync using continuations
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)
{
case TruIdentifier.Int32:
return new AsyncReply(GroupInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4));
case TruIdentifier.Int64:
return new AsyncReply(GroupInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8));
case TruIdentifier.Int16:
return new AsyncReply(GroupInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2));
case TruIdentifier.UInt32:
return new AsyncReply(GroupUInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt32Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 4));
case TruIdentifier.UInt64:
return new AsyncReply(GroupUInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt64Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 8));
case TruIdentifier.UInt16:
return new AsyncReply(GroupUInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)));
return new AsyncReply(GuardDecodedArray(warehouse, GroupUInt16Codec.Decode(tdu.Data.AsSpan(
(int)tdu.PayloadOffset, (int)tdu.PayloadLength)), 2));
default:
var rt = new AsyncReply();
@@ -1285,6 +1355,7 @@ public static class DataDeserializer
// Recursive processor using continuations
Action<uint, uint, ParsedTdu?> processNext = null;
var itemCount = 0;
processNext = (curOffset, curLength, previous) =>
{
@@ -1333,6 +1404,19 @@ public static class DataDeserializer
var reply = Codec.ParseAsync(current, connection, requestSequence);
try
{
ParserGuard.EnsureCollectionCount(
warehouse,
++itemCount,
IntPtr.Size);
}
catch (Exception ex)
{
rt.TriggerError(ex);
return;
}
list.Add(reply);
if (current.TotalLength > 0)
+44 -31
View File
@@ -62,14 +62,16 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
public T Take(KT key)
{
if (dic.ContainsKey(key))
lock (syncRoot)
{
var v = dic[key];
Remove(key);
return v;
}
else
if (dic.TryGetValue(key, out var value))
{
Remove(key);
return value;
}
return default(T);
}
}
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()
{
var a = new T[Count];
dic.Values.CopyTo(a, 0);
return a;
lock (syncRoot)
{
var a = new T[dic.Count];
dic.Values.CopyTo(a, 0);
return a;
}
}
public void Add(KT key, T value)
@@ -132,10 +137,8 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
{
get
{
if (dic.ContainsKey(key))
return dic[key];
else
return default(T);
lock (syncRoot)
return dic.TryGetValue(key, out var value) ? value : default(T);
}
set
{
@@ -157,13 +160,15 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
public void Clear()
{
if (removableList)
foreach (IDestructible v in dic.Values)
if (v != null)
v.OnDestroy -= ItemDestroyed;
lock (syncRoot)
{
if (removableList)
foreach (IDestructible v in dic.Values)
if (v != null)
v.OnDestroy -= ItemDestroyed;
dic.Clear();
}
if (OnCleared != null)
OnCleared(this);
@@ -184,17 +189,18 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
public void Remove(KT key)
{
if (!dic.ContainsKey(key))
return;
var value = dic[key];
if (removableList)
if (value != null)
((IDestructible)value).OnDestroy -= ItemDestroyed;
T value;
lock (syncRoot)
{
if (!dic.TryGetValue(key, out value))
return;
if (removableList)
if (value != null)
((IDestructible)value).OnDestroy -= ItemDestroyed;
dic.Remove(key);
}
if (OnRemoved != null)
OnRemoved(key, value, this);
@@ -208,19 +214,26 @@ public class KeyList<KT, T> : IEnumerable<KeyValuePair<KT, T>>
public int Count
{
get { return dic.Count; }
get
{
lock (syncRoot)
return dic.Count;
}
}
public bool Contains(KT Key)
{
return dic.ContainsKey(Key);
lock (syncRoot)
return dic.ContainsKey(Key);
}
public bool ContainsKey(KT Key)
{
return dic.ContainsKey(Key);
lock (syncRoot)
return dic.ContainsKey(Key);
}
public bool ContainsValue(T Value)
{
return dic.ContainsValue(Value);
lock (syncRoot)
return dic.ContainsValue(Value);
}
+27 -6
View File
@@ -27,8 +27,6 @@ namespace Esiur.Data
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 cls = (TduClass)(h >> 6);
@@ -91,6 +89,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
if (ends - offset < cl)
return new ParsedTdu()
{
@@ -102,6 +102,10 @@ namespace Esiur.Data
//offset += data[offset] + (uint)1;
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;
return new ParsedTdu()
@@ -133,6 +137,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
if (ends - offset < cl)
return new ParsedTdu()
{
@@ -158,8 +164,6 @@ namespace Esiur.Data
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 cls = (TduClass)(h >> 6);
@@ -222,6 +226,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
if (ends - offset < cl)
return new ParsedTdu()
{
@@ -235,6 +241,13 @@ namespace Esiur.Data
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;
rt.Trigger(new ParsedTdu()
@@ -269,6 +282,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(ParserGuard.GetWarehouse(connection), cl);
if (ends - offset < cl)
return new ParsedTdu()
{
@@ -347,8 +362,6 @@ namespace Esiur.Data
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 cls = (TduClass)(h >> 6);
@@ -411,6 +424,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(warehouse, cl);
if (ends - offset < cl)
return new ParsedTdu()
{
@@ -422,6 +437,10 @@ namespace Esiur.Data
//offset += data[offset] + (uint)1;
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;
return new ParsedTdu()
@@ -453,6 +472,8 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
cl = cl << 8 | data[offset++];
ParserGuard.EnsurePacketSize(warehouse, cl);
if (ends - offset < cl)
return new ParsedTdu()
{
+61
View File
@@ -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;
}
+13 -1
View File
@@ -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;
@@ -91,6 +95,10 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
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)
return new PlainTdu()
{
@@ -128,6 +136,10 @@ namespace Esiur.Data
for (uint i = 0; i < cll; i++)
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)
return new PlainTdu()
{
+5
View File
@@ -112,6 +112,11 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
c.Assign(s);
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
{
ClientConnected(c);
+6 -1
View File
@@ -185,7 +185,12 @@ public class EpAuthPacket : Packet
if (NotEnough(offset, ends, 1))
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)
return -(int)Tdu.Value.TotalLength;
+6 -1
View File
@@ -135,7 +135,12 @@ class EpPacket : Packet
if (NotEnough(offset, ends, 1))
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)
return -(int)Tdu.Value.TotalLength;
+6
View File
@@ -121,6 +121,7 @@ public partial class EpConnection : NetworkConnection, IStore
Warehouse _serverWarehouse;
public EpServer Server => _server;
internal Warehouse ParsingWarehouse => Instance?.Warehouse ?? _serverWarehouse ?? Warehouse.Default;
//public EpServer Server
//{
// get => _server;
@@ -1857,6 +1858,11 @@ public partial class EpConnection : NetworkConnection, IStore
offset = processPacket(msg, offset, ends, data, chunkId);
}
}
catch (ParserLimitException ex)
{
Global.Log("EpConnection:ParserLimit", LogType.Warning, ex.Message);
Close();
}
catch (Exception ex)
{
Global.Log(ex);
+242 -69
View File
@@ -214,6 +214,8 @@ partial class EpConnection
// Global.Counters suffer from). Used by the deadlock experiments.
/// <summary>Number of resources fully attached on this connection (a monotonic progress signal).</summary>
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>
public long CycleBreakCount { get; private set; }
/// <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;
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
readonly HashSet<uint> _peerAttachmentRequests = new HashSet<uint>();
// resources might get attached by the client
internal KeyList<IResource, DateTime> _cache = new();
@@ -928,38 +931,55 @@ partial class EpConnection
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) =>
{
if (res != null)
try
{
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
if (res != null)
{
SendError(ErrorType.Management, callback, 6);
return;
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
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
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
EndPeerAttachment(resourceId);
}
}).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]);
if (!TryBeginPeerAttachment(resourceId, out var exceptionCode, out var exceptionMessage))
{
SendError(ErrorType.Management, callback, (ushort)exceptionCode, exceptionMessage);
return;
}
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);
return;
if (res.Instance.Applicable(_session, ActionType.Attach, null) == Ruling.Denied)
{
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
Global.Log("EpConnection", LogType.Debug, "Not found " + resourceId);
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ResourceNotFound);
EndPeerAttachment(resourceId);
}
}).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)
{
//lock (fetchLock)
@@ -2746,19 +2865,26 @@ partial class EpConnection
var newSequence = requestSequence != null ? requestSequence.Concat(new uint[] { id }).ToArray() : new uint[] { id };
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.
if (parent != null)
AddResourceFetchBlock(parent.Value, id);
ResourceAttachRequestCount++;
SendRequest(EpPacketRequest.AttachResource, id)
.Then((result) =>
{
if (result == null)
{
reply.TriggerError(new AsyncException(ErrorType.Management,
(ushort)ExceptionCode.ResourceNotFound, "Null response"));
FailResourceAttachment(
id,
reply,
new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ResourceNotFound,
"Null response"));
return;
}
@@ -2801,11 +2927,7 @@ partial class EpConnection
ClearResourceFetchNode(id);
TryPublishDeliveredRoots();
reply.Trigger(dr);
}).Error(ex => {
_resourceRequests.Remove(id);
ClearResourceFetchNode(id);
reply.TriggerError(ex);
});
}).Error(ex => FailResourceAttachment(id, reply, ex));
};
if (typeDef == null)
@@ -2827,17 +2949,14 @@ partial class EpConnection
_neededResources[id] = resource;
Instance.Warehouse.Put(Instance.Link + "/" + id.ToString(), resource)
.Then(initResource)
.Error(ex => reply.TriggerError(ex));
.Error(ex => FailResourceAttachment(id, reply, ex));
}
else
{
initResource(resource);
}
}).Error((ex) =>
{
reply.TriggerError(ex);
});
}).Error(ex => FailResourceAttachment(id, reply, ex));
}
else
{
@@ -2854,7 +2973,8 @@ partial class EpConnection
// references in the graph can resolve back to this instance.
_neededResources[id] = 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
{
@@ -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
// later retry is not blocked by a stale entry.
_resourceRequests.Remove(id);
ClearResourceFetchNode(id);
reply.TriggerError(ex);
FailResourceAttachment(id, reply, ex);
});
@@ -2903,8 +3021,10 @@ partial class EpConnection
var reply = new AsyncReply<EpResource>();
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 =>
{
if (result == null)
@@ -3106,10 +3226,62 @@ partial class EpConnection
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)
{
lock (_subscriptionsLock)
{
if (_subscriptions.ContainsKey(resource))
return;
resource.Instance.EventOccurred += Instance_EventOccurred;
resource.Instance.CustomEventOccurred += Instance_CustomEventOccurred;
resource.Instance.PropertyModified += Instance_PropertyModified;
@@ -3147,6 +3319,7 @@ partial class EpConnection
}
_subscriptions.Clear();
_peerAttachmentRequests.Clear();
}
}
+91 -8
View File
@@ -44,6 +44,9 @@ namespace Esiur.Protocol;
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]
@@ -167,19 +170,99 @@ public class EpServer : NetworkServer<EpConnection>, IResource
public override void Add(EpConnection connection)
{
connection.Handle(ResourceOperation.Configure,
new EpServerConnectionContext() {
Server = this,
Warehouse = Instance.Warehouse }
);
if (!TryAdmitConnection(connection))
{
Global.Log(
"EpServer:ConnectionLimit",
LogType.Warning,
$"Rejected connection from {connection.RemoteEndPoint?.Address}: per-IP limit reached.");
connection.Close();
return;
}
connection.ExceptionLevel = ExceptionLevel;
base.Add(connection);
try
{
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)
{
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)
@@ -8,6 +8,45 @@ namespace Esiur.Resource;
public sealed class WarehouseConfiguration
{
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>
+8
View File
@@ -78,6 +78,12 @@ internal static class Program
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
{
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.AddRatePolicy(new BurstRatePolicy("standard-call")
{
@@ -139,6 +145,8 @@ internal static class Program
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
{
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
{
+15
View File
@@ -13,6 +13,7 @@ Coverage includes:
- pull streams backed by `IAsyncEnumerable<T>`;
- `TerminateExecution` through async-enumerator disposal;
- `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:
@@ -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());
});
}
+55
View File
@@ -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));
}
}
+94
View File
@@ -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;
}
}
}