]> git.deb.at Git - rhonda/impostor.git/commitdiff
Add client packet recorder
authorAeonLucid <aeonlucid@outlook.com>
Thu, 22 Oct 2020 14:09:20 +0000 (16:09 +0200)
committerAeonLucid <aeonlucid@outlook.com>
Thu, 22 Oct 2020 14:09:20 +0000 (16:09 +0200)
12 files changed:
src/Impostor.Hazel/Udp/UdpConnectionListener.cs
src/Impostor.Server/Data/DebugConfig.cs
src/Impostor.Server/Impostor.Server.csproj
src/Impostor.Server/Program.cs
src/Impostor.Server/Recorder/ClientRecorder.cs [new file with mode: 0644]
src/Impostor.Server/Recorder/GameRecorder.cs [deleted file]
src/Impostor.Server/Recorder/PacketRecorder.cs [new file with mode: 0644]
src/Impostor.Server/Recorder/PacketSerializationContext.cs [new file with mode: 0644]
src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs [new file with mode: 0644]
src/Impostor.Server/Recorder/RecordedPacketType.cs [new file with mode: 0644]
src/Impostor.Server/config.full.json
src/Impostor.Server/config.json

index aa3802ecb177b36ae463ecbcd627ece8144751b4..1edd56a76a439059191dd25dfd7eda5bb03701ad 100644 (file)
@@ -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))
index b0f942f2e345744af06b8ca803014e5a2cbca21b..2f7d4b3b80b381b09ecb9367f985e679531af6dd 100644 (file)
@@ -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
index 27370687dd72728fa5df31ef479350367f85c18d..7c3f317d1573e08bcf6a43b7a0f0fc4dadd77b6d 100644 (file)
@@ -29,6 +29,7 @@
         <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0-rc.1.20451.7" />
         <PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0-rc.1.20451.14" />
         <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0-rc.1.20451.14" />
+        <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0-rc.2.20475.17" />
         <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
         <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
     </ItemGroup>
 
     <ItemGroup>
+        <Content Include="config.Development.json">
+          <CopyToPublishDirectory>Always</CopyToPublishDirectory>
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+          <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+        </Content>
         <Content Include="config.json">
             <CopyToPublishDirectory>Always</CopyToPublishDirectory>
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
index 8c9e7a3d2361daab493706c7b36864f1b454ea65..f85e23d6b5f94bcc5b2f987866400bfd394a08fc 100644 (file)
@@ -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<DebugConfig>() ?? new DebugConfig();
+
                     var redirector = host.Configuration
                         .GetSection(ServerRedirectorConfig.Section)
                         .Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig();
@@ -88,8 +94,7 @@ namespace Impostor.Server
                     services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section));
 #endif
                     services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
-                    services.Configure<ServerRedirectorConfig>(
-                        host.Configuration.GetSection(ServerRedirectorConfig.Section));
+                    services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
 
                     if (redirector.Enabled)
                     {
@@ -142,7 +147,24 @@ namespace Impostor.Server
                     }
                     else
                     {
-                        services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+                        if (debug.GameRecorderEnabled)
+                        {
+                            services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+                            services.AddSingleton<ObjectPool<PacketSerializationContext>>(serviceProvider =>
+                            {
+                                var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+                                var policy = new PacketSerializationContextPooledObjectPolicy();
+                                return provider.Create(policy);
+                            });
+
+                            services.AddSingleton<PacketRecorder>();
+                            services.AddSingleton<IClientFactory, ClientFactory<ClientRecorder>>();
+                        }
+                        else
+                        {
+                            services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+                        }
+
                         services.AddSingleton<GameManager>();
                         services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
                     }
diff --git a/src/Impostor.Server/Recorder/ClientRecorder.cs b/src/Impostor.Server/Recorder/ClientRecorder.cs
new file mode 100644 (file)
index 0000000..a03be75
--- /dev/null
@@ -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<Client> 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 (file)
index 785e026..0000000
+++ /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 (file)
index 0000000..a949e58
--- /dev/null
@@ -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
+{
+    /// <summary>
+    ///     Records all packets received in <see cref="ClientRecorder.HandleMessageAsync"/>.
+    /// </summary>
+    internal class PacketRecorder : IDisposable
+    {
+        private readonly ILogger<PacketRecorder> _logger;
+        private readonly ObjectPool<PacketSerializationContext> _pool;
+        private readonly SemaphoreSlim _writerLock;
+        private readonly FileStream _writer;
+
+        public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> 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<byte> 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<byte> 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 (file)
index 0000000..07755f6
--- /dev/null
@@ -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 (file)
index 0000000..17b355f
--- /dev/null
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Server.Recorder
+{
+    public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy<PacketSerializationContext>
+    {
+        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 (file)
index 0000000..24e645e
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Impostor.Server.Recorder
+{
+    internal enum RecordedPacketType : byte
+    {
+        Message = 1,
+        Disconnect = 2,
+    }
+}
\ No newline at end of file
index d13e17b9c999356935a01a2ece38d157410bc9bc..e509ca039293de36f3251a32660cf082943e9e58 100644 (file)
@@ -20,6 +20,7 @@
     ]
   },
   "Debug": {
-    "EnableGameRecorder": true
+    "GameRecorderEnabled": true,
+    "GameRecorderPath": ""
   }
 }
\ No newline at end of file
index f969cbf41f21b73249454f3566203e7de4027b0b..f5f0ebe6909a4fabeb908187e2f0aafad81eb5b4 100644 (file)
@@ -4,5 +4,9 @@
     "PublicPort": 22023,
     "ListenIp": "0.0.0.0",
     "ListenPort": 22023
+  },
+  "Debug": {
+    "GameRecorderEnabled": true,
+    "GameRecorderPath": ""
   }
 }
\ No newline at end of file