diff --git a/Directory.Build.props b/Directory.Build.props index 595da325..10c8d749 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ MIT © Meta Vens and contributors true - 1.0.34 + 1.0.35 $(CultLibPackageVersion) diff --git a/samples/eve-browser-network/EveBrowserNetworkSample.csproj b/samples/eve-browser-network/EveBrowserNetworkSample.csproj index 5dde9995..50b41781 100644 --- a/samples/eve-browser-network/EveBrowserNetworkSample.csproj +++ b/samples/eve-browser-network/EveBrowserNetworkSample.csproj @@ -5,7 +5,7 @@ enable $(MSBuildThisFileDirectory)..\..\..\Eve false - 1.0.34 + 1.0.35 0.3.3 $(PackageFeed) diff --git a/scripts/build-unity-package.ps1 b/scripts/build-unity-package.ps1 index 08366cc5..c0e0518f 100644 --- a/scripts/build-unity-package.ps1 +++ b/scripts/build-unity-package.ps1 @@ -9,9 +9,11 @@ $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot $projectPath = Join-Path $repoRoot "src\GameCult.Mesh\GameCult.Mesh.csproj" +$webSocketProjectPath = Join-Path $repoRoot "src\GameCult.Networking.WebSockets\GameCult.Networking.WebSockets.csproj" $quicProjectPath = Join-Path $repoRoot "src\GameCult.Mesh.Quic.Native\GameCult.Mesh.Quic.Native.csproj" $templateRoot = Join-Path $repoRoot "unity\org.gamecult.cultlib" $publishRoot = Join-Path $repoRoot "artifacts\unity-publish" +$webSocketPublishRoot = Join-Path $repoRoot "artifacts\unity-websocket-publish" $quicPublishRoot = Join-Path $repoRoot "artifacts\unity-quic-publish" $quicNativeRoot = Join-Path $repoRoot "artifacts\unity-quic-native" $outputRoot = if ([System.IO.Path]::IsPathRooted($OutputDirectory)) { @@ -27,11 +29,15 @@ if (Test-Path -LiteralPath $publishRoot) { if (Test-Path -LiteralPath $quicPublishRoot) { Remove-Item -LiteralPath $quicPublishRoot -Recurse -Force } +if (Test-Path -LiteralPath $webSocketPublishRoot) { + Remove-Item -LiteralPath $webSocketPublishRoot -Recurse -Force +} if (Test-Path -LiteralPath $outputRoot) { Remove-Item -LiteralPath $outputRoot -Recurse -Force } -$publishArguments = @("publish", $projectPath, "-c", $Configuration, "-o", $publishRoot) +$unityPackageVersion = (Get-Content -LiteralPath (Join-Path $templateRoot "package.json") -Raw | ConvertFrom-Json).version +$publishArguments = @("publish", $projectPath, "-c", $Configuration, "-o", $publishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { $publishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))" } @@ -39,7 +45,15 @@ if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { if ($LASTEXITCODE -ne 0) { throw "CultLib publish failed with exit code $LASTEXITCODE" } -$quicPublishArguments = @("publish", $quicProjectPath, "-c", $Configuration, "-o", $quicPublishRoot) +$webSocketPublishArguments = @("publish", $webSocketProjectPath, "-c", $Configuration, "-f", "netstandard2.1", "-o", $webSocketPublishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") +if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { + $webSocketPublishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))" +} +& dotnet @webSocketPublishArguments +if ($LASTEXITCODE -ne 0) { + throw "CultLib WebSocket publish failed with exit code $LASTEXITCODE" +} +$quicPublishArguments = @("publish", $quicProjectPath, "-c", $Configuration, "-o", $quicPublishRoot, "-p:CultLibPackageVersion=$unityPackageVersion") if (-not [string]::IsNullOrWhiteSpace($NuGetConfig)) { $quicPublishArguments += "-p:RestoreConfigFile=$([IO.Path]::GetFullPath($NuGetConfig))" } @@ -63,6 +77,7 @@ $expectedAssemblies = @( "GameCult.Mesh.dll", "GameCult.Mesh.Quic.Native.dll", "GameCult.Networking.dll", + "GameCult.Networking.WebSockets.dll", "Isopoh.Cryptography.Argon2.dll", "Isopoh.Cryptography.Blake2b.dll", "Isopoh.Cryptography.SecureArray.dll", @@ -88,6 +103,9 @@ foreach ($assembly in Get-ChildItem -LiteralPath $publishRoot -Filter "*.dll") { foreach ($assembly in Get-ChildItem -LiteralPath $quicPublishRoot -Filter "*.dll") { $publishedByName[$assembly.Name] = $assembly } +foreach ($assembly in Get-ChildItem -LiteralPath $webSocketPublishRoot -Filter "*.dll") { + $publishedByName[$assembly.Name] = $assembly +} foreach ($assemblyName in $expectedAssemblies) { if (-not $publishedByName.ContainsKey($assemblyName)) { throw "CultLib Unity package is missing expected assembly: $assemblyName" @@ -112,6 +130,26 @@ New-Item -ItemType Directory -Force -Path (Join-Path $outputRoot "Third Party No Copy-Item -LiteralPath (Join-Path $quicNativeRoot "MSQUIC-LICENSE.txt") ` -Destination (Join-Path $outputRoot "Third Party Notices\MSQUIC-LICENSE.txt") +$meshPackagePath = Join-Path $pluginRoot "GameCult.Mesh.dll" +$meshPackageVersion = [Reflection.AssemblyName]::GetAssemblyName($meshPackagePath).Version.ToString() +if ($meshPackageVersion -ne "$unityPackageVersion.0") { + throw "Packaged GameCult.Mesh.dll version '$meshPackageVersion' does not match package '$unityPackageVersion'." +} +$meshPackageMetadata = [Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($meshPackagePath)) +foreach ($requiredType in @( + "GameCult.Mesh.CultMeshAuthorityTrustPolicy", + "GameCult.Mesh.CultMeshRouteCertificate", + "GameCult.Mesh.CultMeshHttpsContentTransportConnector")) { + if (-not $meshPackageMetadata.Contains($requiredType.Split('.')[-1])) { + throw "Packaged GameCult.Mesh.dll is missing required API '$requiredType'." + } +} +$webSocketPackageMetadata = [Text.Encoding]::UTF8.GetString( + [IO.File]::ReadAllBytes((Join-Path $pluginRoot "GameCult.Networking.WebSockets.dll"))) +if (-not $webSocketPackageMetadata.Contains("CultNetWebSocketSchemaClient")) { + throw "Packaged WebSocket assembly is missing the Unity-compatible CultNet client." +} + if ($UpdateTemplate) { $templatePluginRoot = Join-Path $templateRoot "Runtime\Plugins" foreach ($assemblyName in $expectedAssemblies) { diff --git a/scripts/verify-eve-getting-started.ps1 b/scripts/verify-eve-getting-started.ps1 index 26c69f14..4a87b19d 100644 --- a/scripts/verify-eve-getting-started.ps1 +++ b/scripts/verify-eve-getting-started.ps1 @@ -32,7 +32,7 @@ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sourceRevision)) { throw "Could not determine the CultLib source revision for local package identity." } $verificationNonce = [guid]::NewGuid().ToString("N").Substring(0, 8) -$cultLibLocalVersion = "1.0.34-local.$sourceRevision.$verificationNonce" +$cultLibLocalVersion = "1.0.35-local.$sourceRevision.$verificationNonce" $surfaceLocalVersion = "0.3.3-local.$sourceRevision.$verificationNonce" try { & (Join-Path $EveRoot "scripts\pack-dotnet-surface.ps1") ` diff --git a/src/GameCult.Mesh/CultMeshClient.cs b/src/GameCult.Mesh/CultMeshClient.cs index 19a01c54..230ce55b 100644 --- a/src/GameCult.Mesh/CultMeshClient.cs +++ b/src/GameCult.Mesh/CultMeshClient.cs @@ -27,8 +27,8 @@ namespace GameCult.Mesh public IReadOnlyList? Connectors { get; set; } /// - /// Gets or sets streaming content connectors. TCP content delivery is used by default; - /// applications may replace it or explicitly add legacy RUDP. + /// Gets or sets streaming content connectors. HTTPS and loopback TCP content + /// delivery are used by default; applications may replace them or explicitly add legacy RUDP. /// public IReadOnlyList? ContentConnectors { get; set; } @@ -184,6 +184,7 @@ namespace GameCult.Mesh }, options.ContentConnectors ?? new ICultMeshContentTransportConnector[] { + new CultMeshHttpsContentTransportConnector(), new CultMeshTcpContentTransportConnector() }, options.RealtimeConnectors, diff --git a/src/GameCult.Mesh/CultMeshHttpsContentTransport.cs b/src/GameCult.Mesh/CultMeshHttpsContentTransport.cs new file mode 100644 index 00000000..d6427470 --- /dev/null +++ b/src/GameCult.Mesh/CultMeshHttpsContentTransport.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace GameCult.Mesh +{ + /// + /// Opens immutable content over an Odin-certified HTTPS route. Provider + /// identity comes from the verified route and TLS endpoint; the transfer + /// service remains responsible for checking the advertised chunk hash. + /// + public sealed class CultMeshHttpsContentTransportConnector : ICultMeshContentTransportConnector + { + private readonly Func _createHandler; + + public CultMeshHttpsContentTransportConnector(Func? createHandler = null) + { + _createHandler = createHandler ?? (() => new HttpClientHandler()); + } + + public string ConnectorId => "https-content"; + public int Priority => 0; + + public bool CanConnect(CultMeshTransportCandidate candidate) => + candidate != null && + Uri.TryCreate(candidate.Endpoint, UriKind.Absolute, out var endpoint) && + string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); + + public Task ConnectAsync( + CultMeshTransportCandidate candidate, + CultMeshSessionTarget target, + CancellationToken cancellationToken = default) + { + if (candidate == null) throw new ArgumentNullException(nameof(candidate)); + if (target == null) throw new ArgumentNullException(nameof(target)); + if (!CanConnect(candidate)) + throw new NotSupportedException($"HTTPS content connector does not support '{candidate.Endpoint}'."); + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(new CultMeshHttpsContentTransport( + candidate.Endpoint, + new HttpClient(_createHandler(), disposeHandler: true), + target, + candidate.Generation)); + } + } + + internal sealed class CultMeshHttpsContentTransport : ICultMeshContentTransport, ICultMeshVerifiedTransport + { + private readonly HttpClient _client; + private readonly CultMeshSessionTarget _target; + private readonly string _routeGeneration; + private bool _disposed; + + public CultMeshHttpsContentTransport( + string endpoint, + HttpClient client, + CultMeshSessionTarget target, + string routeGeneration) + { + Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + _client = client ?? throw new ArgumentNullException(nameof(client)); + _target = target ?? throw new ArgumentNullException(nameof(target)); + _routeGeneration = routeGeneration ?? string.Empty; + } + + public string TransportId => "https-content"; + public string Endpoint { get; } + + public bool IsVerifiedFor( + string verseId, + string authorityRuntimeId, + string protocolId, + string routeGeneration) => + string.Equals(_target.VerseId, verseId, StringComparison.Ordinal) && + string.Equals(_target.AuthorityRuntimeId, authorityRuntimeId, StringComparison.Ordinal) && + string.Equals(CultMeshProtocols.Content.Value, protocolId, StringComparison.Ordinal) && + string.Equals(_routeGeneration, routeGeneration, StringComparison.Ordinal); + + public async Task CopyChunkToAsync( + CultMeshCdnChunkRef chunk, + Stream destination, + CancellationToken cancellationToken = default) + { + if (_disposed) throw new ObjectDisposedException(nameof(CultMeshHttpsContentTransport)); + if (chunk == null) throw new ArgumentNullException(nameof(chunk)); + if (destination == null) throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) throw new ArgumentException("Content destination must be writable.", nameof(destination)); + + var requestUri = BuildChunkUri(Endpoint, chunk); + using var response = await _client.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + if (response.Content.Headers.ContentLength is long contentLength && + contentLength != chunk.SizeBytes) + { + throw new InvalidDataException( + $"HTTPS content length {contentLength} did not match advertised chunk size {chunk.SizeBytes}."); + } + using var source = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await source.CopyToAsync(destination, 81920, cancellationToken).ConfigureAwait(false); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _client.Dispose(); + } + + private static Uri BuildChunkUri(string endpoint, CultMeshCdnChunkRef chunk) + { + var builder = new UriBuilder(endpoint); + var separator = string.IsNullOrWhiteSpace(builder.Query) ? string.Empty : builder.Query.TrimStart('?') + "&"; + builder.Query = separator + + "chunkHash=" + Uri.EscapeDataString(CultMeshCdn.NormalizeHash(chunk.ChunkHash, nameof(chunk.ChunkHash))) + + "&recordKey=" + Uri.EscapeDataString(chunk.RecordKey ?? string.Empty); + return builder.Uri; + } + } +} diff --git a/src/GameCult.Networking.WebSockets/CultNetWebSocketMessageIO.cs b/src/GameCult.Networking.WebSockets/CultNetWebSocketMessageIO.cs new file mode 100644 index 00000000..de10a66e --- /dev/null +++ b/src/GameCult.Networking.WebSockets/CultNetWebSocketMessageIO.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace GameCult.Networking.WebSockets; + +internal static class CultNetWebSocketMessageIO +{ + public static Task SendAsync(WebSocket socket, byte[] payload, CancellationToken cancellationToken) => + socket.SendAsync(new ArraySegment(payload), WebSocketMessageType.Binary, true, cancellationToken); + + public static async Task ReceiveAsync( + WebSocket socket, + int maxMessageBytes, + CancellationToken cancellationToken) + { + var segment = new byte[Math.Min(16 * 1024, maxMessageBytes)]; + using var message = new MemoryStream(); + while (true) + { + var result = await socket.ReceiveAsync(new ArraySegment(segment), cancellationToken) + .ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) return null; + if (result.MessageType != WebSocketMessageType.Binary) + { + await socket.CloseAsync( + WebSocketCloseStatus.InvalidMessageType, + "CultNet requires binary MessagePack WebSocket messages.", + cancellationToken).ConfigureAwait(false); + return null; + } + if (message.Length + result.Count > maxMessageBytes) + { + await socket.CloseAsync( + WebSocketCloseStatus.MessageTooBig, + "CultNet message exceeds the configured maximum.", + cancellationToken).ConfigureAwait(false); + return null; + } + message.Write(segment, 0, result.Count); + if (result.EndOfMessage) return message.ToArray(); + } + } +} diff --git a/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaClient.cs b/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaClient.cs index 0a233ab7..394fe207 100644 --- a/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaClient.cs +++ b/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaClient.cs @@ -37,7 +37,7 @@ public sealed class CultNetWebSocketSchemaClient : ICultNetUriSchemaClient, ICul /// public void Connect(string host, int port) { - ArgumentException.ThrowIfNullOrWhiteSpace(host); + if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException("Host is required.", nameof(host)); if (port <= 0 || port > 65535) throw new ArgumentOutOfRangeException(nameof(port)); ConnectAsync(new Uri($"ws://{host}:{port}/cultmesh"), CancellationToken.None) .GetAwaiter().GetResult(); @@ -46,7 +46,7 @@ public sealed class CultNetWebSocketSchemaClient : ICultNetUriSchemaClient, ICul /// Connects to an exact WebSocket route. public async Task ConnectAsync(Uri endpoint, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(endpoint); + if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); if (endpoint.Scheme is not ("ws" or "wss")) throw new ArgumentException("CultNet WebSocket endpoint must use ws:// or wss://.", nameof(endpoint)); ThrowIfDisposed(); @@ -69,7 +69,7 @@ public sealed class CultNetWebSocketSchemaClient : ICultNetUriSchemaClient, ICul /// public void SendCultNet(T message) where T : ICultNetSchemaMessage { - ArgumentNullException.ThrowIfNull(message); + if (message == null) throw new ArgumentNullException(nameof(message)); ThrowIfDisposed(); var socket = _socket; if (socket?.State != WebSocketState.Open) @@ -92,7 +92,7 @@ public sealed class CultNetWebSocketSchemaClient : ICultNetUriSchemaClient, ICul /// public void OnCultNet(Action callback) where T : ICultNetSchemaMessage { - ArgumentNullException.ThrowIfNull(callback); + if (callback == null) throw new ArgumentNullException(nameof(callback)); ThrowIfDisposed(); lock (_handlerGate) { @@ -161,6 +161,6 @@ public sealed class CultNetWebSocketSchemaClient : ICultNetUriSchemaClient, ICul private void ThrowIfDisposed() { - ObjectDisposedException.ThrowIf(_disposed, this); + if (_disposed) throw new ObjectDisposedException(nameof(CultNetWebSocketSchemaClient)); } } diff --git a/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaServer.cs b/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaServer.cs index 240311a1..fa2728d8 100644 --- a/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaServer.cs +++ b/src/GameCult.Networking.WebSockets/CultNetWebSocketSchemaServer.cs @@ -178,44 +178,6 @@ public sealed class CultNetWebSocketSchemaServer : } } -internal static class CultNetWebSocketMessageIO -{ - public static Task SendAsync(WebSocket socket, byte[] payload, CancellationToken cancellationToken) => - socket.SendAsync(payload, WebSocketMessageType.Binary, true, cancellationToken); - - public static async Task ReceiveAsync( - WebSocket socket, - int maxMessageBytes, - CancellationToken cancellationToken) - { - var segment = new byte[Math.Min(16 * 1024, maxMessageBytes)]; - using var message = new MemoryStream(); - while (true) - { - var result = await socket.ReceiveAsync(segment, cancellationToken).ConfigureAwait(false); - if (result.MessageType == WebSocketMessageType.Close) return null; - if (result.MessageType != WebSocketMessageType.Binary) - { - await socket.CloseAsync( - WebSocketCloseStatus.InvalidMessageType, - "CultNet requires binary MessagePack WebSocket messages.", - cancellationToken).ConfigureAwait(false); - return null; - } - if (message.Length + result.Count > maxMessageBytes) - { - await socket.CloseAsync( - WebSocketCloseStatus.MessageTooBig, - "CultNet message exceeds the configured maximum.", - cancellationToken).ConfigureAwait(false); - return null; - } - message.Write(segment, 0, result.Count); - if (result.EndOfMessage) return message.ToArray(); - } - } -} - /// ASP.NET Core endpoint mapping for the CultNet WebSocket schema server. public static class CultNetWebSocketEndpointRouteBuilderExtensions { diff --git a/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj b/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj index 6ced9b42..3144d7fe 100644 --- a/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj +++ b/src/GameCult.Networking.WebSockets/GameCult.Networking.WebSockets.csproj @@ -1,14 +1,18 @@ - net10.0 + net10.0;netstandard2.1 enable enable CultNet schema-v0 WebSocket host adapter for browser and web runtimes GameCult.Networking.WebSockets game;networking;websocket;cultnet;cultmesh - + + + + + diff --git a/tests/GameCult.Mesh.Tests/CultMeshAuthorityProofTests.cs b/tests/GameCult.Mesh.Tests/CultMeshAuthorityProofTests.cs index 246e510a..42f02297 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshAuthorityProofTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshAuthorityProofTests.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -173,6 +177,54 @@ public sealed class CultMeshAuthorityProofTests connector.Attempts.Should().Equal(valid.Endpoint); } + [Test] + public async Task AuthenticatedRemoteContentUsesCertifiedHttpsAndStreamsTheAdvertisedChunk() + { + using var odin = ECDsa.Create(ECCurve.NamedCurves.nistP256); + using var provider = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var payload = Encoding.UTF8.GetBytes("certified-content"); + var route = CultMeshAuthorityProof.CreateSignedRoute( + "aetheria", "aetheria-daemon", "https://provider.example/cultmesh/content", + new[] { CultMeshProtocols.Content.Value }, 0, "content-generation", + CultMeshEcdsaP256PublicKey.From("provider-content", provider), "odin-content", + Now.AddMinutes(-1), Now.AddMinutes(5), odin); + var descriptor = new CultMeshVerseDescriptor( + "aetheria", "Aetheria", CultMeshVerseAuthorityModel.OperatorCluster, + new CultMeshVerseCompatibility("cultmesh.v0", "rules"), + authorityRoutes: new[] { route }); + using var discovery = new CultMeshDiscoveryService(new[] { new SignedRouteSource(descriptor) }); + var handler = new FixedContentHandler(payload); + using var manager = new CultMeshSessionManager( + discovery, + Array.Empty(), + new ICultMeshContentTransportConnector[] + { + new CultMeshHttpsContentTransportConnector(() => handler) + }, + new CultMeshSessionManagerOptions + { + Trust = new CultMeshAuthorityTrustPolicy( + CultMeshAuthorityTrustMode.AuthenticatedRemote, + new[] { CultMeshEcdsaP256PublicKey.From("odin-content", odin) }), + Clock = new FixedClock(Now) + }); + + var session = await manager.ConnectContentAsync( + new CultMeshSessionTarget("aetheria", "aetheria-daemon")); + using var destination = new MemoryStream(); + await session.CopyChunkToAsync(new CultMeshCdnChunkRef + { + ChunkHash = Convert.ToHexString(SHA256.HashData(payload)).ToLowerInvariant(), + RecordKey = "cdn:chunk:certified", + SizeBytes = payload.Length + }, destination); + + destination.ToArray().Should().Equal(payload); + handler.RequestUri.Should().NotBeNull(); + handler.RequestUri!.Scheme.Should().Be(Uri.UriSchemeHttps); + handler.RequestUri.Query.Should().Contain("chunkHash=").And.Contain("recordKey="); + } + private static CultMeshAuthorityRoute SignedRoute(ECDsa odin, ECDsa provider) => CultMeshAuthorityProof.CreateSignedRoute( "aetheria", @@ -326,4 +378,30 @@ public sealed class CultMeshAuthorityProofTests } public void Dispose() { } } + + private sealed class FixedContentHandler : HttpMessageHandler + { + private readonly byte[] _payload; + public FixedContentHandler(byte[] payload) => _payload = payload; + public Uri? RequestUri { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + RequestUri = request.RequestUri; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_payload) + }); + } + } + + private sealed class FixedClock : ICultMeshClock + { + public FixedClock(DateTimeOffset now) => UtcNow = now; + public DateTimeOffset UtcNow { get; } + public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken = default) => + Task.Delay(delay, cancellationToken); + } } diff --git a/unity/org.gamecult.cultlib/Runtime/GameCult.CultLib.asmdef b/unity/org.gamecult.cultlib/Runtime/GameCult.CultLib.asmdef index 100db084..bd13cb38 100644 --- a/unity/org.gamecult.cultlib/Runtime/GameCult.CultLib.asmdef +++ b/unity/org.gamecult.cultlib/Runtime/GameCult.CultLib.asmdef @@ -14,6 +14,7 @@ "GameCult.Mesh.dll", "GameCult.Mesh.Quic.Native.dll", "GameCult.Networking.dll", + "GameCult.Networking.WebSockets.dll", "Isopoh.Cryptography.Argon2.dll", "Isopoh.Cryptography.Blake2b.dll", "Isopoh.Cryptography.SecureArray.dll", diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll index 0e9e063f..2fdc1ff9 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb index 56d08c92..1b4555df 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll index 98c10206..4d90585e 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb index 6213e7e8..345abea5 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll index d1fe789e..d37534a3 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb index 27bf8a8a..fa1b1cb4 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.dll index c7ef8430..a7c40f95 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.pdb index f2a26092..a73f4ef6 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.Quic.Native.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll index 57024e4f..5675cc68 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb index acac39f4..e9bcee25 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll new file mode 100644 index 00000000..233edd88 Binary files /dev/null and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll.meta b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll.meta new file mode 100644 index 00000000..3679db12 --- /dev/null +++ b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.WebSockets.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 78372006a7d6494ca38f58a78eb535a8 diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll index ee85d89f..eab06cce 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb index 09055758..bbacfe84 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb differ diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/x86_64/gamecult_mesh_quic_native.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/x86_64/gamecult_mesh_quic_native.dll index 91133b5a..6805f175 100644 Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/x86_64/gamecult_mesh_quic_native.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/x86_64/gamecult_mesh_quic_native.dll differ diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json index b0a00486..b957b18c 100644 --- a/unity/org.gamecult.cultlib/package.json +++ b/unity/org.gamecult.cultlib/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.cultlib", "displayName": "CultLib", - "version": "1.0.48", + "version": "1.0.49", "unity": "2021.3", "description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.", "license": "MIT",