Bind CultMesh application ingress to session identity
This commit is contained in:
parent
99458dbb57
commit
7a6f0be21f
5 changed files with 38 additions and 5 deletions
|
|
@ -587,7 +587,7 @@ namespace GameCult.Mesh
|
|||
using var snapshot = await CultMeshSnapshotSession.ConnectAsync(
|
||||
_sessions,
|
||||
target,
|
||||
CultMeshProtocols.Content,
|
||||
CultMeshProtocols.Documents,
|
||||
new CultMeshSnapshotRequestOptions
|
||||
{
|
||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
|
|
@ -70,7 +71,9 @@ namespace GameCult.Mesh
|
|||
private readonly HashSet<string> _protocolIds;
|
||||
private readonly HashSet<string> _routeGenerations;
|
||||
private readonly Dictionary<string, CultMeshSessionProofSigner> _proofSigners;
|
||||
private readonly ConcurrentDictionary<ICultNetSchemaServerPeer, string> _acceptedPeers = new();
|
||||
private readonly Func<CultMeshSessionOpenMessage, ICultNetSchemaServerPeer, Task> _handler;
|
||||
private readonly ICultNetSchemaServerPeerLifecycle? _peerLifecycle;
|
||||
private bool _disposed;
|
||||
|
||||
public CultMeshSessionIdentityServer(
|
||||
|
|
@ -90,18 +93,33 @@ namespace GameCult.Mesh
|
|||
.ToDictionary(signer => signer.Route.Generation, StringComparer.Ordinal);
|
||||
_handler = HandleAsync;
|
||||
_server.OnCultNet(_handler);
|
||||
_peerLifecycle = server as ICultNetSchemaServerPeerLifecycle;
|
||||
if (_peerLifecycle != null)
|
||||
_peerLifecycle.PeerDisconnected += ForgetPeer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the runtime identity established by this peer's accepted session-open handshake.
|
||||
/// Application ingress must use this transport-bound value instead of trusting payload identity fields.
|
||||
/// </summary>
|
||||
public bool TryGetSourceRuntimeId(ICultNetSchemaServerPeer peer, out string sourceRuntimeId) =>
|
||||
_acceptedPeers.TryGetValue(peer, out sourceRuntimeId!);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_server.RemoveCultNetMessageListener<CultMeshSessionOpenMessage>(_handler);
|
||||
if (_peerLifecycle != null)
|
||||
_peerLifecycle.PeerDisconnected -= ForgetPeer;
|
||||
_acceptedPeers.Clear();
|
||||
}
|
||||
|
||||
private Task HandleAsync(CultMeshSessionOpenMessage request, ICultNetSchemaServerPeer peer)
|
||||
{
|
||||
var error = Validate(request);
|
||||
if (error == null)
|
||||
error = BindPeer(peer, request.SourceRuntimeId);
|
||||
_proofSigners.TryGetValue(request.RouteGeneration ?? string.Empty, out var signer);
|
||||
peer.SendCultNet(new CultMeshSessionAcceptedMessage
|
||||
{
|
||||
|
|
@ -119,6 +137,19 @@ namespace GameCult.Mesh
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ForgetPeer(ICultNetSchemaServerPeer peer) => _acceptedPeers.TryRemove(peer, out _);
|
||||
|
||||
private string? BindPeer(ICultNetSchemaServerPeer peer, string sourceRuntimeId)
|
||||
{
|
||||
if (_acceptedPeers.TryGetValue(peer, out var established))
|
||||
return string.Equals(established, sourceRuntimeId, StringComparison.Ordinal)
|
||||
? null
|
||||
: "session-source-runtime-rebind-forbidden";
|
||||
return _acceptedPeers.TryAdd(peer, sourceRuntimeId)
|
||||
? null
|
||||
: BindPeer(peer, sourceRuntimeId);
|
||||
}
|
||||
|
||||
private string? Validate(CultMeshSessionOpenMessage request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.MessageId)) return "session-message-id-required";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ public sealed class CultMeshTransportModularityTests
|
|||
new[] { RouteGeneration });
|
||||
server.OnCultNet<CultNetHelloMessage>((message, peer) =>
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = "tcp:" + message.RuntimeId });
|
||||
identity.TryGetSourceRuntimeId(peer, out var sourceRuntimeId).Should().BeTrue(
|
||||
"application ingress must only trust a runtime identity bound by the session-open handshake");
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = $"tcp:{message.RuntimeId}:{sourceRuntimeId}" });
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
var tcpEndpoint = $"cultnet+tcp://127.0.0.1:{server.LocalEndPoint.Port}";
|
||||
|
|
@ -55,7 +57,7 @@ public sealed class CultMeshTransportModularityTests
|
|||
var received = await response.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
session.State.Path!.Endpoint.Should().Be(tcpEndpoint);
|
||||
received.Error.Should().Be("tcp:eve-unity");
|
||||
received.Error.Should().Be("tcp:eve-unity:cultmesh-client");
|
||||
legacy.ConnectCount.Should().Be(0,
|
||||
"a lower transport tier is fallback, not a race against the preferred TCP tier");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit" Version="4.*" />
|
||||
<PackageReference Include="NUnit" Version="4.6.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "org.gamecult.cultlib",
|
||||
"displayName": "CultLib",
|
||||
"version": "1.0.49",
|
||||
"version": "1.0.50",
|
||||
"unity": "2021.3",
|
||||
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
Loading…
Reference in a new issue