diff options
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Recorder')
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 |