Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/protocol-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Transport(TCP 使用 TLS 时先完成 TLS)建立后,Client 首先发送 `H
- bit 3: protocol health check
- bit 4: cancellation reason

当前 wire generation 只接受 `minor 5`(`MinimumCompatibleMinorVersion == MinorVersion == 5`);minor 因此只在握手/发布边界承担 grammar compatibility 校验,不参与握手后的 feature gating。协议边界“能够识别的 capability bit”与端点“实际实现并主动 advertise 的 capability”是两份独立事实:扩展 codec/rules 使其认识新 bit,不会自动让 Client/Server 宣告支持。握手成功后,普通 runtime feature code 只读取冻结的 negotiated capability set;需要具体 wire identity 的能力(例如 compression)再附带 capability-scoped profile/binding。

minor 4 的 `HandshakeRequest` 在三个固定限制字段后编码:

```text
Expand Down
2 changes: 1 addition & 1 deletion src/SharpLink.Runtime/NegotiatedSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal enum RpcSessionProtocolPhase : byte

internal static class RpcSessionProtocolRules
{
internal const ProtocolV2Capabilities KnownCapabilities =
internal const ProtocolV2Capabilities RecognizedCapabilities =
ProtocolV2Capabilities.Metadata |
ProtocolV2Capabilities.Compression |
ProtocolV2Capabilities.FlowControl |
Expand Down
11 changes: 7 additions & 4 deletions src/SharpLink.Runtime/ProtocolV2/ProtocolV2Negotiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal static ProtocolV2NegotiationPolicy Create(
{
throw new ArgumentOutOfRangeException(nameof(minorVersion));
}
if ((supportedCapabilities & ~RpcSessionProtocolRules.KnownCapabilities) != 0)
if ((supportedCapabilities & ~RpcSessionProtocolRules.RecognizedCapabilities) != 0)
throw new ArgumentOutOfRangeException(nameof(supportedCapabilities));
if (maxFramePayloadBytes < SharpLinkProtocolOptions.MinMaxFramePayloadBytes ||
maxFramePayloadBytes > SharpLinkProtocolOptions.MaxMaxFramePayloadBytes)
Expand Down Expand Up @@ -125,8 +125,11 @@ internal ProtocolV2ServerNegotiation(
/// <summary>Pure Protocol v2 offer, intersection, and response-validation rules.</summary>
internal static class ProtocolV2Negotiator
{
private const ProtocolV2Capabilities AlwaysImplementedCapabilities =
RpcSessionProtocolRules.KnownCapabilities & ~ProtocolV2Capabilities.Compression;
internal const ProtocolV2Capabilities AlwaysImplementedCapabilities =
ProtocolV2Capabilities.Metadata |
ProtocolV2Capabilities.FlowControl |
ProtocolV2Capabilities.HealthCheck |
ProtocolV2Capabilities.CancellationReason;

internal static ProtocolV2NegotiationPolicy CreateImplementedPolicy(
int maxFramePayloadBytes,
Expand Down Expand Up @@ -265,7 +268,7 @@ internal static NegotiatedSessionOptions ValidateServerResponse(
SharpLinkErrorCode.Unimplemented,
$"Server requires unsupported protocol minor version {response.MinorVersion}.");
}
if ((response.NegotiatedCapabilities & ~RpcSessionProtocolRules.KnownCapabilities) != 0)
if ((response.NegotiatedCapabilities & ~RpcSessionProtocolRules.RecognizedCapabilities) != 0)
throw Failure(SharpLinkErrorCode.ProtocolViolation, "Server negotiated unknown capabilities.");
if ((response.NegotiatedCapabilities & ~offer.SupportedCapabilities) != 0)
throw Failure(SharpLinkErrorCode.ProtocolViolation, "Server negotiated a capability the client did not offer.");
Expand Down
12 changes: 6 additions & 6 deletions src/SharpLink.Runtime/ProtocolV2/ProtocolV2PayloadCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace SharpLink.Runtime;
/// <summary>Encodes and decodes Protocol v2 control and error payloads.</summary>
public static class ProtocolV2PayloadCodec
{
private const ProtocolV2Capabilities KnownCapabilities =
RpcSessionProtocolRules.KnownCapabilities;
private const ProtocolV2Capabilities RecognizedCapabilities =
RpcSessionProtocolRules.RecognizedCapabilities;
private static readonly Encoding SStrictUtf8 = new UTF8Encoding(false, true);
private const int HandshakeRequestFixedBytes =
sizeof(ushort) + sizeof(ulong) + sizeof(ulong) + sizeof(int) + sizeof(int) + sizeof(int);
Expand Down Expand Up @@ -102,7 +102,7 @@ public static void WriteHandshakeResponse(
in ProtocolV2HandshakeResponse response)
{
ArgumentNullException.ThrowIfNull(writer);
ValidateKnownCapabilities(response.NegotiatedCapabilities, nameof(response));
ValidateRecognizedCapabilities(response.NegotiatedCapabilities, nameof(response));
ValidateOutboundCompressionSelection(response);
ValidateLocalLimits(response.MaxFramePayloadBytes, response.StreamReceiveWindowBytes,
response.ConnectionReceiveWindowBytes);
Expand Down Expand Up @@ -147,7 +147,7 @@ public static ProtocolV2HandshakeResponse ReadHandshakeResponse(
var profile = profileLength == 0 ? null : ReadCompressionProfile(ref reader, profileLength);
ValidatePeerLimits(maxFrame, streamWindow, connectionWindow);
var negotiatedCapabilities = (ProtocolV2Capabilities)unchecked((ulong)capabilitiesBits);
if ((negotiatedCapabilities & ~KnownCapabilities) != 0)
if ((negotiatedCapabilities & ~RecognizedCapabilities) != 0)
throw ProtocolV2FrameParser.Violation("HandshakeResponse negotiated unknown capabilities.");
var response = new ProtocolV2HandshakeResponse(
unchecked((ushort)minorBits),
Expand All @@ -160,11 +160,11 @@ public static ProtocolV2HandshakeResponse ReadHandshakeResponse(
return response;
}

private static void ValidateKnownCapabilities(
private static void ValidateRecognizedCapabilities(
ProtocolV2Capabilities capabilities,
string parameterName)
{
if ((capabilities & ~KnownCapabilities) != 0)
if ((capabilities & ~RecognizedCapabilities) != 0)
throw new ArgumentOutOfRangeException(parameterName, "Handshake capabilities contain unknown bits.");
}

Expand Down
2 changes: 1 addition & 1 deletion src/SharpLink.Runtime/RpcSession.Negotiation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ internal void EnsureInboundFrameAllowed(
$"Negotiated protocol minor version {options.ProtocolMinorVersion} exceeds the local " +
$"version {ProtocolV2Constants.MinorVersion}.");
}
if ((options.Capabilities & ~RpcSessionProtocolRules.KnownCapabilities) != 0)
if ((options.Capabilities & ~RpcSessionProtocolRules.RecognizedCapabilities) != 0)
throw NegotiationViolation("Negotiated capabilities contain unknown bits.");
if (options.MaxFramePayloadBytes < SharpLinkProtocolOptions.MinMaxFramePayloadBytes ||
options.MaxFramePayloadBytes > RuntimeContext.Protocol.MaxFramePayloadBytes)
Expand Down
29 changes: 24 additions & 5 deletions test/SharpLink.UnitTests/Protocol/ProtocolV2NegotiatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public void ClientOfferShouldContainOnlyPolicyAndAuthenticationInputs()

Ensure(offer.MinorVersion == ProtocolV2Constants.MinorVersion,
"the implemented policy must advertise the current minor version");
Ensure(offer.SupportedCapabilities == RpcSessionProtocolRules.KnownCapabilities,
"one central implemented-capability set must drive the offer");
Ensure(offer.SupportedCapabilities == policy.SupportedCapabilities,
"the client offer must publish the complete explicit local policy");
Ensure(offer.RequiredCapabilities ==
(ProtocolV2Capabilities.Metadata | ProtocolV2Capabilities.Compression),
"the caller's required capabilities must remain explicit policy input");
Expand All @@ -39,6 +39,25 @@ public void ClientOfferShouldContainOnlyPolicyAndAuthenticationInputs()
"the client provider order must become the offer preference order");
}

[Test]
public void ImplementedCapabilitiesShouldRemainExplicitAndProtocolRecognized()
{
const ProtocolV2Capabilities expected =
ProtocolV2Capabilities.Metadata |
ProtocolV2Capabilities.FlowControl |
ProtocolV2Capabilities.HealthCheck |
ProtocolV2Capabilities.CancellationReason;

Ensure(ProtocolV2Negotiator.AlwaysImplementedCapabilities == expected,
"endpoint implementation support must remain an explicit declaration");
Ensure((ProtocolV2Negotiator.AlwaysImplementedCapabilities &
~RpcSessionProtocolRules.RecognizedCapabilities) == ProtocolV2Capabilities.None,
"every implemented capability must be recognized by the protocol boundary");
Ensure((ProtocolV2Negotiator.AlwaysImplementedCapabilities &
ProtocolV2Capabilities.Compression) == ProtocolV2Capabilities.None,
"compression must remain conditional on configured provider support");
}

[Test]
public void PolicyConstructionShouldRejectEveryIllegalLocalState()
{
Expand Down Expand Up @@ -426,7 +445,7 @@ public void ServerResultShouldValidateToEquivalentClientSnapshot()
{
var clientProviders = Bindings("client-only", "shared");
var serverProviders = Bindings("server-only", "shared");
var offeredCapabilities = RpcSessionProtocolRules.KnownCapabilities;
var offeredCapabilities = RpcSessionProtocolRules.RecognizedCapabilities;
var offer = new ProtocolV2HandshakeRequest(
ProtocolV2Constants.MinorVersion,
offeredCapabilities,
Expand Down Expand Up @@ -476,11 +495,11 @@ public void RepeatedInputsShouldProduceEquivalentResultsAndErrors()
var clientProviders = Bindings("client-only", "shared");
var serverProviders = Bindings("server-only", "shared");
var offer = CreateOffer(
RpcSessionProtocolRules.KnownCapabilities,
RpcSessionProtocolRules.RecognizedCapabilities,
ProtocolV2Capabilities.Metadata,
clientProviders);
var policy = CreatePolicy(
RpcSessionProtocolRules.KnownCapabilities,
RpcSessionProtocolRules.RecognizedCapabilities,
serverProviders,
maxFramePayloadBytes: 4096,
streamReceiveWindowBytes: 1024,
Expand Down
Loading