diff --git a/Projects/Server/Buffers/CircularBuffer.cs b/Projects/Server/Buffers/CircularBuffer.cs index 540d23a0b..20a9d3a55 100644 --- a/Projects/Server/Buffers/CircularBuffer.cs +++ b/Projects/Server/Buffers/CircularBuffer.cs @@ -129,7 +129,7 @@ namespace System.Buffers public Span GetSpan(int index) { - if (index < 0 || index > 1) + if (index is < 0 or > 1) { throw new ArgumentOutOfRangeException(nameof(index)); } diff --git a/Projects/Server/Buffers/CircularBufferReader.cs b/Projects/Server/Buffers/CircularBufferReader.cs index 56619c6e0..40eeafc2f 100644 --- a/Projects/Server/Buffers/CircularBufferReader.cs +++ b/Projects/Server/Buffers/CircularBufferReader.cs @@ -32,6 +32,10 @@ namespace Server.Network public int Position { get; private set; } public int Remaining => Length - Position; + // Only used for debugging! + public ReadOnlySpan First => _first; + public ReadOnlySpan Second => _second; + public CircularBufferReader(ref CircularBuffer buffer) : this(buffer.GetSpan(0), buffer.GetSpan(1)) { } @@ -58,12 +62,9 @@ namespace Server.Network try { - using var sw = new StreamWriter("Packets.log", true); - + using var sw = new StreamWriter("unhandled-packets.log", true); sw.WriteLine("Client: {0}: Unhandled packet 0x{1:X2}", state, _first[0]); - - Utility.FormatBuffer(sw, _first.ToArray(), new Memory(_second.ToArray())); - + sw.FormatBuffer(_first, _second, Length); sw.WriteLine(); sw.WriteLine(); } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 47c6bf381..5c0f91e60 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -73,6 +73,7 @@ namespace Server.Network internal ParserState _parserState = ParserState.AwaitingNextPacket; internal ProtocolState _protocolState = ProtocolState.AwaitingSeed; internal GCHandle _handle; + private bool _packetLogging; internal enum ParserState { @@ -97,6 +98,13 @@ namespace Server.Network Error } + private static string _packetLoggingPath; + + public static void Configure() + { + _packetLoggingPath = ServerConfiguration.GetSetting("netstate.packetLoggingPath", Path.Combine(Core.BaseDirectory, "Packets")); + } + public static void Initialize() { Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive); @@ -142,6 +150,21 @@ namespace Server.Network CreatedCallback?.Invoke(this); } + // Only use this for debugging. This will make your server very slow! + public bool PacketLogging + { + get => _packetLogging; + set + { + _packetLogging = value; + + if (_packetLogging) + { + StartPacketLog(); + } + } + } + public DateTime ConnectedOn { get; } public TimeSpan ConnectedFor => Core.Now - ConnectedOn; @@ -486,6 +509,11 @@ namespace Server.Network buffer.CopyFrom(span); } + if (PacketLogging) + { + LogPacket(span, ReadOnlySpan.Empty, span.Length, false); + } + SendPipe.Writer.Advance((uint)length); if (!_flushQueued) @@ -506,6 +534,48 @@ namespace Server.Network } } + private void StartPacketLog() + { + try + { + var logDir = Path.Combine(_packetLoggingPath, _toString); + PathUtility.EnsureDirectory(logDir); + var logPath = Path.Combine(logDir, "packets.log"); + using var op = new StreamWriter(logPath, true); + + op.WriteLine(">>>>>>>>>> Logging started {0:yyyy/MM/dd HH:mm::ss} <<<<<<<<<<", Core.Now); + op.WriteLine(); + op.WriteLine(); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private void LogPacket(ReadOnlySpan first, ReadOnlySpan second, int totalLength, bool incoming) + { + try + { + var logDir = Path.Combine(_packetLoggingPath, _toString); + PathUtility.EnsureDirectory(logDir); + var logPath = Path.Combine(logDir, "packets.log"); + + const string incomingStr = "Client -> Server"; + const string outgoingStr = "Server -> Client"; + + using var sw = new StreamWriter(logPath, true); + sw.WriteLine($"{Core.Now:HH:mm:ss.ffff}: {(incoming ? incomingStr : outgoingStr)} 0x{first[0]:X2} (Length: {totalLength})"); + sw.FormatBuffer(first, second, totalLength); + sw.WriteLine(); + sw.WriteLine(); + } + catch + { + // ignored + } + } + public void HandleReceive() { if (!_running) @@ -760,6 +830,11 @@ namespace Server.Network UpdatePacketCount(packetId); + if (PacketLogging) + { + LogPacket(packetReader.First, packetReader.Second, packetLength, true); + } + handler.OnReceive(this, packetReader, ref packetLength); prof?.Finish(packetLength); diff --git a/Projects/Server/Text/HexStringConverter.cs b/Projects/Server/Text/HexStringConverter.cs index 28b786de3..257c73642 100644 --- a/Projects/Server/Text/HexStringConverter.cs +++ b/Projects/Server/Text/HexStringConverter.cs @@ -59,6 +59,27 @@ namespace Server.Text return result; } + public static unsafe int ToSpacedHexString(this ReadOnlySpan bytes, Span result) + { + var charsWritten = 0; + fixed (char* resultP = result) + { + for (int i = 0; i < bytes.Length; i++) + { + var resultP2 = (uint*)(resultP + charsWritten); + *resultP2 = m_Lookup32Chars[bytes[i]]; + charsWritten += 2; + + if (i < bytes.Length - 1) + { + *(resultP + charsWritten++) = ' '; + } + } + } + + return charsWritten; + } + public static string ToDelimitedHexString(this byte[] bytes) => ((ReadOnlySpan)bytes).ToDelimitedHexString(); public static unsafe string ToDelimitedHexString(this ReadOnlySpan bytes) diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 286e488e7..e7cd004fb 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -11,6 +11,7 @@ using System.Xml; using Microsoft.Toolkit.HighPerformance; using Server.Buffers; using Server.Random; +using Server.Text; namespace Server { @@ -632,207 +633,42 @@ namespace Server } } - public static void FormatBuffer(TextWriter output, Stream input, int length) + public static void FormatBuffer(this TextWriter op, ReadOnlySpan first, ReadOnlySpan second, int totalLength) { - output.WriteLine(" 0 1 2 3 4 5 6 7 8 9 A B C D E F"); - output.WriteLine(" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --"); + op.WriteLine(" 0 1 2 3 4 5 6 7 8 9 A B C D E F"); + op.WriteLine(" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --"); - var byteIndex = 0; - - var whole = length >> 4; - var rem = length & 0xF; - - for (var i = 0; i < whole; ++i, byteIndex += 16) + if (totalLength <= 0) { - var bytes = new StringBuilder(49); - var chars = new StringBuilder(16); - - for (var j = 0; j < 16; ++j) - { - var c = input.ReadByte(); - - bytes.Append(c.ToString("X2")); - - if (j != 7) - { - bytes.Append(' '); - } - else - { - bytes.Append(" "); - } - - if (c >= 0x20 && c < 0x7F) - { - chars.Append((char)c); - } - else - { - chars.Append('.'); - } - } - - output.Write(byteIndex.ToString("X4")); - output.Write(" "); - output.Write(bytes.ToString()); - output.Write(" "); - output.WriteLine(chars.ToString()); + op.WriteLine("0000 "); + return; } - if (rem != 0) + Span lineBytes = stackalloc byte[16]; + Span lineChars = stackalloc char[47]; + for (var i = 0; i < totalLength; i += 16) { - var bytes = new StringBuilder(49); - var chars = new StringBuilder(rem); - - for (var j = 0; j < 16; ++j) + var length = Math.Min(totalLength - i, 16); + if (i < first.Length) { - if (j < rem) + var firstLength = Math.Min(length, first.Length - i); + first.Slice(i, firstLength).CopyTo(lineBytes); + + if (firstLength < length) { - var c = input.ReadByte(); - - bytes.Append(c.ToString("X2")); - - if (j != 7) - { - bytes.Append(' '); - } - else - { - bytes.Append(" "); - } - - if (c >= 0x20 && c < 0x7F) - { - chars.Append((char)c); - } - else - { - chars.Append('.'); - } - } - else - { - bytes.Append(" "); + second[..(length - first.Length - i)].CopyTo(lineBytes[(length - firstLength)..]); } } - - output.Write(byteIndex.ToString("X4")); - output.Write(" "); - output.Write(bytes.ToString()); - output.Write(" "); - output.WriteLine(chars.ToString()); - } - } - - public static void FormatBuffer(TextWriter output, params Memory[] mems) - { - output.WriteLine(" 0 1 2 3 4 5 6 7 8 9 A B C D E F"); - output.WriteLine(" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --"); - - var byteIndex = 0; - - var length = 0; - for (var i = 0; i < mems.Length; i++) - { - length += mems[i].Length; - } - - var position = 0; - var memIndex = 0; - var span = mems[memIndex].Span; - - var whole = length >> 4; - var rem = length & 0xF; - - for (var i = 0; i < whole; ++i, byteIndex += 16) - { - var bytes = new StringBuilder(49); - var chars = new StringBuilder(16); - - for (var j = 0; j < 16; ++j) + else { - var c = span[position++]; - if (position > span.Length) - { - span = mems[memIndex++].Span; - position = 0; - } - - bytes.Append(c.ToString("X2")); - - if (j != 7) - { - bytes.Append(' '); - } - else - { - bytes.Append(" "); - } - - if (c >= 0x20 && c < 0x7F) - { - chars.Append((char)c); - } - else - { - chars.Append('.'); - } + second.Slice(i - first.Length, length).CopyTo(lineBytes); } - output.Write(byteIndex.ToString("X4")); - output.Write(" "); - output.Write(bytes.ToString()); - output.Write(" "); - output.WriteLine(chars.ToString()); - } + var charsWritten = ((ReadOnlySpan)lineBytes[..length]).ToSpacedHexString(lineChars); - if (rem != 0) - { - var bytes = new StringBuilder(49); - var chars = new StringBuilder(rem); - - for (var j = 0; j < 16; ++j) - { - if (j < rem) - { - var c = span[position++]; - if (position > span.Length) - { - span = mems[memIndex++].Span; - position = 0; - } - - bytes.Append(c.ToString("X2")); - - if (j != 7) - { - bytes.Append(' '); - } - else - { - bytes.Append(" "); - } - - if (c >= 0x20 && c < 0x7F) - { - chars.Append((char)c); - } - else - { - chars.Append('.'); - } - } - else - { - bytes.Append(" "); - } - } - - output.Write(byteIndex.ToString("X4")); - output.Write(" "); - output.Write(bytes.ToString()); - output.Write(" "); - output.WriteLine(chars.ToString()); + op.Write("{0:X4} ", i); + op.Write(lineChars[..charsWritten]); + op.WriteLine(); } } diff --git a/Projects/UOContent/Network/NetworkCommands.cs b/Projects/UOContent/Network/NetworkCommands.cs new file mode 100644 index 000000000..ce35f5af2 --- /dev/null +++ b/Projects/UOContent/Network/NetworkCommands.cs @@ -0,0 +1,60 @@ +using Server.Commands; +using Server.Commands.Generic; +using Server.Mobiles; + +namespace Server.Network +{ + public class PacketLoggingCommand : BaseCommand + { + public static void Initialize() + { + TargetCommands.Register(new PacketLoggingCommand()); + } + + public PacketLoggingCommand() + { + AccessLevel = AccessLevel.Developer; + Commands = new[] { "PacketLogging" }; + ObjectTypes = ObjectTypes.Mobiles; + Supports = CommandSupport.AllMobiles; + Usage = "PacketLogging "; + Description = "Enables or disables packet logging for a particular user until they disconnect."; + } + + public override void Execute(CommandEventArgs e, object targeted) + { + if (e.Arguments.Length == 0) + { + LogFailure("Format: PacketLogging "); + return; + } + + var from = e.Mobile; + var enable = Utility.ToBoolean(e.Arguments[0]); + + if (targeted is not PlayerMobile pm) + { + LogFailure("That is not a player."); + } + else if (pm.NetState == null) + { + LogFailure("The player is not connected."); + } + else if (from != pm && from.AccessLevel < pm.AccessLevel) + { + LogFailure("You do not have the required access level to do this."); + } + else + { + pm.NetState.PacketLogging = enable; + var enabled = enable ? "enabled" : "disabled"; + CommandLogging.WriteLine( + from, + $"{from.AccessLevel} {CommandLogging.Format(from)} {enabled} packet logging for {pm.Account.Username} ({pm.NetState})" + ); + + AddResponse($"Packet logging has been {enabled} for {pm.Account.Username} ({pm.NetState})"); + } + } + } +}