diff --git a/Projects/Server.Tests/Tests/Network/PipeTests.cs b/Projects/Server.Tests/Tests/Network/PipeTests.cs index 1885fa166..ab4556dc6 100644 --- a/Projects/Server.Tests/Tests/Network/PipeTests.cs +++ b/Projects/Server.Tests/Tests/Network/PipeTests.cs @@ -6,19 +6,20 @@ using Xunit; namespace Server.Tests.Network { + [Collection("Sequential Tests")] public class PipeTests { private async void DelayedExecute(Action action) { - await Task.Delay(5).ConfigureAwait(false); + await Task.Delay(5); action(); } [Fact] - public async void Await() + public async void AwaitRead() { - var pipe = new Pipe(new byte[100]); + var pipe = new Pipe(new byte[128]); var reader = pipe.Reader; var writer = pipe.Writer; @@ -26,20 +27,47 @@ namespace Server.Tests.Network DelayedExecute(() => { // Write some data into the pipe - var r = writer.TryGetMemory(); - Assert.True(r.Buffer[0].Count == 99); - r.Buffer[0][0] = 0x1; - r.Buffer[0][1] = 0x2; - r.Buffer[0][2] = 0x3; + var buffer = writer.TryGetMemory(); + Assert.Equal(127, buffer.Length); + + buffer.CopyFrom(new byte[] { 1 }); + buffer.CopyFrom(new byte[] { 2 }); + buffer.CopyFrom(new byte[] { 3 }); writer.Advance(3); writer.Flush(); }); var result = await reader.Read(); + Assert.Equal(3, result.Buffer[0].Count); } + [Fact] + public async void AwaitWrite() + { + var pipe = new Pipe(new byte[128]); + + var reader = pipe.Reader; + var writer = pipe.Writer; + + // Fill the entire pipe up + writer.Advance(127); + + DelayedExecute(() => + { + // Read some data from the pipe + var buffer = reader.TryRead(); + Assert.Equal(127, buffer.Length); + + reader.Advance(50); + reader.Commit(); + }); + + var result = await writer.GetMemory(); + Assert.Equal(50, result.Length); + } + private bool _signal; private void Consumer(object state) @@ -53,11 +81,9 @@ namespace Server.Tests.Network { var result = reader.TryRead(); - var first = result.Buffer[0]; - - for (int i = 0; i < first.Count; i++) + for (int i = 0; i < result.Buffer[0].Count; i++) { - Assert.True(first[i] == expected_value); + Assert.Equal(expected_value, result.Buffer[0][i]); count++; if (count == 0x1000) @@ -66,10 +92,9 @@ namespace Server.Tests.Network } } - var second = result.Buffer[1]; - for (int i = 0; i < second.Count; i++) + for (int i = 0; i < result.Buffer[1].Count; i++) { - Assert.True(second[i] == expected_value); + Assert.Equal(expected_value, result.Buffer[1][i]); count++; if (count == 0x1000) @@ -85,9 +110,9 @@ namespace Server.Tests.Network } [Fact] - public void Threading() + public async void Threading() { - var pipe = new Pipe(new byte[0x1001]); + var pipe = new Pipe(new byte[0x1000]); ThreadPool.UnsafeQueueUserWorkItem(Consumer, pipe); @@ -105,12 +130,8 @@ namespace Server.Tests.Network continue; } - result.CopyFrom(new[] { - expected_value, expected_value, expected_value, expected_value, - expected_value, expected_value, expected_value, expected_value, - expected_value, expected_value, expected_value, expected_value, - expected_value, expected_value, expected_value, expected_value - }); + result.CopyFrom(new[] { expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, + expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value }); writer.Advance(16); count += 16; @@ -129,54 +150,54 @@ namespace Server.Tests.Network [Fact] public void Wrap() { - var pipe = new Pipe(new byte[10]); + var pipe = new Pipe(new byte[16]); var reader = pipe.Reader; var writer = pipe.Writer; var result = writer.TryGetMemory(); - - Assert.Equal(9, result.Length); + Assert.Equal(15, result.Length); Assert.Equal(0u, reader.GetAvailable()); result = reader.TryRead(); Assert.Equal(0, result.Length); writer.Advance(7); result = writer.TryGetMemory(); - Assert.Equal(2, result.Length); + Assert.Equal(8, result.Length); Assert.Equal(7u, reader.GetAvailable()); result = reader.TryRead(); Assert.Equal(7, result.Length); reader.Advance(4); result = writer.TryGetMemory(); - Assert.Equal(6, result.Length); + Assert.Equal(12, result.Length); Assert.Equal(3u, reader.GetAvailable()); result = reader.TryRead(); Assert.Equal(3, result.Length); writer.Advance(3); result = writer.TryGetMemory(); - Assert.Equal(3, result.Length); + Assert.Equal(9, result.Length); Assert.Equal(6u, reader.GetAvailable()); result = reader.TryRead(); Assert.Equal(6, result.Length); + } [Fact] public void Match() { - var pipe = new Pipe(new byte[10]); + var pipe = new Pipe(new byte[16]); var reader = pipe.Reader; var writer = pipe.Writer; - for (uint i = 0; i < 9; i++) + for (uint i = 0; i < 16; i++) { writer.Advance(i); writer.Flush(); - Assert.True(reader.GetAvailable() == i); + Assert.Equal(i, reader.GetAvailable()); reader.Advance(i); } } @@ -184,39 +205,36 @@ namespace Server.Tests.Network [Fact] public void Sequence() { - var pipe = new Pipe(new byte[10]); + var pipe = new Pipe(new byte[16]); var reader = pipe.Reader; var writer = pipe.Writer; - var result = writer.TryGetMemory(); - Assert.Equal(9, result.Length); + var buffer = writer.TryGetMemory(); + Assert.Equal(15, buffer.Length); - result.CopyFrom(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); + buffer.CopyFrom(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); writer.Advance(9); writer.Flush(); Assert.Equal(9u, reader.GetAvailable()); - result = reader.TryRead(); - - var first = result.Buffer[0]; + buffer = reader.TryRead(); for (int i = 0; i < 9; i++) { - Assert.Equal(i, first[i]); + Assert.Equal(i, buffer.Buffer[0][i]); } reader.Advance(4); - result = reader.TryRead(); - Assert.Equal(5, result.Length); - first = result.Buffer[0]; - Assert.Equal(4, first[0]); - Assert.Equal(5, first[1]); - Assert.Equal(6, first[2]); - Assert.Equal(7, first[3]); - Assert.Equal(8, first[4]); + buffer = reader.TryRead(); + Assert.Equal(5, buffer.Length); + Assert.Equal(4, buffer.Buffer[0][0]); + Assert.Equal(5, buffer.Buffer[0][1]); + Assert.Equal(6, buffer.Buffer[0][2]); + Assert.Equal(7, buffer.Buffer[0][3]); + Assert.Equal(8, buffer.Buffer[0][4]); } } } diff --git a/Projects/Server/Diagnostics/PacketProfile.cs b/Projects/Server/Diagnostics/PacketProfile.cs index a71869299..80e13969a 100644 --- a/Projects/Server/Diagnostics/PacketProfile.cs +++ b/Projects/Server/Diagnostics/PacketProfile.cs @@ -33,22 +33,22 @@ namespace Server.Diagnostics public class PacketSendProfile : BasePacketProfile { - private static readonly Dictionary _profiles = new(); + private static readonly Dictionary _profiles = new(); private long _created; - public PacketSendProfile(Type type) : base(type.FullName) + public PacketSendProfile(int packetId) : base($"0x{packetId:X2}") { } public static IEnumerable Profiles => _profiles.Values; [MethodImpl(MethodImplOptions.Synchronized)] - public static PacketSendProfile Acquire(Type type) + public static PacketSendProfile Acquire(int packetId) { - if (!_profiles.TryGetValue(type, out var prof)) + if (!_profiles.TryGetValue(packetId, out var prof)) { - _profiles.Add(type, prof = new PacketSendProfile(type)); + _profiles.Add(packetId, prof = new PacketSendProfile(packetId)); } return prof; @@ -72,8 +72,7 @@ namespace Server.Diagnostics private static readonly Dictionary _profiles = new(); - public PacketReceiveProfile(int packetId) - : base($"0x{packetId:X2}") + public PacketReceiveProfile(int packetId) : base($"0x{packetId:X2}") { } diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 4dd2c62a6..90064f114 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -489,7 +489,7 @@ namespace Server // Handle networking events += TcpServer.Slice(); events += NetState.HandleAllReceives(); - events += NetState.FlushAll(); + events += NetState.Slice(); // Execute captured post-await methods (like Timer.Pause) events += _eventLoopContext.ExecuteTasks(); diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 84122ae29..6c2d7d829 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -2711,7 +2711,7 @@ namespace Server if (m_NetState != null) { m_NetState.CancelAllTrades(); - m_NetState.Disconnect(); + m_NetState.Disconnect($"Player {this} has been deleted."); } DropHolding(); diff --git a/Projects/Server/Network/ISocket.cs b/Projects/Server/Network/ISocket.cs index 866e546c6..b1182c77a 100644 --- a/Projects/Server/Network/ISocket.cs +++ b/Projects/Server/Network/ISocket.cs @@ -29,6 +29,8 @@ namespace Server.Network public Task SendAsync(IList> buffer, SocketFlags flags); + public int Send(IList> buffer, SocketFlags flags); + public Task ReceiveAsync(IList> buffer, SocketFlags flags); public void Shutdown(SocketShutdown how); diff --git a/Projects/Server/Network/NetState/DumpNetStates.cs b/Projects/Server/Network/NetState/DumpNetStates.cs new file mode 100644 index 000000000..ad0e3a821 --- /dev/null +++ b/Projects/Server/Network/NetState/DumpNetStates.cs @@ -0,0 +1,39 @@ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019-2021 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: DumpNetStates.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.IO; + +namespace Server.Network +{ + public static class DumpNetStates + { + public static void Initialize() + { + CommandSystem.Register("DumpNetStates", AccessLevel.Administrator, DumpNetStatesCommand); + } + + public static void DumpNetStatesCommand(CommandEventArgs args) + { + using var file = new StreamWriter("netstatedump.csv"); + + file.WriteLine("NetState, RecvTask, SendTask, ProtocolState, ParserState"); + + foreach (var ns in TcpServer.Instances) + { + file.WriteLine($"{ns}, {ns._recvState}, {ns._sendState}, {ns._protocolState}, {ns._parserState}"); + } + } + } +} diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 5c011641f..62fc7a181 100644 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -23,6 +23,7 @@ using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Threading; using Server.Accounting; +using Server.Diagnostics; using Server.Gumps; using Server.HuePickers; using Server.Items; @@ -37,57 +38,102 @@ namespace Server.Network public partial class NetState : IComparable { - private static int RecvPipeSize = 1024 * 64 + 1; - private static int SendPipeSize = 1024 * 256 + 1; + private const int RecvPipeSize = 1024 * 64; + private const int SendPipeSize = 1024 * 256; private static int GumpCap = 512; private static int HuePickerCap = 512; private static int MenuCap = 512; + private static int PacketPerSecondThreshold = 3000; private static readonly Queue FlushPending = new(2048); private static readonly ConcurrentQueue Disposed = new(); - private static NetworkState NetworkState = NetworkState.ResumeState; public static NetStateCreatedCallback CreatedCallback { get; set; } private readonly string _toString; private ClientVersion _version; private long _nextActivityCheck; - private volatile bool _running; + private int _running; private volatile DecodePacket _packetDecoder; private volatile EncodePacket _packetEncoder; private bool _flushQueued; + private readonly long[] _packetThrottles = new long[0x100]; + private readonly long[] _packetCounts = new long[0x100]; + private string _disconnectReason = string.Empty; internal int _authId; internal int _seed; + internal ParserState _parserState = ParserState.Uninitialized; + internal ProtocolState _protocolState = ProtocolState.Uninitialized; + internal RecvState _recvState = RecvState.Uninitialized; + internal SendState _sendState = SendState.Uninitialized; + + internal enum ParserState + { + Uninitialized, + AwaitingNextPacket, + AwaitingPartialPacket, + ProcessingPacket, + Throttled, + Error + } + + internal enum ProtocolState + { + Uninitialized, + AwaitingSeed, // Based on the way the seed arrives, we know if this is a login server or a game server connection + + LoginServer_AwaitingLogin, + LoginServer_AwaitingServerSelect, + LoginServer_ServerSelectAck, + + GameServer_AwaitingGameServerLogin, + GameServer_LoggedIn, + + Error + } + + internal enum RecvState + { + Uninitialized, + AwaitingMemory, + AwaitingRecv, + DataReceived, + Exited, + } + + internal enum SendState + { + Uninitialized, + AwaitingData, + Sending, + SendCompleted, + Exited, + } public static void Configure() { - RecvPipeSize = ServerConfiguration.GetOrUpdateSetting("netstate.recvPipeSize", RecvPipeSize); - SendPipeSize = ServerConfiguration.GetOrUpdateSetting("netstate.sendPipeSize", SendPipeSize); GumpCap = ServerConfiguration.GetOrUpdateSetting("netstate.gumpCap", GumpCap); HuePickerCap = ServerConfiguration.GetOrUpdateSetting("netstate.huePickerCap", HuePickerCap); MenuCap = ServerConfiguration.GetOrUpdateSetting("netstate.menuCap", MenuCap); + PacketPerSecondThreshold = ServerConfiguration.GetOrUpdateSetting("netstate.packetsPerSecondThreshold", PacketPerSecondThreshold); } public static void Initialize() { - var checkAliveDuration = TimeSpan.FromMinutes(1.5); - Timer.DelayCall(checkAliveDuration, checkAliveDuration, CheckAllAlive); + Timer.DelayCall(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1.5), CheckAllAlive); } public NetState(ISocket connection) { - _running = false; Connection = connection; Seeded = false; Gumps = new List(); HuePickers = new List(); Menus = new List(); Trades = new List(); - var recvBuffer = GC.AllocateUninitializedArray(RecvPipeSize); - RecvPipe = new Pipe(recvBuffer); - var sendBuffer = GC.AllocateUninitializedArray(SendPipeSize); - SendPipe = new Pipe(sendBuffer); + RecvPipe = new Pipe(GC.AllocateUninitializedArray(RecvPipeSize)); + SendPipe = new Pipe(GC.AllocateUninitializedArray(SendPipeSize)); _nextActivityCheck = Core.TickCount + 30000; try @@ -111,8 +157,6 @@ namespace Server.Network public TimeSpan ConnectedFor => DateTime.UtcNow - ConnectedOn; - public DateTime ThrottledUntil { get; set; } - public IPAddress Address { get; } public DecodePacket PacketDecoder @@ -133,7 +177,11 @@ namespace Server.Network public List Trades { get; } - public bool Running => _running; + public bool Running + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _running == 1; + } public bool Seeded { get; set; } @@ -163,6 +211,52 @@ namespace Server.Network public int CompareTo(NetState other) => string.CompareOrdinal(_toString, other?._toString); + private void SetPacketTime(int packetID) + { + if (packetID < 0 || packetID >= 0x100) + { + return; + } + + _packetThrottles[packetID] = Core.TickCount; + } + + public long GetPacketDelay(int packetID) + { + if (packetID < 0 || packetID >= 0x100) + { + return 0; + } + + return _packetThrottles[packetID]; + } + + private void UpdatePacketCount(int packetID) + { + if (packetID < 0 || packetID >= 0x100) + { + return; + } + + _packetCounts[packetID]++; + } + + public int CheckPacketCounts() + { + for (int i = 0; i < _packetCounts.Length; i++) + { + long count = _packetCounts[i]; + _packetCounts[i] = 0; + + if (count > PacketPerSecondThreshold) + { + return i; + } + } + + return 0; + } + public void ValidateAllTrades() { for (var i = Trades.Count - 1; i >= 0; --i) @@ -270,7 +364,7 @@ namespace Server.Network else { WriteConsole("Exceeded menu cap, disconnecting..."); - Disconnect(); + Disconnect("Exceeded menu cap."); } } @@ -300,7 +394,7 @@ namespace Server.Network else { WriteConsole("Exceeded hue picker cap, disconnecting..."); - Disconnect(); + Disconnect("Exceeded hue picker cap."); } } @@ -330,7 +424,7 @@ namespace Server.Network else { WriteConsole("Exceeded gump cap, disconnecting..."); - Disconnect(); + Disconnect("Exceeded gump cap."); } } @@ -357,16 +451,6 @@ namespace Server.Network public override string ToString() => _toString; - public static void Pause() - { - NetworkState.Pause(ref NetworkState); - } - - public static void Resume() - { - NetworkState.Resume(ref NetworkState); - } - public bool GetSendBuffer(out CircularBuffer cBuffer) { var result = SendPipe.Writer.TryGetMemory(); @@ -377,19 +461,27 @@ namespace Server.Network public void Send(ReadOnlySpan span) { - if (span == null) + if (span == null || Connection == null || BlockAllPackets) { return; } var length = span.Length; - if (Connection == null || BlockAllPackets || length <= 0 || !GetSendBuffer(out var buffer)) + if (length <= 0 || !GetSendBuffer(out var buffer)) { return; } try { + PacketSendProfile prof = null; + + if (Core.Profiling) + { + prof = PacketSendProfile.Acquire(span[0]); + prof.Start(); + } + if (_packetEncoder != null) { _packetEncoder(span, buffer, out length); @@ -406,14 +498,16 @@ namespace Server.Network FlushPending.Enqueue(this); _flushQueued = true; } + + prof?.Finish(); } catch (Exception ex) { #if DEBUG Console.WriteLine(ex); - TraceException(ex); #endif - Disconnect(); + TraceException(ex); + Disconnect("Exception while sending."); } } @@ -431,74 +525,359 @@ namespace Server.Network { byte[] buffer = p.Compile(CompressionEnabled, out var length); - if (buffer.Length > 0 && length > 0) + if (buffer == null) { - var result = writer.TryGetMemory(); - if (result.IsClosed) + WriteConsole("null buffer send, disconnecting...", this); + using (StreamWriter op = new StreamWriter("null_send.log", true)) { - p.OnSend(); - return; + op.WriteLine("{0} Client: {1}: null buffer send, disconnecting...", DateTime.UtcNow, this); + op.WriteLine(new System.Diagnostics.StackTrace()); } - if (result.Length >= length) - { - result.CopyFrom(buffer.AsSpan(0, length)); - writer.Advance((uint)length); + Disconnect("Attempted to send null packet buffer."); + return; + } - if (!_flushQueued) - { - FlushPending.Enqueue(this); - _flushQueued = true; - } - } - else + if (buffer.Length <= 0 || length <= 0) + { + p.OnSend(); + return; + } + + PacketSendProfile prof = null; + + if (Core.Profiling) + { + prof = PacketSendProfile.Acquire(p.PacketID); + prof.Start(); + } + + var result = writer.TryGetMemory(); + + if (result.Length >= length) + { + result.CopyFrom(buffer.AsSpan(0, length)); + writer.Advance((uint)length); + + if (!_flushQueued) { - WriteConsole("Too much data pending, disconnecting..."); - Disconnect(); + FlushPending.Enqueue(this); + _flushQueued = true; } } else { - WriteConsole("Didn't write anything!"); + WriteConsole("Too much data pending, disconnecting..."); + Disconnect("Too much data pending."); } + + prof?.Finish(length); } catch (Exception ex) { #if DEBUG Console.WriteLine(ex); - TraceException(ex); #endif - Disconnect(); - } - finally - { - p.OnSend(); + TraceException(ex); + Disconnect("Exception while sending."); } } internal void Start() { - if (Connection == null || _running) + if (Interlocked.CompareExchange(ref _running, 1, 0) == 1 || Connection == null) { return; } - _running = true; + _parserState = ParserState.AwaitingNextPacket; + _protocolState = ProtocolState.AwaitingSeed; ThreadPool.UnsafeQueueUserWorkItem(RecvTask, null); ThreadPool.UnsafeQueueUserWorkItem(SendTask, null); } + // Return true if there was any data to be processed. False otherwise. Used for idle detection. + public bool HandleReceive() + { + if (!Running) + { + return false; + } + + bool active = false; + + var reader = RecvPipe.Reader; + + try + { + // Process as many packets as we can synchronously + while (Running && _parserState != ParserState.Error && _protocolState != ProtocolState.Error) + { + var result = reader.TryRead(); + var length = result.Length; + + if (length <= 0) + { + break; + } + + // There was at least some data found, so it's not idle. + active = true; + + var packetReader = new CircularBufferReader(result.Buffer); + var packetId = packetReader.ReadByte(); + int packetLength = length; + + // These can arrive at any time and are only informational + if (IncomingPackets.IsInfoPacket(packetId)) + { + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + } + else + { + switch (_protocolState) + { + case ProtocolState.Uninitialized: + { + HandleError(packetId, packetLength); + return true; + } + + case ProtocolState.AwaitingSeed: + { + if (packetId == 0xEF) + { + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + if (_parserState == ParserState.AwaitingNextPacket) + { + _protocolState = ProtocolState.LoginServer_AwaitingLogin; + } + } + else if (length >= 4) + { + int seed = (packetId << 24) | (packetReader.ReadByte() << 16) | (packetReader.ReadByte() << 8) | packetReader.ReadByte(); + + if (seed == 0) + { + HandleError(0, 0); + return true; + } + + _seed = seed; + packetLength = 4; + + _parserState = ParserState.AwaitingNextPacket; + _protocolState = ProtocolState.GameServer_AwaitingGameServerLogin; + } + else + { + _parserState = ParserState.AwaitingPartialPacket; + } + break; + } + + case ProtocolState.LoginServer_AwaitingLogin: + { + if (packetId != 0xCF && packetId != 0x80) + { + WriteConsole("Possible encrypted client detected, disconnecting..."); + HandleError(packetId, packetLength); + return true; + } + + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + if (_parserState == ParserState.AwaitingNextPacket) + { + _protocolState = ProtocolState.LoginServer_AwaitingServerSelect; + } + break; + } + + case ProtocolState.LoginServer_AwaitingServerSelect: + { + if (packetId != 0xA0) + { + HandleError(packetId, packetLength); + return true; + } + + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + if (_parserState == ParserState.AwaitingNextPacket) + { + _protocolState = ProtocolState.LoginServer_ServerSelectAck; + Disconnect(string.Empty); + } + break; + } + + case ProtocolState.LoginServer_ServerSelectAck: + { + // The code should never arrive here unless a packet is sent after server select. + HandleError(packetId, packetLength); + return true; + } + + case ProtocolState.GameServer_AwaitingGameServerLogin: + { + if (packetId != 0x91) + { + HandleError(packetId, packetLength); + return true; + } + + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + if (_parserState == ParserState.AwaitingNextPacket) + { + _protocolState = ProtocolState.GameServer_LoggedIn; + } + break; + } + + case ProtocolState.GameServer_LoggedIn: + { + _parserState = ParserState.ProcessingPacket; + _parserState = HandlePacket(packetReader, packetId, length, out packetLength); + break; + } + } + } + + if (_parserState == ParserState.AwaitingNextPacket) + { + reader.Advance((uint)packetLength); + } + else if (_parserState == ParserState.AwaitingPartialPacket || _parserState == ParserState.Throttled) + { + break; + } + else + { + HandleError(packetId, packetLength); + break; + } + } + + reader.Commit(); + } + catch (Exception ex) + { +#if DEBUG + Console.WriteLine(ex); +#endif + TraceException(ex); + Disconnect("Exception during HandleReceive"); + } + + return active; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void HandleError(byte packetId, int packetLength) + { + var msg = + $"{this} entered bad state on packet 0x{packetId:X2} with length {packetLength} while in protocol state {_protocolState} and parser state {_parserState}"; + Disconnect(msg); + _parserState = ParserState.Error; + _protocolState = ProtocolState.Error; + } + + /* + * length is the total buffer length. We might be able to use packetReader.Capacity() instead. + * packetLength is the length of the packet that this function actually found. + */ + private ParserState HandlePacket(CircularBufferReader packetReader, byte packetId, int length, out int packetLength) + { + PacketHandler handler = GetHandler(packetId); + if (handler == null) + { + WriteConsole($"received unknown packet 0x{packetId:X2} while in state {_protocolState}"); + packetLength = length; + return ParserState.Error; + } + + packetLength = handler.Length; + if (packetLength <= 0) + { + // Variable length packet. See if we have pulled in the length. + if (length < 3) + { + return ParserState.AwaitingPartialPacket; + } + + packetLength = packetReader.ReadUInt16(); + if (packetLength < 3) + { + return ParserState.Error; + } + } + + // Not enough data, let's wait for more to come in + if (length < packetLength) + { + return ParserState.AwaitingPartialPacket; + } + + if (handler.Ingame) + { + if (Mobile == null) + { + WriteConsole($"received packet 0x{packetId:X2} before having been attached to a mobile"); + return ParserState.Error; + } + + if (Mobile.Deleted) + { + return ParserState.Error; + } + } + + ThrottlePacketCallback throttler = handler.ThrottleCallback; + if (throttler != null) + { + if (!throttler(packetId, this, out bool drop)) + { + return drop ? ParserState.AwaitingNextPacket : ParserState.Throttled; + } + + SetPacketTime(packetId); + } + + PacketReceiveProfile prof = null; + + if (Core.Profiling) + { + prof = PacketReceiveProfile.Acquire(packetId); + prof?.Start(); + } + + UpdatePacketCount(packetId); + + handler.OnReceive(this, packetReader, ref packetLength); + + prof?.Finish(packetLength); + + return ParserState.AwaitingNextPacket; + } + private async void SendTask(object state) { var reader = SendPipe.Reader; try { - while (_running) + while (Running) { + _sendState = SendState.AwaitingData; + var result = await reader.Read(); - if (result.IsClosed) + + if (result.IsClosed || !Running) { break; } @@ -508,7 +887,11 @@ namespace Server.Network continue; } - var bytesWritten = await Connection.SendAsync(result.Buffer, SocketFlags.None).ConfigureAwait(false); + _sendState = SendState.Sending; + + var bytesWritten = await Connection.SendAsync(result.Buffer, SocketFlags.None); + + _sendState = SendState.SendCompleted; if (bytesWritten > 0) { @@ -516,17 +899,48 @@ namespace Server.Network reader.Advance((uint)bytesWritten); } } + + // Grab any remaining data and flush it + var data = reader.TryRead(); + + if (data.Length > 0) + { + _sendState = SendState.Sending; + Connection.Send(data.Buffer, SocketFlags.None); + + reader.Advance((uint)data.Length); + } + } + catch (SocketException ex) + { + // If the user closes the connection (or the recv side does) + // between the check for m_Running above and the call to SendAsync, + // we can still get a socket exception here. That's ok. +#if DEBUG + Console.WriteLine(ex); +#endif } catch (Exception ex) { #if DEBUG Console.WriteLine(ex); - TraceException(ex); #endif + TraceException(ex); } finally { - Disconnect(); + try + { + Connection.Shutdown(SocketShutdown.Both); + Connection.Close(); + } + catch (Exception ex) + { + TraceException(ex); + } + + Disconnect("Exiting SendTask."); + _sendState = SendState.Exited; } } @@ -543,16 +957,12 @@ namespace Server.Network try { - while (_running) + while (Running) { - if (NetworkState == NetworkState.PauseState) - { - continue; - } - + _recvState = RecvState.AwaitingMemory; var result = await writer.GetMemory(); - if (result.IsClosed) + if (result.IsClosed || !Running) { break; } @@ -562,12 +972,16 @@ namespace Server.Network continue; } - var bytesWritten = await socket.ReceiveAsync(result.Buffer, SocketFlags.None).ConfigureAwait(false); + _recvState = RecvState.AwaitingRecv; + + var bytesWritten = await socket.ReceiveAsync(result.Buffer, SocketFlags.None); if (bytesWritten <= 0) { break; } + _recvState = RecvState.DataReceived; + DecodePacket(result.Buffer, ref bytesWritten); writer.Advance((uint)bytesWritten); @@ -575,17 +989,27 @@ namespace Server.Network // No need to flush } + + Disconnect(string.Empty); + } + catch (SocketException ex) + { +#if DEBUG + Console.WriteLine(ex); +#endif + Disconnect(string.Empty); } catch (Exception ex) { #if DEBUG Console.WriteLine(ex); - TraceException(ex); #endif + Disconnect("RecvTask exited unexpectedly."); + TraceException(ex); } finally { - Disconnect(); + _recvState = RecvState.Exited; } } @@ -604,56 +1028,6 @@ namespace Server.Network return count; } - public bool HandleReceive() - { - if (Connection == null || !_running) - { - return false; - } - - try - { - var reader = RecvPipe.Reader; - - // Process as many packets as we can synchronously - while (true) - { - var result = reader.TryRead(); - - if (result.IsClosed || result.Length <= 0) - { - return false; - } - - var bytesProcessed = this.ProcessPacket(result.Buffer); - - if (bytesProcessed <= 0) - { - // Error - // TODO: Throw exception instead? - if (bytesProcessed < 0) - { - Disconnect(); - return false; - } - - return true; - } - - reader.Advance((uint)bytesProcessed); - } - } - catch (Exception ex) - { -#if DEBUG - Console.WriteLine(ex); - TraceException(ex); -#endif - Disconnect(); - return false; - } - } - public void Flush() { if (Connection != null) @@ -664,12 +1038,20 @@ namespace Server.Network _flushQueued = false; } - public static int FlushAll() + public static void FlushAll() + { + while (FlushPending.Count != 0) + { + FlushPending.Dequeue()?.Flush(); + } + } + + public static int Slice() { int count = 0; - while (FlushPending.TryDequeue(out var ns)) + while (FlushPending.Count != 0) { - ns.Flush(); + FlushPending.Dequeue()?.Flush(); count++; } @@ -677,7 +1059,6 @@ namespace Server.Network { ns.Dispose(); TcpServer.Instances.Remove(ns); - count++; } return count; @@ -688,7 +1069,7 @@ namespace Server.Network if (Connection != null && _nextActivityCheck - curTicks < 0) { WriteConsole("Disconnecting due to inactivity..."); - Disconnect(); + Disconnect("Disconnecting due to inactivity."); } } @@ -709,19 +1090,7 @@ namespace Server.Network } } - public bool CheckEncrypted(int packetID) - { - if (!SentFirstPacket && packetID != 0xF0 && packetID != 0xF1 && packetID != 0xCF && packetID != 0x80 && - packetID != 0x91 && packetID != 0xA4 && packetID != 0xEF) - { - WriteConsole("Encrypted client detected, disconnecting"); - Disconnect(); - return true; - } - - return false; - } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public PacketHandler GetHandler(int packetID) => IncomingPackets.GetHandler(packetID); public static void TraceException(Exception ex) @@ -744,41 +1113,64 @@ namespace Server.Network Console.WriteLine(ex); } - public void Disconnect() + public void Disconnect(string reason) { - if (Connection == null || !_running) + if (Interlocked.CompareExchange(ref _running, 0, 1) == 0) { return; } - _running = false; - try { - Connection.Shutdown(SocketShutdown.Both); + if (_disconnectReason != string.Empty) + { + throw new Exception("Attempted to disconnect a netstate twice."); + } } - catch (SocketException ex) + catch (Exception ex) { TraceException(ex); } - try + _disconnectReason = reason; + + if (Connection == null) { - Connection.Close(); - } - catch (SocketException ex) - { - TraceException(ex); + return; } Disposed.Enqueue(this); } + public void TraceDisconnect() + { + if (_disconnectReason == string.Empty) + { + return; + } + + try + { + using StreamWriter op = new StreamWriter("network-disconnects.log", true); + op.WriteLine($"# {DateTime.UtcNow}"); + + op.WriteLine($"NetState: {this}"); + op.WriteLine(_disconnectReason); + + op.WriteLine(); + op.WriteLine(); + } + catch (Exception ex) + { + TraceException(ex); + } + } + private void Dispose() { - Connection = null; + TraceDisconnect(); - RecvPipe.Writer.Flush(); + RecvPipe.Writer.Close(); SendPipe.Writer.Close(); var m = Mobile; diff --git a/Projects/Server/Network/NetworkSocket.cs b/Projects/Server/Network/NetworkSocket.cs index 8170cdc83..e4e4c3a8a 100644 --- a/Projects/Server/Network/NetworkSocket.cs +++ b/Projects/Server/Network/NetworkSocket.cs @@ -24,7 +24,7 @@ namespace Server.Network { public class NetworkSocket : ISocket { - private Socket _connection; + private readonly Socket _connection; public Socket Connection { @@ -51,6 +51,9 @@ namespace Server.Network public Task SendAsync(IList> buffers, SocketFlags flags) => _connection.SendAsync(buffers, flags); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Send(IList> buffers, SocketFlags flags) => _connection.Send(buffers, flags); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Task ReceiveAsync(IList> buffers, SocketFlags flags) => _connection.ReceiveAsync(buffers, flags); diff --git a/Projects/Server/Network/Packet.cs b/Projects/Server/Network/Packet.cs index 198ed3af1..314375694 100644 --- a/Projects/Server/Network/Packet.cs +++ b/Projects/Server/Network/Packet.cs @@ -22,7 +22,7 @@ namespace Server.Network if (Core.Profiling) { - var prof = PacketSendProfile.Acquire(GetType()); + var prof = PacketSendProfile.Acquire(PacketID); prof.Increment(); } } @@ -37,7 +37,7 @@ namespace Server.Network if (Core.Profiling) { - var prof = PacketSendProfile.Acquire(GetType()); + var prof = PacketSendProfile.Acquire(PacketID); prof.Increment(); } } diff --git a/Projects/Server/Network/PacketHandler.cs b/Projects/Server/Network/PacketHandler.cs index 360735861..2d1d38b15 100644 --- a/Projects/Server/Network/PacketHandler.cs +++ b/Projects/Server/Network/PacketHandler.cs @@ -4,7 +4,7 @@ namespace Server.Network { public delegate void OnPacketReceive(NetState state, CircularBufferReader reader, ref int packetLength); - public delegate TimeSpan ThrottlePacketCallback(NetState state); + public delegate bool ThrottlePacketCallback(int packetId, NetState state, out bool drop); public class PacketHandler { diff --git a/Projects/Server/Network/Packets/IncomingAccountPackets.cs b/Projects/Server/Network/Packets/IncomingAccountPackets.cs index d9bbd1a07..451dce856 100644 --- a/Projects/Server/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/Server/Network/Packets/IncomingAccountPackets.cs @@ -140,7 +140,7 @@ namespace Server.Network if (info == null || a == null || cityIndex < 0 || cityIndex >= info.Length) { - state.Disconnect(); + state.Disconnect("Invalid city selected during character creation."); } else { @@ -197,7 +197,7 @@ namespace Server.Network else { state.BlockAllPackets = false; - state.Disconnect(); + state.Disconnect("Character creation blocked."); } } } @@ -256,7 +256,7 @@ namespace Server.Network if (a == null || charSlot < 0 || charSlot >= a.Length) { - state.Disconnect(); + state.Disconnect("Invalid character slot selected."); } else { @@ -277,11 +277,11 @@ namespace Server.Network if (m == null) { - state.Disconnect(); + state.Disconnect("Empty character slot selected."); return; } - m.NetState?.Disconnect(); + m.NetState?.Disconnect("Character selected for a player already logged in."); state.SendClientVersionRequest(); @@ -380,9 +380,11 @@ namespace Server.Network public static void GameLogin(NetState state, CircularBufferReader reader, ref int packetLength) { + // TODO: Connection throttling + if (state.SentFirstPacket) { - state.Disconnect(); + state.Disconnect("Duplicate game login packet received."); return; } @@ -390,14 +392,16 @@ namespace Server.Network var authID = reader.ReadInt32(); - if ( - !m_AuthIDWindow.TryGetValue(authID, out var ap) || - state._authId != 0 && authID != state._authId || - state._authId == 0 && authID != state._seed - ) + if (!m_AuthIDWindow.TryGetValue(authID, out var ap)) { - state.WriteConsole("Invalid client detected, disconnecting"); - state.Disconnect(); + state.WriteConsole("Invalid client detected, disconnecting..."); + state.Disconnect("Unable to find auth id."); + } + + if (state._authId != 0 && authID != state._authId || state._authId == 0 && authID != state._seed) + { + state.WriteConsole("Invalid client detected, disconnecting..."); + state.Disconnect("Invalid auth id in game login packet."); return; } @@ -424,7 +428,7 @@ namespace Server.Network } else { - state.Disconnect(); + state.Disconnect("Login rejected by GameLogin packet handler."); } } @@ -436,7 +440,7 @@ namespace Server.Network if (info == null || a == null || index < 0 || index >= info.Length) { - state.Disconnect(); + state.Disconnect("Invalid server selected."); } else { @@ -457,7 +461,7 @@ namespace Server.Network if (state._seed == 0) { state.WriteConsole("Invalid client detected, disconnecting"); - state.Disconnect(); + state.Disconnect("Duplicate seed sent."); return; } @@ -471,9 +475,11 @@ namespace Server.Network public static void AccountLogin(NetState state, CircularBufferReader reader, ref int packetLength) { + // TODO: Throttle Connection + if (state.SentFirstPacket) { - state.Disconnect(); + state.Disconnect("Duplicate account login packet sent."); return; } @@ -512,7 +518,7 @@ namespace Server.Network private static void AccountLogin_ReplyRej(this NetState state, ALRReason reason) { state.SendAccountLoginRejected(reason); - state.Disconnect(); + state.Disconnect("Account login rejected by AccountLogin packet handler."); } private class LoginTimer : Timer diff --git a/Projects/Server/Network/Packets/IncomingExtendedCommandPackets.cs b/Projects/Server/Network/Packets/IncomingExtendedCommandPackets.cs index 400c4a685..9a4921fc8 100644 --- a/Projects/Server/Network/Packets/IncomingExtendedCommandPackets.cs +++ b/Projects/Server/Network/Packets/IncomingExtendedCommandPackets.cs @@ -106,9 +106,9 @@ namespace Server.Network public static void ExtendedCommand(NetState state, CircularBufferReader reader, ref int packetLength) { - int packetID = reader.ReadUInt16(); + int packetId = reader.ReadUInt16(); - var ph = GetExtendedHandler(packetID); + var ph = GetExtendedHandler(packetId); if (ph == null) { @@ -122,11 +122,11 @@ namespace Server.Network { state.WriteConsole( "Sent in-game packet (0xBFx{0:X2}) before having been attached to a mobile", - packetID + packetId ); } - state.Disconnect(); + state.Disconnect($"Sent in-game packet(0xBFx{packetId:X2}) but mobile is deleted."); } else { diff --git a/Projects/Server/Network/Packets/IncomingPackets.cs b/Projects/Server/Network/Packets/IncomingPackets.cs index af8af3e4c..67f4f29fa 100644 --- a/Projects/Server/Network/Packets/IncomingPackets.cs +++ b/Projects/Server/Network/Packets/IncomingPackets.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Server.Network { @@ -82,87 +83,25 @@ namespace Server.Network } } - public static int ProcessPacket(this NetState ns, ArraySegment[] buffer) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInfoPacket(byte packetId) { - var reader = new CircularBufferReader(buffer); - - var packetId = reader.ReadByte(); - - if (!ns.Seeded) + // These packets can arrive at any time during the login process. They're just informational. + return packetId switch { - if (packetId == 0xEF) - { - // new packet in client 6.0.5.0 replaces the traditional seed method with a seed packet - // 0xEF = 239 = multicast IP, so this should never appear in a normal seed. So this is backwards compatible with older clients. - ns.Seeded = true; - } - else - { - var seed = (packetId << 24) | (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte(); - - if (seed == 0) - { - ns.WriteConsole("Invalid client detected, disconnecting"); - return -1; - } - - ns._seed = seed; - ns.Seeded = true; - - return 4; - } - } - - if (ns.CheckEncrypted(packetId)) - { - return -1; - } - - // Get Handlers - var handler = ns.GetHandler(packetId); - - if (handler == null) - { - reader.Trace(ns); - return -1; - } - - // We use this for failing fast where we already know the length, but may not read it entirely - var packetLength = handler.Length; - - if (handler.Length <= 0 && reader.Length >= 3) - { - packetLength = reader.ReadUInt16(); - if (packetLength < 3) - { - return -1; - } - } - - // Not enough data, let's wait for more to come in - if (reader.Length < packetLength) - { - return 0; - } - - if (handler.Ingame && ns.Mobile?.Deleted != false) - { - ns.WriteConsole("Sent ingame packet (0x{1:X2}) without being attached to a valid mobile.", ns, packetId); - return -1; - } - - var throttled = handler.ThrottleCallback?.Invoke(ns) ?? TimeSpan.Zero; - - if (throttled > TimeSpan.Zero) - { - ns.ThrottledUntil = DateTime.UtcNow + throttled; - } - - // The packet length is sent in as a ref to support situations where a smaller/larger packet is read. - // Example is DropReq to support 6.0.1.7+ where the packet is 1 byte larger - handler.OnReceive(ns, reader, ref packetLength); - - return packetLength; + 0x01 => true, // Disconnect + 0x73 => true, // Ping + 0xA4 => true, // SystemInfo + 0xB1 => true, // Gump Response + 0xBB => true, // Account ID + 0xBD => true, // Client Version + 0xBE => true, // Assist Version + 0xD9 => true, // Hardware Info + 0xDD => true, // Gumps (Packed) + 0xE1 => true, // Client Type + 0xF4 => true, // CrashReport + _ => false + }; } } } diff --git a/Projects/Server/Network/Packets/IncomingPlayerPackets.cs b/Projects/Server/Network/Packets/IncomingPlayerPackets.cs index 98b8fe72e..93bfbace6 100644 --- a/Projects/Server/Network/Packets/IncomingPlayerPackets.cs +++ b/Projects/Server/Network/Packets/IncomingPlayerPackets.cs @@ -372,7 +372,7 @@ namespace Server.Network if (!buttonExists) { state.WriteConsole("Invalid gump response, disconnecting..."); - state.Disconnect(); + state.Disconnect("Invalid gump response."); return; } @@ -381,7 +381,7 @@ namespace Server.Network if (switchCount < 0 || switchCount > gump.m_Switches) { state.WriteConsole("Invalid gump response, disconnecting..."); - state.Disconnect(); + state.Disconnect("Invalid gump response."); return; } @@ -397,7 +397,7 @@ namespace Server.Network if (textCount < 0 || textCount > gump.m_TextEntries) { state.WriteConsole("Invalid gump response, disconnecting..."); - state.Disconnect(); + state.Disconnect("Invalid gump response."); return; } @@ -411,7 +411,7 @@ namespace Server.Network if (textLength > 239) { state.WriteConsole("Invalid gump response, disconnecting..."); - state.Disconnect(); + state.Disconnect("Invalid gump response."); return; } @@ -593,14 +593,14 @@ namespace Server.Network if (ph.Ingame && state.Mobile == null) { state.WriteConsole( - "Sent ingame packet (0xD7x{0:X2}) before having been attached to a mobile", + "Sent in-game packet (0xD7x{0:X2}) before being attached to a mobile", packetId ); - state.Disconnect(); + state.Disconnect($"Sent in-game packet (0xD7x{packetId:X2}) before being attached to a mobile."); } else if (ph.Ingame && state.Mobile.Deleted) { - state.Disconnect(); + state.Disconnect($"Sent in-game packet(0xD7x{packetId:X2}) but mobile is deleted."); } else { diff --git a/Projects/Server/Network/Pipe.cs b/Projects/Server/Network/Pipe.cs index 7f98fda72..521a68170 100644 --- a/Projects/Server/Network/Pipe.cs +++ b/Projects/Server/Network/Pipe.cs @@ -212,7 +212,24 @@ namespace Server.Network { _pipe._closed = true; - Flush(); + var waiting = _pipe._readAwaitBeginning; + + if (!waiting) + { + return; + } + + Action continuation; + + do + { + continuation = _pipe._readContinuation; + } while (continuation == null); + + _pipe._readContinuation = null; + _pipe._readAwaitBeginning = false; + + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), true); } public void Flush() @@ -257,6 +274,11 @@ namespace Server.Network return true; } + if (_pipe._closed) + { + return true; + } + _pipe._writeAwaitBeginning = true; return false; } @@ -363,7 +385,7 @@ namespace Server.Network public void Commit() { - if (_pipe._readIdx == (_pipe._writeIdx + 1) % _pipe.Size) + if (_pipe._readIdx == ((_pipe._writeIdx + 1) & (_pipe.Size - 1))) { return; } @@ -388,6 +410,30 @@ namespace Server.Network ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), true); } + public void Close() + { + _pipe._closed = true; + + var waiting = _pipe._writeAwaitBeginning; + + if (!waiting) + { + return; + } + + Action continuation; + + do + { + continuation = _pipe._writeContinuation; + } while (continuation == null); + + _pipe._writeContinuation = null; + _pipe._readAwaitBeginning = false; + + ThreadPool.UnsafeQueueUserWorkItem(_ => continuation(), true); + } + #region Awaitable // The following makes it possible to await the reader. Do not use any of this directly. @@ -403,6 +449,11 @@ namespace Server.Network return true; } + if (_pipe._closed) + { + return true; + } + _pipe._readAwaitBeginning = true; return false; } @@ -427,6 +478,12 @@ namespace Server.Network public Pipe(T[] buf) { + // Test if the buffer is a power of two + if (buf.Length == 0 || (buf.Length & (buf.Length - 1)) != 0) + { + throw new ArgumentOutOfRangeException(nameof(buf), "Pipe buffers must have a length that is a power of two"); + } + _buffer = buf; _writeIdx = 0; _readIdx = 0; diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 68db1eb45..dfa0093a4 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -172,6 +172,8 @@ namespace Server ns.Send(buffer); } + + NetState.FlushAll(); } public static void Broadcast(int hue, bool ascii, string format, params object[] args) @@ -569,9 +571,6 @@ namespace Server ++_Saves; - NetState.FlushAll(); - NetState.Pause(); - WorldState = WorldState.Saving; m_DiskWriteHandle.Reset(); @@ -611,8 +610,6 @@ namespace Server } ThreadPool.QueueUserWorkItem(WriteFiles); - - NetState.Resume(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs index ae034a0eb..18dd53e0b 100644 --- a/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Bulletin Boards/Packets.cs @@ -11,7 +11,7 @@ namespace Server.Network var buffer = (board.BoardName ?? "").GetBytesUtf8(); - Stream.Write((byte)0x00); // PacketID + Stream.Write((byte)0x00); // Packet ID Stream.Write(board.Serial); // Bulletin board serial // Bulletin board name @@ -38,7 +38,7 @@ namespace Server.Network EnsureCapacity(22 + poster.Length + subject.Length + time.Length); - Stream.Write((byte)0x01); // PacketID + Stream.Write((byte)0x01); // Packet ID Stream.Write(board.Serial); // Bulletin board serial Stream.Write(msg.Serial); // Message serial @@ -77,7 +77,7 @@ namespace Server.Network EnsureCapacity(22 + poster.Length + subject.Length + time.Length); - Stream.Write((byte)0x02); // PacketID + Stream.Write((byte)0x02); // Packet ID Stream.Write(board.Serial); // Bulletin board serial Stream.Write(msg.Serial); // Message serial diff --git a/Projects/UOContent/Accounting/AccountAttackLimiter.cs b/Projects/UOContent/Accounting/AccountAttackLimiter.cs index 4072c588a..9cf167396 100644 --- a/Projects/UOContent/Accounting/AccountAttackLimiter.cs +++ b/Projects/UOContent/Accounting/AccountAttackLimiter.cs @@ -24,23 +24,26 @@ namespace Server.Accounting return; } - IncomingPackets.RegisterThrottler(0x80, Throttle_Callback); - IncomingPackets.RegisterThrottler(0x91, Throttle_Callback); - IncomingPackets.RegisterThrottler(0xCF, Throttle_Callback); + IncomingPackets.RegisterThrottler(0x80, Throttle); + IncomingPackets.RegisterThrottler(0x91, Throttle); + IncomingPackets.RegisterThrottler(0xCF, Throttle); } - public static TimeSpan Throttle_Callback(NetState ns) + public static bool Throttle(int packetId, NetState ns, out bool drop) { var accessLog = FindAccessLog(ns); if (accessLog == null) { - return TimeSpan.Zero; + drop = false; + return true; } var date = DateTime.UtcNow; var access = accessLog.LastAccessTime + ComputeThrottle(accessLog.Counts); - return date >= access ? TimeSpan.Zero : date - access; + var allow = date >= access; + drop = !allow; + return allow; } public static InvalidAccountAccessLog FindAccessLog(NetState ns) diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 04b5dcdf7..85fc714ed 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -216,7 +216,7 @@ namespace Server.Misc { if (!(state.Account is Account acct)) { - state.Disconnect(); + state.Disconnect("Attempted to delete a character but the account could not be found."); return; } diff --git a/Projects/UOContent/Commands/Generic/Commands/Commands.cs b/Projects/UOContent/Commands/Generic/Commands/Commands.cs index c6fd7aed6..d1607ffae 100644 --- a/Projects/UOContent/Commands/Generic/Commands/Commands.cs +++ b/Projects/UOContent/Commands/Generic/Commands/Commands.cs @@ -911,14 +911,13 @@ namespace Server.Commands.Generic if (list.Count > 20) { CommandLogging.Enabled = false; - NetState.Pause(); + NetState.FlushAll(); } base.ExecuteList(e, list); if (list.Count > 20) { - NetState.Resume(); flushToLog = true; CommandLogging.Enabled = true; } @@ -1250,7 +1249,7 @@ namespace Server.Commands.Generic AddResponse($"They have been {(m_Ban ? "banned" : "kicked")}."); - targState.Disconnect(); + targState.Disconnect($"Banned by {from}."); if (m_Ban) { diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index f3be00890..2c2825c7c 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -201,15 +201,13 @@ namespace Server.Commands list.Count == 1 ? "" : "s" ); - NetState.Pause(); + NetState.FlushAll(); for (var i = 0; i < list.Count; ++i) { list[i].Delete(); } - NetState.Resume(); - from.SendMessage("You have deleted {0} object{1}.", list.Count, list.Count == 1 ? "" : "s"); } else diff --git a/Projects/UOContent/Commands/Object Creation/Decorate.cs b/Projects/UOContent/Commands/Object Creation/Decorate.cs index ece83ef5f..ba9979e88 100644 --- a/Projects/UOContent/Commands/Object Creation/Decorate.cs +++ b/Projects/UOContent/Commands/Object Creation/Decorate.cs @@ -5,6 +5,7 @@ using Server.Engines.Quests.Haven; using Server.Engines.Quests.Necro; using Server.Engines.Spawners; using Server.Items; +using Server.Network; using Server.Utilities; namespace Server.Commands @@ -27,6 +28,8 @@ namespace Server.Commands m_Mobile.SendMessage("Generating world decoration, please wait."); + NetState.FlushAll(); + Generate("Data/Decoration/Britannia", Map.Trammel, Map.Felucca); Generate("Data/Decoration/Trammel", Map.Trammel); Generate("Data/Decoration/Felucca", Map.Felucca); diff --git a/Projects/UOContent/Commands/Object Creation/DecorateMag.cs b/Projects/UOContent/Commands/Object Creation/DecorateMag.cs index 41bd5ef75..aa05a4653 100644 --- a/Projects/UOContent/Commands/Object Creation/DecorateMag.cs +++ b/Projects/UOContent/Commands/Object Creation/DecorateMag.cs @@ -6,6 +6,7 @@ using Server.Engines.Quests.Haven; using Server.Engines.Quests.Necro; using Server.Engines.Spawners; using Server.Items; +using Server.Network; using Server.Utilities; namespace Server.Commands @@ -28,6 +29,8 @@ namespace Server.Commands m_Mobile.SendMessage("Generating world decoration, please wait."); + NetState.FlushAll(); + Generate("Data/Decoration/RuinedMaginciaTram", Map.Trammel); Generate("Data/Decoration/RuinedMaginciaFel", Map.Felucca); diff --git a/Projects/UOContent/Commands/Object Creation/GenTeleporter.cs b/Projects/UOContent/Commands/Object Creation/GenTeleporter.cs index dd0c5b1d9..f2542a731 100644 --- a/Projects/UOContent/Commands/Object Creation/GenTeleporter.cs +++ b/Projects/UOContent/Commands/Object Creation/GenTeleporter.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text.Json.Serialization; using Server.Items; using Server.Json; +using Server.Network; namespace Server.Commands { @@ -68,6 +69,9 @@ namespace Server.Commands var from = e.Mobile; from.SendMessage("Generating teleporters, please wait."); + + NetState.FlushAll(); + var c = new TeleportersCreator(); if (!ProcessTeleporterData(from, c.CreateTeleporter)) diff --git a/Projects/UOContent/Commands/SignParser.cs b/Projects/UOContent/Commands/SignParser.cs index d0df6070d..7d1f91bf2 100644 --- a/Projects/UOContent/Commands/SignParser.cs +++ b/Projects/UOContent/Commands/SignParser.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using Server.Items; +using Server.Network; namespace Server.Commands { @@ -28,6 +29,8 @@ namespace Server.Commands var list = new List(); from.SendMessage("Generating signs, please wait."); + NetState.FlushAll(); + using (var ip = new StreamReader(cfg)) { string line; diff --git a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs index 2ad788ed4..7aae32865 100644 --- a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs +++ b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs @@ -206,16 +206,14 @@ namespace Server.Engines.Doom eable.Free(); e.Mobile.SendMessage("Generating Lamp Room puzzle..."); + + NetState.FlushAll(); + new LeverPuzzleController().MoveToWorld(lp_Center, Map.Malas); - if (!installed) - { - e.Mobile.SendMessage("There was a problem generating the puzzle."); - } - else - { - e.Mobile.SendMessage("Lamp room puzzle successfully generated."); - } + e.Mobile.SendMessage( + !installed ? "There was a problem generating the puzzle." : "Lamp room puzzle successfully generated." + ); } public static Item AddLeverPuzzlePart(int[] loc, Item newitem) diff --git a/Projects/UOContent/Gumps/AdminGump.cs b/Projects/UOContent/Gumps/AdminGump.cs index c54ac2988..8e713a456 100644 --- a/Projects/UOContent/Gumps/AdminGump.cs +++ b/Projects/UOContent/Gumps/AdminGump.cs @@ -1762,7 +1762,7 @@ namespace Server.Gumps { if (!ban) { - NetState.Pause(); + NetState.FlushAll(); } for (var i = 0; i < rads.Count; ++i) @@ -1796,11 +1796,6 @@ namespace Server.Gumps } } - if (!ban) - { - NetState.Resume(); - } - from.SendGump( new NoticeGump( 1060637, @@ -2365,7 +2360,7 @@ namespace Server.Gumps if (!hasAccess) { - ns.Disconnect(); + ns.Disconnect("Server has been locked down."); ++count; } } @@ -3736,7 +3731,7 @@ namespace Server.Gumps "kicking", CommandLogging.Format(m) ); - ns.Disconnect(); + ns.Disconnect($"Kicked by {from}."); notice = "They have been kicked."; } else @@ -3762,7 +3757,7 @@ namespace Server.Gumps var ns = m.NetState; - ns?.Disconnect(); + ns?.Disconnect($"Banned by {from}."); notice = "They have been banned."; } diff --git a/Projects/UOContent/Gumps/ClientGump.cs b/Projects/UOContent/Gumps/ClientGump.cs index 7481daa21..9084b8baf 100644 --- a/Projects/UOContent/Gumps/ClientGump.cs +++ b/Projects/UOContent/Gumps/ClientGump.cs @@ -272,7 +272,7 @@ namespace Server.Gumps { focus.Say("I've been kicked!"); - m_State.Disconnect(); + m_State.Disconnect($"Kicked by ${from}."); CommandLogging.WriteLine( from, diff --git a/Projects/UOContent/Items/Skill Items/Camping/Bedroll.cs b/Projects/UOContent/Items/Skill Items/Camping/Bedroll.cs index 17136e01e..2385ab2b1 100644 --- a/Projects/UOContent/Items/Skill Items/Camping/Bedroll.cs +++ b/Projects/UOContent/Items/Skill Items/Camping/Bedroll.cs @@ -121,7 +121,7 @@ namespace Server.Items pm.PlaceInBackpack(m_Bedroll); pm.BedrollLogout = true; - sender.Disconnect(); + sender.Disconnect("Used a bedroll to log out."); } Campfire.RemoveEntry(m_Entry); diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index d11a0aebb..d31ef3136 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -172,7 +172,7 @@ namespace Server.Misc if (ns.Connection != null) { ns.WriteConsole("Disconnecting, bad version"); - ns.Disconnect(); + ns.Disconnect($"Invalid client version {ns.Version}."); } } diff --git a/Projects/UOContent/Misc/DoorGenerator.cs b/Projects/UOContent/Misc/DoorGenerator.cs index 2a910c21e..fa69da13e 100644 --- a/Projects/UOContent/Misc/DoorGenerator.cs +++ b/Projects/UOContent/Misc/DoorGenerator.cs @@ -347,7 +347,7 @@ namespace Server { World.Broadcast(0x35, true, "Generating doors, please wait."); - NetState.Pause(); + NetState.FlushAll(); m_Map = Map.Trammel; m_Count = 0; @@ -389,8 +389,6 @@ namespace Server var malasCount = m_Count; - NetState.Resume(); - World.Broadcast( 0x35, true, diff --git a/Projects/UOContent/Misc/HardwareInfo.cs b/Projects/UOContent/Misc/HardwareInfo.cs index 191cd3321..3be146304 100644 --- a/Projects/UOContent/Misc/HardwareInfo.cs +++ b/Projects/UOContent/Misc/HardwareInfo.cs @@ -145,35 +145,35 @@ namespace Server { reader.ReadByte(); // 1: <4.0.1a, 2>=4.0.1a - var info = new HardwareInfo(); - - info.InstanceID = reader.ReadInt32(); - info.OSMajor = reader.ReadInt32(); - info.OSMinor = reader.ReadInt32(); - info.OSRevision = reader.ReadInt32(); - info.CpuManufacturer = reader.ReadByte(); - info.CpuFamily = reader.ReadInt32(); - info.CpuModel = reader.ReadInt32(); - info.CpuClockSpeed = reader.ReadInt32(); - info.CpuQuantity = reader.ReadByte(); - info.PhysicalMemory = reader.ReadInt32(); - info.ScreenWidth = reader.ReadInt32(); - info.ScreenHeight = reader.ReadInt32(); - info.ScreenDepth = reader.ReadInt32(); - info.DXMajor = reader.ReadInt16(); - info.DXMinor = reader.ReadInt16(); - info.VCDescription = reader.ReadLittleUniSafe(64); - info.VCVendorID = reader.ReadInt32(); - info.VCDeviceID = reader.ReadInt32(); - info.VCMemory = reader.ReadInt32(); - info.Distribution = reader.ReadByte(); - info.ClientsRunning = reader.ReadByte(); - info.ClientsInstalled = reader.ReadByte(); - info.PartialInstalled = reader.ReadByte(); - info.Language = reader.ReadLittleUniSafe(4); - info.Unknown = reader.ReadAsciiSafe(64); - - info.TimeReceived = DateTime.UtcNow; + var info = new HardwareInfo + { + InstanceID = reader.ReadInt32(), + OSMajor = reader.ReadInt32(), + OSMinor = reader.ReadInt32(), + OSRevision = reader.ReadInt32(), + CpuManufacturer = reader.ReadByte(), + CpuFamily = reader.ReadInt32(), + CpuModel = reader.ReadInt32(), + CpuClockSpeed = reader.ReadInt32(), + CpuQuantity = reader.ReadByte(), + PhysicalMemory = reader.ReadInt32(), + ScreenWidth = reader.ReadInt32(), + ScreenHeight = reader.ReadInt32(), + ScreenDepth = reader.ReadInt32(), + DXMajor = reader.ReadInt16(), + DXMinor = reader.ReadInt16(), + VCDescription = reader.ReadLittleUniSafe(64), + VCVendorID = reader.ReadInt32(), + VCDeviceID = reader.ReadInt32(), + VCMemory = reader.ReadInt32(), + Distribution = reader.ReadByte(), + ClientsRunning = reader.ReadByte(), + ClientsInstalled = reader.ReadByte(), + PartialInstalled = reader.ReadByte(), + Language = reader.ReadLittleUniSafe(4), + Unknown = reader.ReadAsciiSafe(64), + TimeReceived = DateTime.UtcNow + }; if (state.Account is Account acct) { diff --git a/Projects/UOContent/Misc/PacketThrottles.cs b/Projects/UOContent/Misc/PacketThrottles.cs new file mode 100644 index 000000000..c4b226b14 --- /dev/null +++ b/Projects/UOContent/Misc/PacketThrottles.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using Server.Json; +using Server.Mobiles; + +namespace Server.Network +{ + public static class PacketThrottles + { + // Delay in milliseconds + private static readonly int[] Delays = new int[0x100]; + private static string ThrottlesConfiguration = "Configuration/throttles.json"; + + public static void Initialize() + { + CommandSystem.Register("GetThrottle", AccessLevel.Administrator, GetThrottle); + CommandSystem.Register("SetThrottle", AccessLevel.Administrator, SetThrottle); + + var configPath = ThrottlesConfiguration; + var path = Path.Join(Core.BaseDirectory, configPath); + + if (File.Exists(path)) + { + var throttles = JsonConfig.Deserialize>(path); + foreach (var (k, v) in throttles) + { + if (!Utility.ToInt32(k, out var packetId)) + { + Utility.PushColor(ConsoleColor.DarkYellow); + Console.WriteLine("Packet Throttles: Error deserializing {0} from {1}", k, configPath); + Utility.PopColor(); + continue; + } + + Delays[packetId] = v; + } + } + else + { + Delays[0x03] = 5; // Speech + Delays[0xAD] = 5; // Speech + Delays[0x75] = 500; // Rename request + } + + for (int i = 0; i < 0x100; i++) + { + if (Delays[i] > 0) + { + IncomingPackets.RegisterThrottler(i, Throttle); + } + } + + SaveDelays(); + } + + [Usage("GetThrottle ")] + [Description("Gets throttle for the given packet.")] + public static void GetThrottle(CommandEventArgs e) + { + if (e.Length != 1) + { + e.Mobile.SendMessage("Invalid Command Format. Should be [GetThrottle "); + return; + } + + int packetID = e.GetInt32(0); + + if (packetID < 0 || packetID > 0x100) + { + e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); + return; + } + + + e.Mobile.SendMessage("Packet 0x{0:X} throttle is currently {1}ms.", packetID, Delays[packetID]); + } + + [Usage("SetThrottle ")] + [Description("Sets a throttle for the given packet.")] + public static void SetThrottle(CommandEventArgs e) + { + if (e.Length != 2) + { + e.Mobile.SendMessage("Invalid Command Format. Should be [SetThrottle "); + return; + } + + int packetID = e.GetInt32(0); + int delay = e.GetInt32(1); + + if (packetID < 0 || packetID > 0x100) + { + e.Mobile.SendMessage("Invalid Command Format. PacketID must be between 0 and 0x100."); + return; + } + + + if (delay > 5000) + { + e.Mobile.SendMessage("Invalid Command Format. Delay cannot exceed 5000 milliseconds."); + return; + } + + long oldDelay = Delays[packetID]; + + if (oldDelay == 0 && delay > 0) + { + IncomingPackets.RegisterThrottler(packetID, Throttle); + } + else if (oldDelay > 0 && delay == 0) + { + IncomingPackets.RegisterThrottler(packetID, null); + } + + Delays[packetID] = delay; + SaveDelays(); + } + + private static void SaveDelays() + { + SortedDictionary table = new(); + for (var i = 0; i < Delays.Length; i++) + { + var delay = Delays[i]; + + if (delay != 0) + { + table[$"0x{i:X2}"] = delay; + } + } + + var configPath = ThrottlesConfiguration; + var path = Path.Join(Core.BaseDirectory, configPath); + JsonConfig.Serialize(path, table); + } + + public static bool Throttle(int packetID, NetState ns, out bool drop) + { + if (ns.Mobile is not PlayerMobile player || player.AccessLevel >= AccessLevel.Counselor) + { + drop = false; + return true; + } + + if (Core.TickCount < ns.GetPacketDelay(packetID) + Delays[packetID]) + { + drop = true; + return false; + } + + drop = false; + return true; + } + } +} diff --git a/Projects/UOContent/Misc/ProfanityProtection.cs b/Projects/UOContent/Misc/ProfanityProtection.cs index e72f0f395..cf917938e 100644 --- a/Projects/UOContent/Misc/ProfanityProtection.cs +++ b/Projects/UOContent/Misc/ProfanityProtection.cs @@ -99,7 +99,7 @@ namespace Server.Misc return true; case ProfanityAction.Disconnect: { - from.NetState?.Disconnect(); + from.NetState?.Disconnect("Using profanity."); return false; } diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 8050f3dab..f803b67bd 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -1287,7 +1287,7 @@ namespace Server.Mobiles if (from.NetState != null) { - Timer.DelayCall(TimeSpan.FromSeconds(1.0), from.NetState.Disconnect); + Timer.DelayCall(TimeSpan.FromSeconds(1.0), from.NetState.Disconnect, "Server is locked down"); } } else if (from.AccessLevel >= AccessLevel.Administrator) diff --git a/Projects/UOContent/Network/ProtocolExtensions.cs b/Projects/UOContent/Network/ProtocolExtensions.cs index a66537552..09b5a5756 100644 --- a/Projects/UOContent/Network/ProtocolExtensions.cs +++ b/Projects/UOContent/Network/ProtocolExtensions.cs @@ -39,11 +39,11 @@ namespace Server.Network if (ph.Ingame && state.Mobile == null) { state.WriteConsole("Sent in-game packet (0xBFx{0:X2}) before having been attached to a mobile", packetID); - state.Disconnect(); + state.Disconnect("Sent in-game packet before being attached to a mobile."); } else if (ph.Ingame && state.Mobile.Deleted) { - state.Disconnect(); + state.Disconnect(string.Empty); } else {