From f2a31655a349d92d172d60c8e90255ba133aa917 Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Thu, 22 Oct 2020 16:09:20 +0200 Subject: [PATCH] Add client packet recorder --- .../Udp/UdpConnectionListener.cs | 7 +- src/Impostor.Server/Data/DebugConfig.cs | 3 +- src/Impostor.Server/Impostor.Server.csproj | 6 + src/Impostor.Server/Program.cs | 28 +++- .../Recorder/ClientRecorder.cs | 32 ++++ src/Impostor.Server/Recorder/GameRecorder.cs | 7 - .../Recorder/PacketRecorder.cs | 139 ++++++++++++++++++ .../Recorder/PacketSerializationContext.cs | 56 +++++++ ...tSerializationContextPooledObjectPolicy.cs | 18 +++ .../Recorder/RecordedPacketType.cs | 8 + src/Impostor.Server/config.full.json | 3 +- src/Impostor.Server/config.json | 4 + 12 files changed, 298 insertions(+), 13 deletions(-) create mode 100644 src/Impostor.Server/Recorder/ClientRecorder.cs delete mode 100644 src/Impostor.Server/Recorder/GameRecorder.cs create mode 100644 src/Impostor.Server/Recorder/PacketRecorder.cs create mode 100644 src/Impostor.Server/Recorder/PacketSerializationContext.cs create mode 100644 src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs create mode 100644 src/Impostor.Server/Recorder/RecordedPacketType.cs diff --git a/src/Impostor.Hazel/Udp/UdpConnectionListener.cs b/src/Impostor.Hazel/Udp/UdpConnectionListener.cs index aa3802e..1edd56a 100644 --- a/src/Impostor.Hazel/Udp/UdpConnectionListener.cs +++ b/src/Impostor.Hazel/Udp/UdpConnectionListener.cs @@ -134,11 +134,16 @@ namespace Impostor.Hazel.Udp continue; } } - catch (SocketException e) + catch (SocketException) { // Client no longer reachable, pretend it didn't happen continue; } + catch (ObjectDisposedException) + { + // Socket was disposed, don't care. + return; + } // Get client from active clients if (!_allConnections.TryGetValue(data.RemoteEndPoint, out var client)) diff --git a/src/Impostor.Server/Data/DebugConfig.cs b/src/Impostor.Server/Data/DebugConfig.cs index b0f942f..2f7d4b3 100644 --- a/src/Impostor.Server/Data/DebugConfig.cs +++ b/src/Impostor.Server/Data/DebugConfig.cs @@ -4,6 +4,7 @@ { public const string Section = "Debug"; - public bool EnableGameRecorder { get; set; } + public bool GameRecorderEnabled { get; set; } + public string GameRecorderPath { get; set; } } } \ No newline at end of file diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index 2737068..7c3f317 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -29,6 +29,7 @@ + @@ -38,6 +39,11 @@ + + Always + PreserveNewest + true + Always PreserveNewest diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 8c9e7a3..f85e23d 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -9,9 +9,11 @@ using Impostor.Server.Net.Factories; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Redirector; using Impostor.Server.Plugins; +using Impostor.Server.Recorder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; using Serilog; using Serilog.Events; @@ -80,6 +82,10 @@ namespace Impostor.Server }) .ConfigureServices((host, services) => { + var debug = host.Configuration + .GetSection(DebugConfig.Section) + .Get() ?? new DebugConfig(); + var redirector = host.Configuration .GetSection(ServerRedirectorConfig.Section) .Get() ?? new ServerRedirectorConfig(); @@ -88,8 +94,7 @@ namespace Impostor.Server services.Configure(host.Configuration.GetSection(DebugConfig.Section)); #endif services.Configure(host.Configuration.GetSection(ServerConfig.Section)); - services.Configure( - host.Configuration.GetSection(ServerRedirectorConfig.Section)); + services.Configure(host.Configuration.GetSection(ServerRedirectorConfig.Section)); if (redirector.Enabled) { @@ -142,7 +147,24 @@ namespace Impostor.Server } else { - services.AddSingleton>(); + if (debug.GameRecorderEnabled) + { + services.AddSingleton(new DefaultObjectPoolProvider()); + services.AddSingleton>(serviceProvider => + { + var provider = serviceProvider.GetRequiredService(); + var policy = new PacketSerializationContextPooledObjectPolicy(); + return provider.Create(policy); + }); + + services.AddSingleton(); + services.AddSingleton>(); + } + else + { + services.AddSingleton>(); + } + services.AddSingleton(); services.AddSingleton(p => p.GetRequiredService()); } diff --git a/src/Impostor.Server/Recorder/ClientRecorder.cs b/src/Impostor.Server/Recorder/ClientRecorder.cs new file mode 100644 index 0000000..a03be75 --- /dev/null +++ b/src/Impostor.Server/Recorder/ClientRecorder.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Recorder +{ + internal class ClientRecorder : Client + { + private readonly PacketRecorder _recorder; + + public ClientRecorder(ILogger logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder) + : base(logger, clientManager, gameManager, name, connection) + { + _recorder = recorder; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + await _recorder.WriteMessageAsync(this, reader.Tag, reader.Buffer); + await base.HandleMessageAsync(reader, messageType); + } + + public override async ValueTask HandleDisconnectAsync(string reason) + { + await _recorder.WriteDisconnectAsync(this); + await base.HandleDisconnectAsync(reason); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/GameRecorder.cs b/src/Impostor.Server/Recorder/GameRecorder.cs deleted file mode 100644 index 785e026..0000000 --- a/src/Impostor.Server/Recorder/GameRecorder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Impostor.Server.Recorder -{ - public class GameRecorder - { - - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/PacketRecorder.cs b/src/Impostor.Server/Recorder/PacketRecorder.cs new file mode 100644 index 0000000..a949e58 --- /dev/null +++ b/src/Impostor.Server/Recorder/PacketRecorder.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Data; +using Impostor.Server.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Recorder +{ + /// + /// Records all packets received in . + /// + internal class PacketRecorder : IDisposable + { + private readonly ILogger _logger; + private readonly ObjectPool _pool; + private readonly SemaphoreSlim _writerLock; + private readonly FileStream _writer; + + public PacketRecorder(ILogger logger, IOptions options, ObjectPool pool) + { + var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat"; + var path = Path.Combine(options.Value.GameRecorderPath, name); + + _logger = logger; + _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", path); + _pool = pool; + _writerLock = new SemaphoreSlim(1, 1); + _writer = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + } + + public async Task WriteMessageAsync(ClientRecorder client, byte tag, ReadOnlyMemory buffer) + { + _logger.LogTrace("Writing Message."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Message); + WriteClient(context, client); + WritePacket(context, tag, buffer.Span); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteDisconnectAsync(ClientRecorder client) + { + _logger.LogTrace("Writing Disconnect."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Disconnect); + WriteClient(context, client); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type) + { + // Length placeholder. + context.Writer.Write((int) 0); + context.Writer.Write((byte) type); + } + + private static void WriteClient(PacketSerializationContext context, ClientBase client) + { + var addressBytes = client.Connection.EndPoint.Address.GetAddressBytes(); + + context.Writer.Write((byte) addressBytes.Length); + context.Writer.Write(addressBytes); + context.Writer.Write((ushort) client.Connection.EndPoint.Port); + } + + private static void WritePacket(PacketSerializationContext context, byte tag, ReadOnlySpan buffer) + { + context.Writer.Write((byte) tag); + context.Writer.Write((int) buffer.Length); + context.Writer.Write(buffer); + } + + private static void WriteLength(PacketSerializationContext context) + { + var length = context.Stream.Position; + + context.Stream.Position = 0; + context.Writer.Write((int) length); + context.Stream.Position = length; + } + + private async Task WriteAsync(Stream data) + { + var hasLock = false; + + try + { + hasLock = await _writerLock.WaitAsync(TimeSpan.FromMinutes(1)); + + if (hasLock) + { + data.Position = 0; + + await data.CopyToAsync(_writer); + await _writer.FlushAsync(); + } + } + finally + { + if (hasLock) + { + _writerLock.Release(); + } + } + } + + public void Dispose() + { + _writer.Dispose(); + _writerLock.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/PacketSerializationContext.cs b/src/Impostor.Server/Recorder/PacketSerializationContext.cs new file mode 100644 index 0000000..07755f6 --- /dev/null +++ b/src/Impostor.Server/Recorder/PacketSerializationContext.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Text; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContext + { + private const int InitialStreamSize = 0x100; + private const int MaximumStreamSize = 0x100000; + + private MemoryStream _memory; + private BinaryWriter _writer; + + public MemoryStream Stream + { + get + { + if (_memory == null) + { + _memory = new MemoryStream(InitialStreamSize); + } + + return _memory; + } + private set => _memory = value; + } + + public BinaryWriter Writer + { + get + { + if (_writer == null) + { + _writer = new BinaryWriter(Stream, Encoding.UTF8, true); + } + + return _writer; + } + private set => _writer = value; + } + + public void Reset() + { + if (Stream.Capacity > MaximumStreamSize) + { + Stream = null; + Writer = null; + } + else + { + Stream.Position = 0L; + Stream.SetLength(0L); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs b/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs new file mode 100644 index 0000000..17b355f --- /dev/null +++ b/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy + { + public PacketSerializationContext Create() + { + return new PacketSerializationContext(); + } + + public bool Return(PacketSerializationContext obj) + { + obj.Reset(); + return true; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Recorder/RecordedPacketType.cs b/src/Impostor.Server/Recorder/RecordedPacketType.cs new file mode 100644 index 0000000..24e645e --- /dev/null +++ b/src/Impostor.Server/Recorder/RecordedPacketType.cs @@ -0,0 +1,8 @@ +namespace Impostor.Server.Recorder +{ + internal enum RecordedPacketType : byte + { + Message = 1, + Disconnect = 2, + } +} \ No newline at end of file diff --git a/src/Impostor.Server/config.full.json b/src/Impostor.Server/config.full.json index d13e17b..e509ca0 100644 --- a/src/Impostor.Server/config.full.json +++ b/src/Impostor.Server/config.full.json @@ -20,6 +20,7 @@ ] }, "Debug": { - "EnableGameRecorder": true + "GameRecorderEnabled": true, + "GameRecorderPath": "" } } \ No newline at end of file diff --git a/src/Impostor.Server/config.json b/src/Impostor.Server/config.json index f969cbf..f5f0ebe 100644 --- a/src/Impostor.Server/config.json +++ b/src/Impostor.Server/config.json @@ -4,5 +4,9 @@ "PublicPort": 22023, "ListenIp": "0.0.0.0", "ListenPort": 22023 + }, + "Debug": { + "GameRecorderEnabled": true, + "GameRecorderPath": "" } } \ No newline at end of file -- 2.39.5