mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-09-08 10:10:49 +00:00
Limits
This commit is contained in:
@@ -649,6 +649,7 @@ public static class Codec
|
|||||||
public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)
|
public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var tdu = ComposeInternal(valueOrSource, warehouse, connection);
|
var tdu = ComposeInternal(valueOrSource, warehouse, connection);
|
||||||
|
ParserGuard.EnsureRemotePacketSize(connection, (ulong)tdu.Composed.LongLength);
|
||||||
return tdu.Composed;
|
return tdu.Composed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,10 @@ public static class DataSerializer
|
|||||||
public static Tdu StringComposer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu StringComposer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var b = Encoding.UTF8.GetBytes((string)value);
|
var b = Encoding.UTF8.GetBytes((string)value);
|
||||||
|
ParserGuard.EnsureRemoteAllocation(
|
||||||
|
connection,
|
||||||
|
ParserGuard.MultiplySaturated((ulong)b.LongLength, 2),
|
||||||
|
"string");
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.String, b, (uint)b.Length, null, null);
|
return new Tdu(TduIdentifier.String, b, (uint)b.Length, null, null);
|
||||||
}
|
}
|
||||||
@@ -370,6 +374,10 @@ public static class DataSerializer
|
|||||||
public static Tdu ResourceLinkComposer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu ResourceLinkComposer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var b = Encoding.UTF8.GetBytes((ResourceLink)value);
|
var b = Encoding.UTF8.GetBytes((ResourceLink)value);
|
||||||
|
ParserGuard.EnsureRemoteAllocation(
|
||||||
|
connection,
|
||||||
|
ParserGuard.MultiplySaturated((ulong)b.LongLength, 2),
|
||||||
|
"resource link");
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.ResourceLink, b, (uint)b.Length, null, null);
|
return new Tdu(TduIdentifier.ResourceLink, b, (uint)b.Length, null, null);
|
||||||
}
|
}
|
||||||
@@ -446,12 +454,14 @@ public static class DataSerializer
|
|||||||
public static Tdu RawDataComposerFromArray(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu RawDataComposerFromArray(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var b = (byte[])value;
|
var b = (byte[])value;
|
||||||
|
ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.LongLength, "raw data");
|
||||||
return new Tdu(TduIdentifier.RawData, b, (uint)b.Length, null, null);
|
return new Tdu(TduIdentifier.RawData, b, (uint)b.Length, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Tdu RawDataComposerFromList(dynamic value, Warehouse warehouse, EpConnection connection)
|
public static Tdu RawDataComposerFromList(dynamic value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var b = value as List<byte>;
|
var b = value as List<byte>;
|
||||||
|
ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.Count, "raw data");
|
||||||
return new Tdu(TduIdentifier.RawData, b.ToArray(), (uint)b.Count, null, null);
|
return new Tdu(TduIdentifier.RawData, b.ToArray(), (uint)b.Count, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +526,9 @@ public static class DataSerializer
|
|||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (value is ICollection collection)
|
||||||
|
ParserGuard.EnsureRemoteCollectionCount(connection, collection.Count, GetTypedArrayElementSize(tru));
|
||||||
|
|
||||||
if (tru.Identifier == TruIdentifier.Int32)
|
if (tru.Identifier == TruIdentifier.Int32)
|
||||||
{
|
{
|
||||||
composed = GroupInt32Codec.Encode((IList<int>)value);
|
composed = GroupInt32Codec.Encode((IList<int>)value);
|
||||||
@@ -748,12 +761,18 @@ public static class DataSerializer
|
|||||||
|
|
||||||
// Pre-size the buffer from the element count (when known) to avoid repeated
|
// Pre-size the buffer from the element count (when known) to avoid repeated
|
||||||
// List<byte> reallocations as items are appended. 4 bytes/element is a rough hint.
|
// List<byte> reallocations as items are appended. 4 bytes/element is a rough hint.
|
||||||
var rt = new List<byte>(value is ICollection collection ? collection.Count * 4 : 16);
|
var knownCount = value is ICollection collection ? collection.Count : -1;
|
||||||
|
if (knownCount >= 0)
|
||||||
|
ParserGuard.EnsureRemoteCollectionCount(connection, knownCount, IntPtr.Size);
|
||||||
|
var rt = new List<byte>(knownCount >= 0 ? knownCount * 4 : 16);
|
||||||
|
|
||||||
Tdu? previous = null;
|
Tdu? previous = null;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
foreach (var i in value)
|
foreach (var i in value)
|
||||||
{
|
{
|
||||||
|
if (knownCount < 0)
|
||||||
|
ParserGuard.EnsureRemoteCollectionCount(connection, ++count, IntPtr.Size);
|
||||||
var tdu = Codec.ComposeInternal(i, warehouse, connection);
|
var tdu = Codec.ComposeInternal(i, warehouse, connection);
|
||||||
if (previous != null && tdu.MatchType(previous.Value))
|
if (previous != null && tdu.MatchType(previous.Value))
|
||||||
{
|
{
|
||||||
@@ -862,13 +881,24 @@ public static class DataSerializer
|
|||||||
|
|
||||||
var rt = new List<byte>();
|
var rt = new List<byte>();
|
||||||
var map = (IMap)value;
|
var map = (IMap)value;
|
||||||
|
var serialized = map.Serialize();
|
||||||
|
ParserGuard.EnsureRemoteCollectionCount(connection, serialized.Length, IntPtr.Size);
|
||||||
|
|
||||||
foreach (var el in map.Serialize())
|
foreach (var el in serialized)
|
||||||
rt.AddRange(Codec.Compose(el, warehouse, connection));
|
rt.AddRange(Codec.Compose(el, warehouse, connection));
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null);
|
return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int GetTypedArrayElementSize(Tru tru)
|
||||||
|
=> tru.Identifier switch
|
||||||
|
{
|
||||||
|
TruIdentifier.Int16 or TruIdentifier.UInt16 => 2,
|
||||||
|
TruIdentifier.Int32 or TruIdentifier.UInt32 => 4,
|
||||||
|
TruIdentifier.Int64 or TruIdentifier.UInt64 => 8,
|
||||||
|
_ => IntPtr.Size
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composes an indexed CLR structure using the compatible Map<byte, object> wire shape.
|
/// Composes an indexed CLR structure using the compatible Map<byte, object> wire shape.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ public sealed class ParserLimitException : Exception
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised before transmission when a composed value exceeds a budget advertised by the peer.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RemoteParserLimitException : Exception
|
||||||
|
{
|
||||||
|
public RemoteParserLimitException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static class ParserGuard
|
internal static class ParserGuard
|
||||||
{
|
{
|
||||||
internal static Warehouse? GetWarehouse(EpConnection? connection)
|
internal static Warehouse? GetWarehouse(EpConnection? connection)
|
||||||
@@ -70,4 +80,64 @@ internal static class ParserGuard
|
|||||||
|
|
||||||
internal static ulong MultiplySaturated(ulong value, ulong multiplier)
|
internal static ulong MultiplySaturated(ulong value, ulong multiplier)
|
||||||
=> value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier;
|
=> value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier;
|
||||||
|
|
||||||
|
internal static void EnsureRemotePacketSize(EpConnection? connection, ulong size)
|
||||||
|
{
|
||||||
|
var limit = connection?.Session?.RemoteMaximumPacketSize ?? 0;
|
||||||
|
if (limit > 0 && size > limit)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Composed packet payload of {size} bytes exceeds the peer's advertised {limit}-byte limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EnsureRemoteAllocation(
|
||||||
|
EpConnection? connection,
|
||||||
|
ulong size,
|
||||||
|
string kind)
|
||||||
|
{
|
||||||
|
var limit = connection?.Session?.RemoteMaximumAllocationSize ?? 0;
|
||||||
|
if (limit > 0 && size > limit)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Composed {kind} would allocate {size} bytes at the peer, exceeding its advertised {limit}-byte limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EnsureRemoteCollectionCount(
|
||||||
|
EpConnection? connection,
|
||||||
|
int count,
|
||||||
|
int estimatedBytesPerItem = 0)
|
||||||
|
{
|
||||||
|
var limit = connection?.Session?.RemoteMaximumCollectionItems ?? 0;
|
||||||
|
if (limit > 0 && count > limit)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Composed collection has {count} items, exceeding the peer's advertised {limit}-item limit.");
|
||||||
|
|
||||||
|
if (estimatedBytesPerItem > 0)
|
||||||
|
EnsureRemoteAllocation(
|
||||||
|
connection,
|
||||||
|
MultiplySaturated((ulong)count, (ulong)estimatedBytesPerItem),
|
||||||
|
"collection");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EnsureRemoteTypeMetadataDepth(EpConnection? connection, Tru? tru)
|
||||||
|
{
|
||||||
|
var limit = connection?.Session?.RemoteMaximumTypeMetadataDepth ?? 0;
|
||||||
|
if (limit <= 0 || tru == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var depth = GetTypeMetadataDepth(tru);
|
||||||
|
if (depth > limit)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Composed TRU type metadata depth of {depth} exceeds the peer's advertised limit of {limit}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetTypeMetadataDepth(Tru tru)
|
||||||
|
{
|
||||||
|
if (tru is not TruComposite composite || composite.SubTypes == null || composite.SubTypes.Length == 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var maximumChildDepth = 0;
|
||||||
|
foreach (var child in composite.SubTypes)
|
||||||
|
maximumChildDepth = Math.Max(maximumChildDepth, GetTypeMetadataDepth(child));
|
||||||
|
|
||||||
|
return 1 + maximumChildDepth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ public struct Tdu
|
|||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
throw new Exception("Metadata must be provided for types.");
|
throw new Exception("Metadata must be provided for types.");
|
||||||
|
|
||||||
|
ParserGuard.EnsureRemoteTypeMetadataDepth(connection, metadata);
|
||||||
var metadataData = metadata.Compose(connection);
|
var metadataData = metadata.Compose(connection);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace Esiur.Net.Packets
|
|||||||
AuthenticationProtocol,
|
AuthenticationProtocol,
|
||||||
AuthenticationData,
|
AuthenticationData,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
CipherNonce
|
CipherNonce,
|
||||||
|
MaximumPacketSize,
|
||||||
|
MaximumAllocationSize,
|
||||||
|
MaximumCollectionItems,
|
||||||
|
MaximumTypeMetadataDepth,
|
||||||
|
MaximumEncryptedRecordSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,17 +369,25 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
var provider = _session.EncryptionProvider
|
var provider = _session.EncryptionProvider
|
||||||
?? throw new InvalidOperationException("Session encryption is active without a provider.");
|
?? throw new InvalidOperationException("Session encryption is active without a provider.");
|
||||||
var maximumRecordSize = ParsingWarehouse.Configuration.Encryption.MaximumRecordSize;
|
var maximumRecordSize = ParsingWarehouse.Configuration.Encryption.MaximumRecordSize;
|
||||||
|
var remoteMaximumRecordSize = _session?.RemoteMaximumEncryptedRecordSize ?? 0;
|
||||||
|
|
||||||
if (maximumRecordSize > 0
|
if (maximumRecordSize > 0
|
||||||
&& (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > maximumRecordSize)
|
&& (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > maximumRecordSize)
|
||||||
throw new ParserLimitException(
|
throw new ParserLimitException(
|
||||||
$"Encrypted record would exceed the {maximumRecordSize}-byte limit.");
|
$"Encrypted record would exceed the {maximumRecordSize}-byte limit.");
|
||||||
|
if (remoteMaximumRecordSize > 0
|
||||||
|
&& (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > remoteMaximumRecordSize)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Encrypted record would exceed the peer's advertised {remoteMaximumRecordSize}-byte limit.");
|
||||||
|
|
||||||
var protectedPayload = cipher.Encrypt(plaintext);
|
var protectedPayload = cipher.Encrypt(plaintext);
|
||||||
|
|
||||||
if (maximumRecordSize > 0 && protectedPayload.Length > maximumRecordSize)
|
if (maximumRecordSize > 0 && protectedPayload.Length > maximumRecordSize)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Encryption provider `{provider.DefaultName}` exceeded its declared record overhead.");
|
$"Encryption provider `{provider.DefaultName}` exceeded its declared record overhead.");
|
||||||
|
if (remoteMaximumRecordSize > 0 && protectedPayload.Length > remoteMaximumRecordSize)
|
||||||
|
throw new RemoteParserLimitException(
|
||||||
|
$"Encrypted record of {protectedPayload.Length} bytes exceeds the peer's advertised {remoteMaximumRecordSize}-byte limit.");
|
||||||
if (protectedPayload.Length > int.MaxValue - EncryptedRecordHeaderSize)
|
if (protectedPayload.Length > int.MaxValue - EncryptedRecordHeaderSize)
|
||||||
throw new ParserLimitException("Encrypted record exceeds the runtime allocation limit.");
|
throw new ParserLimitException("Encrypted record exceeds the runtime allocation limit.");
|
||||||
|
|
||||||
@@ -454,6 +462,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopulateLocalLimitHeaders();
|
||||||
var headers = _session.LocalHeaders.Copy();
|
var headers = _session.LocalHeaders.Copy();
|
||||||
|
|
||||||
// Anonymous sessions still exchange typed records. They therefore
|
// Anonymous sessions still exchange typed records. They therefore
|
||||||
@@ -491,6 +500,16 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
| ((byte)_session.EncryptionMode & 0x3)), headers);
|
| ((byte)_session.EncryptionMode & 0x3)), headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PopulateLocalLimitHeaders()
|
||||||
|
{
|
||||||
|
var configuration = ParsingWarehouse.Configuration;
|
||||||
|
_session.LocalHeaders.MaximumPacketSize = configuration.Parser.MaximumPacketSize;
|
||||||
|
_session.LocalHeaders.MaximumAllocationSize = configuration.Parser.MaximumAllocationSize;
|
||||||
|
_session.LocalHeaders.MaximumCollectionItems = configuration.Parser.MaximumCollectionItems;
|
||||||
|
_session.LocalHeaders.MaximumTypeMetadataDepth = configuration.Parser.MaximumTypeMetadataDepth;
|
||||||
|
_session.LocalHeaders.MaximumEncryptedRecordSize = configuration.Encryption.MaximumRecordSize;
|
||||||
|
}
|
||||||
|
|
||||||
void PrepareEncryptionOffer()
|
void PrepareEncryptionOffer()
|
||||||
{
|
{
|
||||||
if (_session.AuthenticationMode == AuthenticationMode.None)
|
if (_session.AuthenticationMode == AuthenticationMode.None)
|
||||||
@@ -1653,6 +1672,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
? $"anonymous:{RemoteEndPoint?.Address}"
|
? $"anonymous:{RemoteEndPoint?.Address}"
|
||||||
: remoteHeaders.Domain;
|
: remoteHeaders.Domain;
|
||||||
_session.AuthenticationMode = _authPacket.AuthMode;
|
_session.AuthenticationMode = _authPacket.AuthMode;
|
||||||
|
PopulateLocalLimitHeaders();
|
||||||
var localHeaders = _session.LocalHeaders.Copy();
|
var localHeaders = _session.LocalHeaders.Copy();
|
||||||
|
|
||||||
if (!NegotiateEncryptionAsResponder(localHeaders))
|
if (!NegotiateEncryptionAsResponder(localHeaders))
|
||||||
@@ -1787,6 +1807,16 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
if (_session.AuthenticationMode == AuthenticationMode.None
|
if (_session.AuthenticationMode == AuthenticationMode.None
|
||||||
&& _authPacket.Method == EpAuthPacketMethod.SessionEstablished)
|
&& _authPacket.Method == EpAuthPacketMethod.SessionEstablished)
|
||||||
{
|
{
|
||||||
|
var remoteHeaders = new SessionHeaders();
|
||||||
|
if (_authPacket.Tdu != null)
|
||||||
|
{
|
||||||
|
remoteHeaders = Codec.ParseIndexedType<SessionHeaders>(
|
||||||
|
_authPacket.Tdu.Value,
|
||||||
|
ParsingWarehouse);
|
||||||
|
remoteHeaders.AuthenticationData = null;
|
||||||
|
}
|
||||||
|
_session.RemoteHeaders = remoteHeaders;
|
||||||
|
|
||||||
_session.Authenticated = true;
|
_session.Authenticated = true;
|
||||||
_session.LocalIdentity = null;
|
_session.LocalIdentity = null;
|
||||||
_session.RemoteIdentity = null;
|
_session.RemoteIdentity = null;
|
||||||
|
|||||||
@@ -329,7 +329,18 @@ partial class EpConnection
|
|||||||
//callbackCounter++; // avoid thread racing
|
//callbackCounter++; // avoid thread racing
|
||||||
_requests.Add(c, reply);
|
_requests.Add(c, reply);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
SendRequestPacket(action, c, args);
|
SendRequestPacket(action, c, args);
|
||||||
|
}
|
||||||
|
catch (RemoteParserLimitException ex)
|
||||||
|
{
|
||||||
|
_requests.Take(c);
|
||||||
|
reply.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
}
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +383,18 @@ partial class EpConnection
|
|||||||
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
||||||
|
|
||||||
_requests.Add(callbackId, reply);
|
_requests.Add(callbackId, reply);
|
||||||
|
try
|
||||||
|
{
|
||||||
SendRequestPacket(action, callbackId, args);
|
SendRequestPacket(action, callbackId, args);
|
||||||
|
}
|
||||||
|
catch (RemoteParserLimitException ex)
|
||||||
|
{
|
||||||
|
_requests.Take(callbackId);
|
||||||
|
reply.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
}
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +409,18 @@ partial class EpConnection
|
|||||||
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
||||||
|
|
||||||
_requests.Add(callbackId, reply);
|
_requests.Add(callbackId, reply);
|
||||||
|
try
|
||||||
|
{
|
||||||
SendRequestPacket(action, callbackId, args);
|
SendRequestPacket(action, callbackId, args);
|
||||||
|
}
|
||||||
|
catch (RemoteParserLimitException ex)
|
||||||
|
{
|
||||||
|
_requests.Take(callbackId);
|
||||||
|
reply.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
}
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +535,8 @@ partial class EpConnection
|
|||||||
if (Instance == null)
|
if (Instance == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
@@ -509,7 +544,7 @@ partial class EpConnection
|
|||||||
.AddUInt32(callbackId);
|
.AddUInt32(callbackId);
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
if (args.Length == 1)
|
else if (args.Length == 1)
|
||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
||||||
@@ -526,6 +561,18 @@ partial class EpConnection
|
|||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (RemoteParserLimitException ex) when (
|
||||||
|
action != EpPacketReply.PermissionError
|
||||||
|
&& action != EpPacketReply.ExecutionError
|
||||||
|
&& action != EpPacketReply.Warning)
|
||||||
|
{
|
||||||
|
SendError(
|
||||||
|
ErrorType.Exception,
|
||||||
|
callbackId,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
internal AsyncReply SendSubscribeRequest(uint instanceId, byte index)
|
internal AsyncReply SendSubscribeRequest(uint instanceId, byte index)
|
||||||
@@ -677,7 +724,19 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pr = Codec.Parse(tdu.Value, this, null);
|
object pr;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pr = Codec.Parse(tdu.Value, this, null);
|
||||||
|
}
|
||||||
|
catch (ParserLimitException ex)
|
||||||
|
{
|
||||||
|
req.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pr is AsyncReply asyncReply)
|
if (pr is AsyncReply asyncReply)
|
||||||
{
|
{
|
||||||
@@ -724,7 +783,20 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = Codec.Parse(tdu, this, null);
|
object value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = Codec.Parse(tdu, this, null);
|
||||||
|
}
|
||||||
|
catch (ParserLimitException ex)
|
||||||
|
{
|
||||||
|
_requests.Take(callbackId);
|
||||||
|
req.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value is AsyncReply reply)
|
if (value is AsyncReply reply)
|
||||||
{
|
{
|
||||||
@@ -819,7 +891,20 @@ partial class EpConnection
|
|||||||
if (req == null)
|
if (req == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var value = Codec.Parse(tdu, this, null);
|
object value;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = Codec.Parse(tdu, this, null);
|
||||||
|
}
|
||||||
|
catch (ParserLimitException ex)
|
||||||
|
{
|
||||||
|
_requests.Take(callbackId);
|
||||||
|
req.TriggerError(new AsyncException(
|
||||||
|
ErrorType.Management,
|
||||||
|
(ushort)ExceptionCode.ParserLimitExceeded,
|
||||||
|
ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value is AsyncReply asyncReply)
|
if (value is AsyncReply asyncReply)
|
||||||
{
|
{
|
||||||
@@ -3993,6 +4078,14 @@ partial class EpConnection
|
|||||||
|
|
||||||
private void Instance_PropertyModified(PropertyModificationInfo info)
|
private void Instance_PropertyModified(PropertyModificationInfo info)
|
||||||
{
|
{
|
||||||
|
// Attachment permission does not imply property-read permission. A
|
||||||
|
// method-only resource can be attached so its exported functions are
|
||||||
|
// callable while all of its properties remain private. Re-evaluate the
|
||||||
|
// property operation before broadcasting each modification, matching
|
||||||
|
// the permission checks already applied to event notifications.
|
||||||
|
if (!IsOperationAllowed(info.Resource, info.PropertyDef, ActionType.GetProperty))
|
||||||
|
return;
|
||||||
|
|
||||||
SendNotification(EpPacketNotification.PropertyModified,
|
SendNotification(EpPacketNotification.PropertyModified,
|
||||||
info.Resource.Instance.Id,
|
info.Resource.Instance.Id,
|
||||||
info.Cursor.Generation.ToByteArray(),
|
info.Cursor.Generation.ToByteArray(),
|
||||||
|
|||||||
@@ -100,6 +100,21 @@ public sealed class SessionHeaders : IndexedStructure
|
|||||||
[Index((int)EpAuthPacketHeader.CipherNonce)]
|
[Index((int)EpAuthPacketHeader.CipherNonce)]
|
||||||
public byte[]? CipherNonce { get; set; }
|
public byte[]? CipherNonce { get; set; }
|
||||||
|
|
||||||
|
[Index((int)EpAuthPacketHeader.MaximumPacketSize)]
|
||||||
|
public uint? MaximumPacketSize { get; set; }
|
||||||
|
|
||||||
|
[Index((int)EpAuthPacketHeader.MaximumAllocationSize)]
|
||||||
|
public uint? MaximumAllocationSize { get; set; }
|
||||||
|
|
||||||
|
[Index((int)EpAuthPacketHeader.MaximumCollectionItems)]
|
||||||
|
public int? MaximumCollectionItems { get; set; }
|
||||||
|
|
||||||
|
[Index((int)EpAuthPacketHeader.MaximumTypeMetadataDepth)]
|
||||||
|
public int? MaximumTypeMetadataDepth { get; set; }
|
||||||
|
|
||||||
|
[Index((int)EpAuthPacketHeader.MaximumEncryptedRecordSize)]
|
||||||
|
public uint? MaximumEncryptedRecordSize { get; set; }
|
||||||
|
|
||||||
internal SessionHeaders Copy() => (SessionHeaders)MemberwiseClone();
|
internal SessionHeaders Copy() => (SessionHeaders)MemberwiseClone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +136,12 @@ public class Session
|
|||||||
public SessionHeaders LocalHeaders { get; set; } = new SessionHeaders();
|
public SessionHeaders LocalHeaders { get; set; } = new SessionHeaders();
|
||||||
public SessionHeaders RemoteHeaders { get; set; } = new SessionHeaders();
|
public SessionHeaders RemoteHeaders { get; set; } = new SessionHeaders();
|
||||||
|
|
||||||
|
public uint RemoteMaximumPacketSize => RemoteHeaders?.MaximumPacketSize ?? 0;
|
||||||
|
public uint RemoteMaximumAllocationSize => RemoteHeaders?.MaximumAllocationSize ?? 0;
|
||||||
|
public int RemoteMaximumCollectionItems => RemoteHeaders?.MaximumCollectionItems ?? 0;
|
||||||
|
public int RemoteMaximumTypeMetadataDepth => RemoteHeaders?.MaximumTypeMetadataDepth ?? 0;
|
||||||
|
public uint RemoteMaximumEncryptedRecordSize => RemoteHeaders?.MaximumEncryptedRecordSize ?? 0;
|
||||||
|
|
||||||
//public AuthenticationMethod AuthenticationMethod { get; set; }
|
//public AuthenticationMethod AuthenticationMethod { get; set; }
|
||||||
//public AuthenticationMethod RemoteMethod { get; set; }
|
//public AuthenticationMethod RemoteMethod { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Data.Types;
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Permissions;
|
||||||
|
|
||||||
namespace Esiur.Tests.Unit.Integration;
|
namespace Esiur.Tests.Unit.Integration;
|
||||||
|
|
||||||
@@ -36,10 +41,68 @@ public class AttachmentSecurityTests
|
|||||||
Assert.Equal(1, cluster.Connection.ResourceAttachRequestCount);
|
Assert.Equal(1, cluster.Connection.ResourceAttachRequestCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PropertyNotifications_RespectPerPropertyReadPermission()
|
||||||
|
{
|
||||||
|
PropertyBroadcastResource? source = null;
|
||||||
|
await using var cluster = await IntegrationCluster.StartAsync(async warehouse =>
|
||||||
|
{
|
||||||
|
var permissions = new PropertyBroadcastPermissions();
|
||||||
|
warehouse.RegisterManager(permissions);
|
||||||
|
source = await warehouse.Put("sys/property-broadcast", new PropertyBroadcastResource());
|
||||||
|
source.Instance!.Managers.Add(permissions);
|
||||||
|
}).WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
var remote = (EpResource)await cluster.Connection.Get("sys/property-broadcast");
|
||||||
|
var received = new List<string>();
|
||||||
|
remote.Instance.PropertyModified += info => received.Add(info.PropertyDef.Name);
|
||||||
|
|
||||||
|
source!.Secret = 7;
|
||||||
|
source.Visible = 9;
|
||||||
|
|
||||||
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3);
|
||||||
|
while (!received.Contains(nameof(PropertyBroadcastResource.Visible)) &&
|
||||||
|
DateTime.UtcNow < deadline)
|
||||||
|
await Task.Delay(20);
|
||||||
|
|
||||||
|
Assert.Contains(nameof(PropertyBroadcastResource.Visible), received);
|
||||||
|
Assert.DoesNotContain(nameof(PropertyBroadcastResource.Secret), received);
|
||||||
|
}
|
||||||
|
|
||||||
static Task<IntegrationCluster> StartCluster()
|
static Task<IntegrationCluster> StartCluster()
|
||||||
=> IntegrationCluster.StartAsync(async warehouse =>
|
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||||
{
|
{
|
||||||
await warehouse.Put("sys/first", new RateLimitedResource());
|
await warehouse.Put("sys/first", new RateLimitedResource());
|
||||||
await warehouse.Put("sys/second", new RateLimitedResource());
|
await warehouse.Put("sys/second", new RateLimitedResource());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sealed class PropertyBroadcastPermissions : IPermissionsManager
|
||||||
|
{
|
||||||
|
public Map<string, object> Settings { get; } = new();
|
||||||
|
|
||||||
|
public Ruling Applicable(
|
||||||
|
IResource resource,
|
||||||
|
Session session,
|
||||||
|
ActionType action,
|
||||||
|
MemberDef member,
|
||||||
|
object inquirer = null!)
|
||||||
|
{
|
||||||
|
if (action == ActionType.Attach)
|
||||||
|
return Ruling.Allowed;
|
||||||
|
if (action == ActionType.GetProperty)
|
||||||
|
return member?.Name == nameof(PropertyBroadcastResource.Secret)
|
||||||
|
? Ruling.Denied
|
||||||
|
: Ruling.Allowed;
|
||||||
|
return Ruling.DontCare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resource]
|
||||||
|
public partial class PropertyBroadcastResource
|
||||||
|
{
|
||||||
|
[Export] int secret;
|
||||||
|
[Export] int visible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,46 @@ using System.Net;
|
|||||||
[Collection("Integration")]
|
[Collection("Integration")]
|
||||||
public class SessionHeadersIntegrationTests
|
public class SessionHeadersIntegrationTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Handshake_ExchangesParserAndEncryptedRecordBudgetsInBothDirections()
|
||||||
|
{
|
||||||
|
await using var cluster = await IntegrationCluster
|
||||||
|
.StartAsync(
|
||||||
|
warehouse =>
|
||||||
|
{
|
||||||
|
warehouse.Configuration.Parser.MaximumPacketSize = 7_100_001;
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 3_100_002;
|
||||||
|
warehouse.Configuration.Parser.MaximumCollectionItems = 51_003;
|
||||||
|
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 54;
|
||||||
|
warehouse.Configuration.Encryption.MaximumRecordSize = 7_101_004;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
populateClient: warehouse =>
|
||||||
|
{
|
||||||
|
warehouse.Configuration.Parser.MaximumPacketSize = 6_200_001;
|
||||||
|
warehouse.Configuration.Parser.MaximumAllocationSize = 2_200_002;
|
||||||
|
warehouse.Configuration.Parser.MaximumCollectionItems = 42_003;
|
||||||
|
warehouse.Configuration.Parser.MaximumTypeMetadataDepth = 45;
|
||||||
|
warehouse.Configuration.Encryption.MaximumRecordSize = 6_201_004;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
})
|
||||||
|
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
var serverConnection = Assert.Single(cluster.Server.Connections);
|
||||||
|
|
||||||
|
Assert.Equal(7_100_001u, cluster.Connection.Session.RemoteMaximumPacketSize);
|
||||||
|
Assert.Equal(3_100_002u, cluster.Connection.Session.RemoteMaximumAllocationSize);
|
||||||
|
Assert.Equal(51_003, cluster.Connection.Session.RemoteMaximumCollectionItems);
|
||||||
|
Assert.Equal(54, cluster.Connection.Session.RemoteMaximumTypeMetadataDepth);
|
||||||
|
Assert.Equal(7_101_004u, cluster.Connection.Session.RemoteMaximumEncryptedRecordSize);
|
||||||
|
|
||||||
|
Assert.Equal(6_200_001u, serverConnection.Session.RemoteMaximumPacketSize);
|
||||||
|
Assert.Equal(2_200_002u, serverConnection.Session.RemoteMaximumAllocationSize);
|
||||||
|
Assert.Equal(42_003, serverConnection.Session.RemoteMaximumCollectionItems);
|
||||||
|
Assert.Equal(45, serverConnection.Session.RemoteMaximumTypeMetadataDepth);
|
||||||
|
Assert.Equal(6_201_004u, serverConnection.Session.RemoteMaximumEncryptedRecordSize);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AcceptedConnection_RaisesReadyAndDisconnectedLifecycleEvents()
|
public async Task AcceptedConnection_RaisesReadyAndDisconnectedLifecycleEvents()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ namespace Esiur.Tests.Unit;
|
|||||||
|
|
||||||
public class ParserSecurityTests
|
public class ParserSecurityTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Composer_RejectsValuesAbovePeerAdvertisedBudgetsBeforeSend()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var connection = new EpConnection();
|
||||||
|
connection.Session.RemoteHeaders.MaximumPacketSize = 64;
|
||||||
|
connection.Session.RemoteHeaders.MaximumAllocationSize = 6;
|
||||||
|
connection.Session.RemoteHeaders.MaximumCollectionItems = 2;
|
||||||
|
|
||||||
|
Assert.Throws<RemoteParserLimitException>(() =>
|
||||||
|
Codec.Compose("four", warehouse, connection));
|
||||||
|
Assert.Throws<RemoteParserLimitException>(() =>
|
||||||
|
Codec.Compose(new object[] { 1, 2, 3 }, warehouse, connection));
|
||||||
|
|
||||||
|
connection.Session.RemoteHeaders.MaximumAllocationSize = 0;
|
||||||
|
connection.Session.RemoteHeaders.MaximumCollectionItems = 0;
|
||||||
|
connection.Session.RemoteHeaders.MaximumPacketSize = 3;
|
||||||
|
Assert.Throws<RemoteParserLimitException>(() =>
|
||||||
|
Codec.Compose(new byte[] { 1, 2, 3, 4 }, warehouse, connection));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PacketParser_RejectsOversizedDeclarationBeforePayloadArrives()
|
public void PacketParser_RejectsOversizedDeclarationBeforePayloadArrives()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user