summaryrefslogtreecommitdiff
path: root/Impostor-dev/src/Impostor.Server/Recorder
diff options
context:
space:
mode:
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Recorder')
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs86
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs201
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs56
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs18
-rw-r--r--Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs10
5 files changed, 371 insertions, 0 deletions
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs
new file mode 100644
index 0000000..5763c70
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs
@@ -0,0 +1,86 @@
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Config;
+using Impostor.Server.Net;
+using Impostor.Server.Net.Hazel;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Recorder
+{
+ internal class ClientRecorder : Client
+ {
+ private readonly PacketRecorder _recorder;
+ private bool _isFirst;
+ private bool _createdGame;
+ private bool _recordAfter;
+
+ public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder)
+ : base(logger, antiCheatOptions, clientManager, gameManager, name, connection)
+ {
+ _recorder = recorder;
+ _isFirst = true;
+ _createdGame = false;
+ _recordAfter = true;
+ }
+
+ public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType)
+ {
+ using var messageCopy = reader.Copy();
+
+ // Trigger connect event.
+ if (_isFirst)
+ {
+ _isFirst = false;
+
+ await _recorder.WriteConnectAsync(this);
+ }
+
+ // Check if we were in-game before handling the message.
+ var inGame = Player?.Game != null;
+
+ if (!_recordAfter)
+ {
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+
+ // Handle the message.
+ await base.HandleMessageAsync(reader, messageType);
+
+ // Player created a game.
+ if (reader.Tag == MessageFlags.HostGame)
+ {
+ _createdGame = true;
+ }
+ else if (reader.Tag == MessageFlags.JoinGame && _createdGame)
+ {
+ _createdGame = false;
+
+ // We created a game and are now in-game, store that event.
+ if (!inGame && Player?.Game != null)
+ {
+ await _recorder.WriteGameCreatedAsync(this, Player.Game.Code);
+ }
+
+ _recordAfter = false;
+
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+
+ if (_recordAfter)
+ {
+ // Trigger message event.
+ await _recorder.WriteMessageAsync(this, messageCopy, messageType);
+ }
+ }
+
+ public override async ValueTask HandleDisconnectAsync(string reason)
+ {
+ await _recorder.WriteDisconnectAsync(this, reason);
+ await base.HandleDisconnectAsync(reason);
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs
new file mode 100644
index 0000000..af2ea4f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs
@@ -0,0 +1,201 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Impostor.Api.Games;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Config;
+using Impostor.Server.Net;
+using Microsoft.Extensions.Hosting;
+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 : BackgroundService
+ {
+ private readonly string _path;
+ private readonly ILogger<PacketRecorder> _logger;
+ private readonly ObjectPool<PacketSerializationContext> _pool;
+ private readonly Channel<byte[]> _channel;
+
+ public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool)
+ {
+ var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat";
+
+ _path = Path.Combine(options.Value.GameRecorderPath, name);
+ _logger = logger;
+ _pool = pool;
+
+ _channel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", _path);
+
+ var writer = File.Open(_path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+
+ // Handle messages.
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var result = await _channel.Reader.ReadAsync(stoppingToken);
+
+ await writer.WriteAsync(result, stoppingToken);
+ await writer.FlushAsync(stoppingToken);
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ }
+
+ // Clean up.
+ await writer.DisposeAsync();
+ }
+
+ public async Task WriteConnectAsync(ClientRecorder client)
+ {
+ _logger.LogTrace("Writing Connect.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Connect);
+ WriteClient(context, client, true);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteDisconnectAsync(ClientRecorder client, string reason)
+ {
+ _logger.LogTrace("Writing Disconnect.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Disconnect);
+ WriteClient(context, client, false);
+ context.Writer.Write(reason);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteMessageAsync(ClientRecorder client, IMessageReader reader, MessageType messageType)
+ {
+ _logger.LogTrace("Writing Message.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Message);
+ WriteClient(context, client, false);
+ WritePacket(context, reader, messageType);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteGameCreatedAsync(ClientRecorder client, GameCode gameCode)
+ {
+ _logger.LogTrace("Writing GameCreated {0}.", gameCode);
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.GameCreated);
+ WriteClient(context, client, false);
+ WriteGameCode(context, gameCode);
+ 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, bool full)
+ {
+ var address = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345);
+ var addressBytes = address.Address.GetAddressBytes();
+
+ context.Writer.Write(client.Id);
+
+ if (full)
+ {
+ context.Writer.Write((byte) addressBytes.Length);
+ context.Writer.Write(addressBytes);
+ context.Writer.Write((ushort) address.Port);
+ context.Writer.Write(client.Name);
+ }
+ }
+
+ private static void WritePacket(PacketSerializationContext context, IMessageReader reader, MessageType messageType)
+ {
+ context.Writer.Write((byte) messageType);
+ context.Writer.Write((byte) reader.Tag);
+ context.Writer.Write((int) reader.Length);
+ context.Writer.Write(reader.Buffer, reader.Offset, reader.Length);
+ }
+
+ private static void WriteGameCode(PacketSerializationContext context, in GameCode gameCode)
+ {
+ context.Writer.Write(gameCode.Code);
+ }
+
+ 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(MemoryStream data)
+ {
+ await _channel.Writer.WriteAsync(data.ToArray());
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs
new file mode 100644
index 0000000..07755f6
--- /dev/null
+++ b/Impostor-dev/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/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs
new file mode 100644
index 0000000..17b355f
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs
@@ -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/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs
new file mode 100644
index 0000000..a8a20bc
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs
@@ -0,0 +1,10 @@
+namespace Impostor.Server.Recorder
+{
+ internal enum RecordedPacketType : byte
+ {
+ Connect = 1,
+ Disconnect = 2,
+ Message = 3,
+ GameCreated = 4
+ }
+} \ No newline at end of file