diff --git a/etc/atlasd.sample.json b/etc/atlasd.sample.json index a6a4087a..dab3dc47 100644 --- a/etc/atlasd.sample.json +++ b/etc/atlasd.sample.json @@ -132,6 +132,11 @@ "port": 6112, "tcp_nodelay": true }, + "realm_listener": { + "interface": "0.0.0.0", + "port": 6113, + "tcp_nodelay": true + }, "realm": { "host": "Anonymous", "name": "Atlas" diff --git a/src/Atlasd/Battlenet/Channel.cs b/src/Atlasd/Battlenet/Channel.cs index 5c38ea9a..bc03fac5 100644 --- a/src/Atlasd/Battlenet/Channel.cs +++ b/src/Atlasd/Battlenet/Channel.cs @@ -805,7 +805,8 @@ public void SquelchUpdate(GameState client) { foreach (var user in Users) { - new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(client, user), user.Ping, RenderOnlineName(client, user), user.Statstring).WriteTo(client.Client); + //NOTE: statstrings are not sent on EID_USERUPDATE, doing so causes a rendering bug with Diablo II hardcore characters gaining ops + new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(client, user), user.Ping, RenderOnlineName(client, user), "").WriteTo(client.Client); } } } @@ -925,7 +926,8 @@ public bool UpdateUser(GameState client, Account.Flags flags, Int32 ping, byte[] { foreach (var user in Users) { - new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(user, client), client.Ping, RenderOnlineName(user, client), client.Statstring).WriteTo(user.Client); + //NOTE: statstrings are not sent on EID_USERUPDATE, doing so causes a rendering bug with Diablo II hardcore characters gaining ops + new ChatEvent(ChatEvent.EventIds.EID_USERUPDATE, RenderChannelFlags(user, client), client.Ping, RenderOnlineName(user, client), "").WriteTo(user.Client); } } diff --git a/src/Atlasd/Battlenet/ClientState.cs b/src/Atlasd/Battlenet/ClientState.cs index 227c34f7..1cc9a119 100644 --- a/src/Atlasd/Battlenet/ClientState.cs +++ b/src/Atlasd/Battlenet/ClientState.cs @@ -3,6 +3,7 @@ using Atlasd.Battlenet.Protocols.Game; using Atlasd.Battlenet.Protocols.Game.Messages; using Atlasd.Daemon; +using Atlasd.Helpers; using Atlasd.Localization; using System; using System.Collections.Generic; @@ -22,6 +23,7 @@ class ClientState public bool IsClosing { get; private set; } = false; public GameState GameState { get; private set; } + public RealmState RealmState { get; set; } public ProtocolType ProtocolType { get; private set; } public EndPoint RemoteEndPoint { get; private set; } public IPAddress RemoteIPAddress { get; private set; } @@ -563,5 +565,27 @@ public void SocketIOCompleted_External(object sender, SocketAsyncEventArgs e) SocketIOCompleted(sender, e); } + + // exists at this level because state split across two component objects + public byte[] GenerateDiabloIIStatstring() + { + var statstring = Product.ToByteArray(GameState.Product); + + if (RealmState.ActiveCharacter != null) + { + var partial = new byte[][] + { + "Olympus".ToBytes(), + new byte[] { 0x2C }, //comma + RealmState.ActiveCharacter.Name.ToBytes(), + new byte[] { 0x2C }, //comma + RealmState.ActiveCharacter.Statstring.ToBytes() + }.SelectMany(x => x).ToArray(); + + statstring = statstring.Concat(partial).ToArray(); + } + + return statstring; + } } } diff --git a/src/Atlasd/Battlenet/Common.cs b/src/Atlasd/Battlenet/Common.cs index f8318235..91e245da 100644 --- a/src/Atlasd/Battlenet/Common.cs +++ b/src/Atlasd/Battlenet/Common.cs @@ -1,5 +1,6 @@ using Atlasd.Battlenet.Protocols.Game; using Atlasd.Battlenet.Protocols.Game.Messages; +using Atlasd.Battlenet.Protocols.MCP.Models; using Atlasd.Battlenet.Protocols.Udp; using Atlasd.Daemon; using Atlasd.Localization; @@ -42,13 +43,17 @@ public ShutdownEvent(string adminMessage, bool cancelled, DateTime eventDate, Ti public static ConcurrentDictionary ActiveChannels; public static ConcurrentDictionary ActiveClans; public static ConcurrentDictionary ActiveClientStates; + public static ConcurrentDictionary RealmClientStates; public static List ActiveGameAds; public static ConcurrentDictionary ActiveGameStates; + public static Realm Realm; public static IPAddress DefaultAddress { get; private set; } public static int DefaultPort { get; private set; } public static ServerSocket Listener { get; private set; } + public static RealmSocket RealmListener { get; private set; } public static IPAddress ListenerAddress { get; private set; } public static IPEndPoint ListenerEndPoint { get; private set; } + public static IPEndPoint RealmListenerEndPoint { get; private set; } public static int ListenerPort { get; private set; } public static Timer NullTimer { get; private set; } public static Timer PingTimer { get; private set; } @@ -102,14 +107,17 @@ public static void Initialize() ActiveChannels = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); ActiveClans = new ConcurrentDictionary(); ActiveClientStates = new ConcurrentDictionary(); + RealmClientStates = new ConcurrentDictionary(); ActiveGameAds = new List(); ActiveGameStates = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + Realm = new Realm(); InitializeAds(); DefaultAddress = IPAddress.Any; DefaultPort = 6112; InitializeListener(); + InitializeRealmListener(); NullTimer = new Timer(ProcessNullTimer, ActiveGameStates, 100, 100); PingTimer = new Timer(ProcessPingTimer, ActiveGameStates, 100, 100); @@ -201,7 +209,35 @@ private static void InitializeListener() UdpListener = new UdpListener(ListenerEndPoint); Listener = new ServerSocket(ListenerEndPoint); } - + + private static void InitializeRealmListener() + { + Settings.State.RootElement.TryGetProperty("battlenet", out var battlenetJson); + battlenetJson.TryGetProperty("realm_listener", out var listenerJson); + listenerJson.TryGetProperty("interface", out var interfaceJson); + listenerJson.TryGetProperty("port", out var portJson); + + var listenerAddressStr = interfaceJson.GetString(); + if (!IPAddress.TryParse(listenerAddressStr, out IPAddress listenerAddress)) + { + Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Unable to parse IP address from [battlenet.realm_listener.interface] with value [{listenerAddressStr}]; using any"); + listenerAddress = DefaultAddress; + } + ListenerAddress = listenerAddress; + + portJson.TryGetInt32(out var port); + ListenerPort = port; + + if (!IPEndPoint.TryParse($"{ListenerAddress}:{ListenerPort}", out IPEndPoint listenerEndPoint)) + { + Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Unable to parse endpoint with value [{ListenerAddress}:{ListenerPort}]"); + return; + } + ListenerEndPoint = listenerEndPoint; + + RealmListener = new RealmSocket(ListenerEndPoint); + } + public static uint GetActiveClientCountByProduct(Product.ProductCode productCode) { var count = (uint)0; diff --git a/src/Atlasd/Battlenet/Exceptions/RealmProtocolException.cs b/src/Atlasd/Battlenet/Exceptions/RealmProtocolException.cs new file mode 100644 index 00000000..28997f32 --- /dev/null +++ b/src/Atlasd/Battlenet/Exceptions/RealmProtocolException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Atlasd.Battlenet.Exceptions +{ + class RealmProtocolException : ProtocolException + { + public RealmProtocolException(ClientState client) : base(Battlenet.ProtocolType.Types.Game, client) { } + public RealmProtocolException(ClientState client, string message) : base(Battlenet.ProtocolType.Types.Game, client, message) { } + public RealmProtocolException(ClientState client, string message, Exception innerException) : base(Battlenet.ProtocolType.Types.Game, client, message, innerException) { } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_ENTERCHAT.cs b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_ENTERCHAT.cs index f83dc618..578a5107 100644 --- a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_ENTERCHAT.cs +++ b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_ENTERCHAT.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text; +using Atlasd.Helpers; namespace Atlasd.Battlenet.Protocols.Game.Messages { @@ -26,6 +27,7 @@ public SID_ENTERCHAT(byte[] buffer) public override bool Invoke(MessageContext context) { if (context == null || context.Client == null || !context.Client.Connected) return false; + var realmState = context.Client.RealmState; var gameState = context.Client.GameState; switch (context.Direction) @@ -68,7 +70,24 @@ public override bool Invoke(MessageContext context) if (Product.IsDiabloII(gameState.Product)) { - statstring = Product.ToByteArray(gameState.Product); + // for some reason, the client sends SID_ENTERCHAT with no MCP_CHARLOGON, right after MCP_CHARCREATE + // i'm not certain this is the correct design, but it works for now + var providedStatstring = statstring.AsString(); + var tokens = providedStatstring.Split(","); + var realm = tokens[0]; + var name = tokens[1]; + + if (realm.Length > 0 && realmState != null) + { + var character = Battlenet.Common.Realm.GetCharacter(accountName, name); + if (character != null) + { + realmState.ActiveCharacter = character; + gameState.CharacterName = character.Name.ToBytes(); + } + } + + statstring = context.Client.GenerateDiabloIIStatstring(); } // Do not use client-provided statstring if config.battlenet.emulation.statstring_updates is not enabled for this product. diff --git a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs index 434914c5..148fcecb 100644 --- a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs +++ b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs @@ -4,6 +4,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; +using System.Text; +using Atlasd.Helpers; +using Atlasd.Utilities; namespace Atlasd.Battlenet.Protocols.Game.Messages { @@ -62,8 +66,6 @@ public override bool Invoke(MessageContext context) } case MessageDirection.ServerToClient: { - var clientToken = (UInt32)context.Arguments["clientToken"]; - /** * [Note this format is slightly different from BNETDocs reference as of 2023-02-18] * (UINT32) MCP Cookie (Client Token) @@ -75,15 +77,84 @@ public override bool Invoke(MessageContext context) * (STRING) Battle.net unique name (* as of D2 1.14d, this is empty) */ - Buffer = new byte[8]; // MCP Cookie + MCP Status only; Atlas does not have realm/MCP servers implemented yet. + if (((byte[])context.Arguments["realmTitle"]).AsString() == "Olympus") + { + Buffer = new byte[18 * 4 + 1]; - using var m = new MemoryStream(Buffer); - using var w = new BinaryWriter(m); + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + var cookie = (UInt32)(new Random().Next(int.MinValue, int.MaxValue)); + + w.Write((UInt32)cookie); + w.Write((UInt32)0x00000000); // status + + var chunk1 = new UInt32[] + { + 0x33316163, + 0x65303830 + }.GetBytes(); + + w.Write(chunk1); + +#if DEBUG + string ipString = "127.0.0.1"; +#else + string ipString = NetworkUtilities.GetPublicAddress(); +#endif + + IPAddress ipAddress = IPAddress.Parse(ipString); + + w.Write(ipAddress.GetBytes()); - w.Write((UInt32)clientToken); - w.Write((UInt32)Statuses.RealmUnavailable); // Atlas does not have realm/MCP servers implemented yet. + Settings.State.RootElement.TryGetProperty("battlenet", out var battlenetJson); + battlenetJson.TryGetProperty("realm_listener", out var listenerJson); + listenerJson.TryGetProperty("interface", out var interfaceJson); + listenerJson.TryGetProperty("port", out var portJson); + + portJson.TryGetUInt16(out var port); + + // strange protocol design + ushort hostOrderPort = port; + UInt32 networkOrderPort = (UInt32)IPAddress.HostToNetworkOrder((short)hostOrderPort); + + w.Write(networkOrderPort); + + // this could use some love + var chunk2 = new UInt32[] + { + 0x66663162, + 0x34613566, + 0x64326639, + 0x63336330, + 0x38326135, + 0x39663937, + 0x62653134, + 0x36313861, + 0x36353032, + 0x31353066, + 0x00000000, + 0x00000000 + }.GetBytes(); + + w.Write(chunk2); + w.WriteByteString(Encoding.UTF8.GetBytes("")); + + // to allow associating game and realm connections after MCP_STARTUP + Battlenet.Common.RealmClientStates.TryAdd(cookie, context.Client); + } + else + { + Buffer = new byte[4]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt32)Statuses.RealmUnavailable); + } Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)"); + context.Client.Send(ToByteArray(context.Client.ProtocolType)); return true; } diff --git a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_QUERYREALMS2.cs b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_QUERYREALMS2.cs index 3ce9debd..3ecc191b 100644 --- a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_QUERYREALMS2.cs +++ b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_QUERYREALMS2.cs @@ -42,12 +42,13 @@ public override bool Invoke(MessageContext context) } case MessageDirection.ServerToClient: { - Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)"); - - Dictionary realms = - context.Arguments == null || !context.Arguments.ContainsKey("realms") ? - new Dictionary() : - (Dictionary)context.Arguments["realms"]; + Dictionary realms = new Dictionary + { + { + Encoding.UTF8.GetBytes("Olympus"), + Encoding.UTF8.GetBytes("Diablo II Realm Server") + } + }; /** * (UINT32) Unknown (0) diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Frame.cs b/src/Atlasd/Battlenet/Protocols/MCP/Frame.cs new file mode 100644 index 00000000..f1c82a07 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Frame.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Concurrent; + +namespace Atlasd.Battlenet.Protocols.MCP +{ + class Frame + { + public ConcurrentQueue Messages { get; protected set; } + + public Frame() + { + Messages = new ConcurrentQueue(); + } + + public Frame(ConcurrentQueue messages) + { + Messages = messages; + } + + public byte[] ToByteArray(ProtocolType protocolType) + { + var framebuf = new byte[0]; + var msgs = new ConcurrentQueue(Messages); // Clone Messages into local variable + + while (msgs.Count > 0) + { + if (!msgs.TryDequeue(out var msg)) break; + + var messagebuf = msg.ToByteArray(protocolType); + var buf = new byte[framebuf.Length + messagebuf.Length]; + + Buffer.BlockCopy(framebuf, 0, buf, 0, framebuf.Length); + Buffer.BlockCopy(messagebuf, 0, buf, framebuf.Length, messagebuf.Length); + + framebuf = buf; + } + + return framebuf; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Message.cs b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs new file mode 100644 index 00000000..b2fbecab --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs @@ -0,0 +1,76 @@ +using Atlasd.Battlenet.Protocols.MCP.Messages; +using Atlasd.Battlenet.Exceptions; +using System; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP +{ + abstract class Message + { + public byte Id; + public byte[] Buffer { get; protected set; } + + private const int HEADER_SIZE = 3; + + public static Message FromByteArray(byte[] buffer) + { + UInt16 length = (UInt16)((buffer[1] << 8) + buffer[0]); + byte id = buffer[2]; + byte[] body = new byte[length - HEADER_SIZE]; + + System.Buffer.BlockCopy(buffer, HEADER_SIZE, body, 0, length - HEADER_SIZE); + + return FromByteArray(id, body); + } + + public static Message FromByteArray(byte id, byte[] buffer) + { + return ((MessageIds)id) switch + { + MessageIds.MCP_NULL => new MCP_NULL(buffer), + MessageIds.MCP_STARTUP => new MCP_STARTUP(buffer), + MessageIds.MCP_CHARCREATE => new MCP_CHARCREATE(buffer), + MessageIds.MCP_CHARLOGON => new MCP_CHARLOGON(buffer), + MessageIds.MCP_CHARDELETE => new MCP_CHARDELETE(buffer), + MessageIds.MCP_CHARUPGRADE => new MCP_CHARUPGRADE(buffer), + + MessageIds.MCP_MOTD => new MCP_MOTD(buffer), + + MessageIds.MCP_CHARLIST => new MCP_CHARLIST(buffer), + MessageIds.MCP_CHARLIST2 => new MCP_CHARLIST2(buffer), + /* + MessageIds.MCP_ => new MCP_(buffer), + */ + _ => null, + }; + } + + public static string MessageName(byte messageId) + { + return ((MessageIds)messageId).ToString(); + } + + public byte[] ToByteArray(ProtocolType protocolType) + { + if (protocolType.IsGame()) + { + var size = (UInt16)(HEADER_SIZE + Buffer.Length); + var buffer = new byte[size]; + + buffer[0] = (byte)(size); + buffer[1] = (byte)(size >> 8); + buffer[2] = Id; + + System.Buffer.BlockCopy(Buffer, 0, buffer, HEADER_SIZE, Buffer.Length); + + return buffer; + } + else + { + throw new NotSupportedException(); + } + } + + public abstract bool Invoke(MessageContext context); + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/MessageContext.cs b/src/Atlasd/Battlenet/Protocols/MCP/MessageContext.cs new file mode 100644 index 00000000..6432ceed --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/MessageContext.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Atlasd.Battlenet.Protocols.MCP +{ + class MessageContext + { + public Dictionary Arguments { get; protected set; } + public RealmState RealmState { get; protected set; } + public MessageDirection Direction { get; protected set; } + + public MessageContext(RealmState realmState, MessageDirection direction, Dictionary arguments = null) + { + Arguments = arguments; + RealmState = realmState; + Direction = direction; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages.cs b/src/Atlasd/Battlenet/Protocols/MCP/MessageIds.cs similarity index 83% rename from src/Atlasd/Battlenet/Protocols/MCP/Messages.cs rename to src/Atlasd/Battlenet/Protocols/MCP/MessageIds.cs index 549e13a3..ba9df251 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/MessageIds.cs @@ -1,7 +1,8 @@ namespace Atlasd.Battlenet.Protocols.MCP { - enum Messages : byte + enum MessageIds : byte { + MCP_NULL = 0x00, MCP_STARTUP = 0x01, MCP_CHARCREATE = 0x02, MCP_CREATEGAME = 0x03, @@ -13,7 +14,7 @@ enum Messages : byte MCP_REQUESTLADDERDATA = 0x11, MCP_MOTD = 0x12, MCP_CANCELGAMECREATE = 0x13, - MCP_CREATEQUEUE = 0x14, + MCP_CREATEQUEUE = 0x14, // not on bnetdocs MCP_CHARRANK = 0x16, MCP_CHARLIST = 0x17, MCP_CHARUPGRADE = 0x18, diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CANCELGAMECREATE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CANCELGAMECREATE.cs new file mode 100644 index 00000000..5a0dc5bd --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CANCELGAMECREATE.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CANCELGAMECREATE : Message + { + public MCP_CANCELGAMECREATE() + { + Id = (byte)MessageIds.MCP_CANCELGAMECREATE; + Buffer = new byte[0]; + } + + public MCP_CANCELGAMECREATE(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CANCELGAMECREATE; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs new file mode 100644 index 00000000..15b3267e --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Battlenet.Protocols.MCP.Models; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARCREATE: Message + { + public enum Statuses : UInt32 + { + Success = 0x00, + AlreadyExists = 0x14, + Invalid = 0x15, + }; + + public MCP_CHARCREATE() + { + Id = (byte)MessageIds.MCP_CHARCREATE; + Buffer = new byte[0]; + } + + public MCP_CHARCREATE(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARCREATE; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 5) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 5 bytes, got {Buffer.Length}"); + + //(UINT32) Character class + //(UINT16) Character flags + //(STRING) Character name + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var type = (CharacterTypes)(r.ReadUInt32() + 1); // i think this field is documented incorrectly + var flags = (CharacterFlags)(r.ReadUInt16()); + var name = r.ReadByteString().AsString(); + + // it appears that a new 0x80 flag is set for hardcore characters on bnet proper + //if ((flags & CharacterFlags.Hardcore) == CharacterFlags.Hardcore) + //{ + // flags = (CharacterFlags)((byte)flags | (byte)0x80); + //} + + if (name.Length < 2) + { + return new MCP_CHARCREATE().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", Statuses.Invalid } })); + } + + var characters = Battlenet.Common.Realm.GetCharacters(realmState.ClientState.GameState.Username); + Character character; + characters.TryGetValue(name, out character); + + Statuses status; + if (character == null) + { + status = Statuses.Success; + character = new Character( + name, type, flags, + (flags & CharacterFlags.Ladder) == CharacterFlags.Ladder ? LadderTypes.Season_1 : LadderTypes.NonLadder + ); + Battlenet.Common.Realm.AddCharacter(realmState.ClientState.GameState.Username, name, character); + } + else + { + //TODO: make names globally unique across accounts as well + status = Statuses.AlreadyExists; + } + + return new MCP_CHARCREATE().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", status } })); + } + case MessageDirection.ServerToClient: + { + int count = 4; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt32)context.Arguments["status"]); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs new file mode 100644 index 00000000..647d65ea --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARDELETE : Message + { + public enum Statuses : UInt32 + { + Success = 0x00, + NotFound = 0x49, + }; + + public MCP_CHARDELETE() + { + Id = (byte)MessageIds.MCP_CHARDELETE; + Buffer = new byte[0]; + } + + public MCP_CHARDELETE(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARDELETE; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 4) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 4 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var cookie = r.ReadUInt16(); + var name = r.ReadByteString().AsString(); + + var character = Battlenet.Common.Realm.GetCharacter(realmState.ClientState.GameState.Username, name); + + Statuses status; + if (character != null) + { + status = Statuses.Success; + Battlenet.Common.Realm.DeleteCharacter(realmState.ClientState.GameState.Username, name); + } + else + { + status = Statuses.NotFound; + } + + return new MCP_CHARDELETE().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "cookie", cookie }, { "status", status } })); + } + case MessageDirection.ServerToClient: + { + int count = 6; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt16)context.Arguments["cookie"]); + w.Write((UInt32)context.Arguments["status"]); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs new file mode 100644 index 00000000..a8b0f6a2 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARLIST : Message + { + public MCP_CHARLIST() + { + Id = (byte)MessageIds.MCP_CHARLIST; + Buffer = new byte[0]; + } + + public MCP_CHARLIST(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARLIST; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from DDV or DXP"); + + if (Buffer.Length < 4) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 4 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var requested = r.ReadUInt32(); + + return new MCP_CHARLIST().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "requested", requested } })); + } + case MessageDirection.ServerToClient: + { + int count = 8; + var characters = Battlenet.Common.Realm.GetCharacters(realmState.ClientState.GameState.Username); + var characterCount = characters.Count; + + foreach (var kv in characters) + { + var character = kv.Value; + + count += 4; + count += character.Name.Length + 1; + count += character.Statstring.ToBytes().Length + 1; + } + + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt16)(characterCount == 0 ? 1 : 1)); // i think this field is documented incorrectly + w.Write((UInt32)(characterCount)); + w.Write((UInt16)(characterCount)); + + foreach (var kv in characters) + { + var name = kv.Key; + var character = kv.Value; + + w.WriteByteString(character.Name.ToBytes()); + w.WriteByteString(character.Statstring.ToBytes()); + } + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs new file mode 100644 index 00000000..34d017fd --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARLIST2 : Message + { + public MCP_CHARLIST2() + { + Id = (byte)MessageIds.MCP_CHARLIST2; + Buffer = new byte[0]; + } + + public MCP_CHARLIST2(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARLIST2; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 4) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 4 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var requested = r.ReadUInt32(); + + return new MCP_CHARLIST2().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "requested", requested } })); + } + case MessageDirection.ServerToClient: + { + int count = 8; + var characters = Battlenet.Common.Realm.GetCharacters(realmState.ClientState.GameState.Username); + var characterCount = characters.Count; + + foreach (var kv in characters) + { + var character = kv.Value; + + count += 4; + count += character.Name.Length + 1; + count += character.Statstring.ToBytes().Length + 1; + } + + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt16)(characterCount == 0 ? 1 : 12)); // i think this field is documented incorrectly + w.Write((UInt32)(characterCount)); + w.Write((UInt16)(characterCount)); + + foreach (var kv in characters) + { + var name = kv.Key; + var character = kv.Value; + + w.Write(UInt32.MaxValue); + w.WriteByteString(character.Name.ToBytes()); + w.WriteByteString(character.Statstring.ToBytes()); + } + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs new file mode 100644 index 00000000..b17da874 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Battlenet.Protocols.MCP.Models; +using Atlasd.Daemon; +using Atlasd.Helpers; +using static Atlasd.Battlenet.Product; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARLOGON : Message + { + public enum Statuses : UInt32 + { + Success = 0x00, + NotFound = 0x46, + Failed = 0x7A, + Expired = 0x7B, + }; + + // set gamestate char name + public MCP_CHARLOGON() + { + Id = (byte)MessageIds.MCP_CHARLOGON; + Buffer = new byte[0]; + } + + public MCP_CHARLOGON(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARLOGON; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 2) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 2 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var name = r.ReadByteString().AsString(); + + var character = Battlenet.Common.Realm.GetCharacter(realmState.ClientState.GameState.Username, name); + + Statuses status; + if (character != null) + { + if ((character.Flags & CharacterFlags.Expansion) == CharacterFlags.Expansion && gameState.Product == ProductCode.DiabloII) + { + status = Statuses.Failed; + } + else + { + status = Statuses.Success; + + realmState.ActiveCharacter = character; + realmState.ClientState.GameState.CharacterName = character.Name.ToBytes(); + var statstring = realmState.ClientState.GenerateDiabloIIStatstring(); + gameState.Statstring = statstring; + } + } + else + { + status = Statuses.NotFound; + } + + return new MCP_CHARLOGON().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", status } })); + } + case MessageDirection.ServerToClient: + { + int count = 4; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt32)context.Arguments["status"]); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARRANK.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARRANK.cs new file mode 100644 index 00000000..0449091c --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARRANK.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARRANK : Message + { + public MCP_CHARRANK() + { + Id = (byte)MessageIds.MCP_CHARRANK; + Buffer = new byte[0]; + } + + public MCP_CHARRANK(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARRANK; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs new file mode 100644 index 00000000..390b6cd9 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Battlenet.Protocols.MCP.Models; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARUPGRADE : Message + { + public enum Statuses : UInt32 + { + Success = 0x00, + NotFound = 0x46, + Failed = 0x7A, + Expired = 0x7B, + Already = 0x7C + }; + + public MCP_CHARUPGRADE() + { + Id = (byte)MessageIds.MCP_CHARUPGRADE; + Buffer = new byte[0]; + } + + public MCP_CHARUPGRADE(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CHARUPGRADE; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 2) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 2 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var name = r.ReadByteString().AsString(); + + var character = Battlenet.Common.Realm.GetCharacter(realmState.ClientState.GameState.Username, name); + + Statuses status; + if (character != null) + { + if ((character.Flags & CharacterFlags.Expansion) == CharacterFlags.Expansion) + { + status = Statuses.Already; + } + else + { + status = Statuses.Success; + character.Flags = character.Flags | CharacterFlags.Expansion; + character.Statstring.Flags = (byte)(character.Statstring.Flags | (byte)CharacterFlags.Expansion); + } + } + else + { + status = Statuses.NotFound; + } + + return new MCP_CHARUPGRADE().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", status } })); + } + case MessageDirection.ServerToClient: + { + int count = 4; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt32)context.Arguments["status"]); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CREATEGAME.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CREATEGAME.cs new file mode 100644 index 00000000..66929197 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CREATEGAME.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CREATEGAME : Message + { + public MCP_CREATEGAME() + { + Id = (byte)MessageIds.MCP_CREATEGAME; + Buffer = new byte[0]; + } + + public MCP_CREATEGAME(byte[] buffer) + { + Id = (byte)MessageIds.MCP_CREATEGAME; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMEINFO.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMEINFO.cs new file mode 100644 index 00000000..e185bd95 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMEINFO.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_GAMEINFO : Message + { + public MCP_GAMEINFO() + { + Id = (byte)MessageIds.MCP_GAMEINFO; + Buffer = new byte[0]; + } + + public MCP_GAMEINFO(byte[] buffer) + { + Id = (byte)MessageIds.MCP_GAMEINFO; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMELIST.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMELIST.cs new file mode 100644 index 00000000..b0247178 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMELIST.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_GAMELIST : Message + { + public MCP_GAMELIST() + { + Id = (byte)MessageIds.MCP_GAMELIST; + Buffer = new byte[0]; + } + + public MCP_GAMELIST(byte[] buffer) + { + Id = (byte)MessageIds.MCP_GAMELIST; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_JOINGAME.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_JOINGAME.cs new file mode 100644 index 00000000..4de52b0a --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_JOINGAME.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_JOINGAME : Message + { + public MCP_JOINGAME() + { + Id = (byte)MessageIds.MCP_JOINGAME; + Buffer = new byte[0]; + } + + public MCP_JOINGAME(byte[] buffer) + { + Id = (byte)MessageIds.MCP_JOINGAME; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs new file mode 100644 index 00000000..4c3a7800 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; +using Atlasd.Helpers; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_MOTD : Message + { + public MCP_MOTD() + { + Id = (byte)MessageIds.MCP_MOTD; + Buffer = new byte[0]; + } + + public MCP_MOTD(byte[] buffer) + { + Id = (byte)MessageIds.MCP_MOTD; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + var gameState = context.RealmState.ClientState.GameState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (!Product.IsDiabloII(gameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length != 0) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be 0 bytes, got {Buffer.Length}"); + + return new MCP_MOTD().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient)); + } + case MessageDirection.ServerToClient: + { + var message = "Welcome to the Olympus realm server for Atlas!"; + + int count = 1 + message.Length + 1; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((byte)0xFF); + w.WriteByteString(message.ToBytes()); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_NULL.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_NULL.cs new file mode 100644 index 00000000..6ee7d249 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_NULL.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_NULL : Message + { + public MCP_NULL() + { + Id = (byte)MessageIds.MCP_NULL; + Buffer = new byte[0]; + } + + public MCP_NULL(byte[] buffer) + { + Id = (byte)MessageIds.MCP_NULL; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_REQUESTLADDERDATA.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_REQUESTLADDERDATA.cs new file mode 100644 index 00000000..0aae20f6 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_REQUESTLADDERDATA.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_REQUESTLADDERDATA : Message + { + public MCP_REQUESTLADDERDATA() + { + Id = (byte)MessageIds.MCP_REQUESTLADDERDATA; + Buffer = new byte[0]; + } + + public MCP_REQUESTLADDERDATA(byte[] buffer) + { + Id = (byte)MessageIds.MCP_REQUESTLADDERDATA; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs new file mode 100644 index 00000000..f4fa45de --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_STARTUP : Message + { + public enum Statuses : UInt32 + { + Success = 1, + Unavailable = 2, + }; + + public MCP_STARTUP() + { + Id = (byte)MessageIds.MCP_STARTUP; + Buffer = new byte[0]; + } + + public MCP_STARTUP(byte[] buffer) + { + Id = (byte)MessageIds.MCP_STARTUP; + Buffer = buffer; + } + + public override bool Invoke(MessageContext context) + { + var realmState = context.RealmState; + + switch (context.Direction) + { + case MessageDirection.ClientToServer: + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + + if (Buffer.Length < 65) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 65 bytes, got {Buffer.Length}"); + + using var m = new MemoryStream(Buffer); + using var r = new BinaryReader(m); + + var cookie = r.ReadUInt32(); + ClientState clientState; + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"Received realm cookie 0x{cookie:X4}"); + + Battlenet.Common.RealmClientStates.TryGetValue(cookie, out clientState); + if (clientState != null) + { + clientState.RealmState = realmState; + realmState.ClientState = clientState; + + if (!Product.IsDiabloII(clientState.GameState.Product)) + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"Realm cookie [0x{cookie:X4}] found and associated"); + return new MCP_STARTUP().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", Statuses.Success } })); + } + else + { + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"Realm cookie [0x{cookie:X4}] does not exist"); + return new MCP_STARTUP().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient, new Dictionary { { "status", Statuses.Unavailable } })); + } + } + case MessageDirection.ServerToClient: + { + int count = 4; + Buffer = new byte[count]; + + using var m = new MemoryStream(Buffer); + using var w = new BinaryWriter(m); + + w.Write((UInt32)context.Arguments["status"]); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_MCP, realmState.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({3 + Buffer.Length} bytes)"); + realmState.Send(ToByteArray(realmState.ProtocolType)); + return true; + } + } + + return false; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs new file mode 100644 index 00000000..5ae2339f --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Models +{ + class Character + { + public string Name { get; private set; } + public CharacterTypes Type { get; private set; } + public CharacterFlags Flags { get; set; } + public LadderTypes Ladder { get; private set; } + public Statstring Statstring { get; private set; } + + public Character(string name, CharacterTypes type, CharacterFlags flags, LadderTypes ladder) + { + Name = name; + Type = type; + Flags = flags; + Ladder = ladder; + + Statstring = new Statstring(type, flags, ladder); + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterFlags.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterFlags.cs new file mode 100644 index 00000000..2c570bf8 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterFlags.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Models +{ + [Flags] + enum CharacterFlags : byte + { + Classic = 0x00, + Hardcore = 0x04, + Dead = 0x08, + Expansion = 0x20, + Ladder = 0x40, + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterTypes.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterTypes.cs new file mode 100644 index 00000000..6b5e841d --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterTypes.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP +{ + enum CharacterTypes : byte + { + Amazon = 0x01, + Sorceress = 0x02, + Necromancer = 0x03, + Paladin = 0x04, + Barbarian = 0x05, + Druid = 0x06, + Assassin = 0x07, + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/LadderTypes.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/LadderTypes.cs new file mode 100644 index 00000000..84275e7f --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/LadderTypes.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Models +{ + enum LadderTypes : byte + { + NonLadder = 0xFF, + Season_1 = 0x01, + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs new file mode 100644 index 00000000..cd9f8f54 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Models +{ + class Realm + { + private ConcurrentDictionary> _characters; + //public ConcurrentDictionary Games { get; private set; } + + public Realm() + { + _characters = new ConcurrentDictionary>(); + } + + public ConcurrentDictionary GetCharacters(string username) + { + ensureCharacters(username.ToLower()); + return getCharacters(username.ToLower()); + } + + public void AddCharacter(string username, string name, Character character) + { + var characters = GetCharacters(username.ToLower()); + characters.TryAdd(name.ToLower(), character); + } + + public Character GetCharacter(string username, string name) + { + var characters = GetCharacters(username.ToLower()); + Character character; + characters.TryGetValue(name.ToLower(), out character); + return character; + } + + public void DeleteCharacter(string username, string name) + { + var characters = GetCharacters(username.ToLower()); + characters.TryRemove(name.ToLower(), out var ignore); + } + + private ConcurrentDictionary getCharacters(string username) + { + ConcurrentDictionary characters; + _characters.TryGetValue(username.ToLower(), out characters); + + return characters; + } + + private void ensureCharacters(string username) + { + var characters = getCharacters(username.ToLower()); + + if (characters == null) + { + _characters.TryAdd(username.ToLower(), new ConcurrentDictionary()); + } + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs new file mode 100644 index 00000000..16d7dcb9 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Models +{ + class Statstring + { + public byte Unknown_1 { get; set; } + public byte Unknown_2 { get; set; } + public byte Head { get; set; } + public byte Torso { get; set; } + public byte Legs { get; set; } + public byte RightArm { get; set; } + public byte LeftArm { get; set; } + public byte RightWeapon { get; set; } + public byte LeftWeapon { get; set; } + public byte Shield { get; set; } + public byte RightShoulder { get; set; } + public byte LeftShoulder { get; set; } + public byte LeftItem { get; set; } + public byte Type { get; set; } + public byte ColorHead { get; set; } + public byte ColorTorso { get; set; } + public byte ColorLegs { get; set; } + public byte ColorRightArm { get; set; } + public byte ColorLeftArm { get; set; } + public byte ColorRightWeapon { get; set; } + public byte ColorLeftWeapon { get; set; } + public byte ColorShield { get; set; } + public byte ColorRightShoulder { get; set; } + public byte ColorLeftShoulder { get; set; } + public byte ColorLeftItem { get; set; } + public byte Level { get; set; } + public byte Flags { get; set; } + public byte Act { get; set; } + public byte Unknown_3 { get; set; } + public byte Unknown_4 { get; set; } + public byte Ladder { get; set; } + public byte Unknown_5 { get; set; } + public byte Unknown_6 { get; set; } + + public Statstring(CharacterTypes type, CharacterFlags flags, LadderTypes ladder) + { + Unknown_1 = 0x84; + Unknown_2 = 0x80; + Head = 0xFF; + Torso = 0xFF; + Legs = 0xFF; + RightArm = 0xFF; + LeftArm = 0xFF; + RightWeapon = getRightWeapon(type); + LeftWeapon = 0xFF; + Shield = getShield(type); + RightShoulder = 0xFF; + LeftShoulder = 0xFF; + LeftItem = 0xFF; + Type = (byte)type; + ColorHead = 0xFF; + ColorTorso = 0xFF; + ColorLegs = 0xFF; + ColorRightArm = 0xFF; + ColorLeftArm = 0xFF; + ColorRightWeapon = 0xFF; + ColorLeftWeapon = 0xFF; + ColorShield = 0xFF; + ColorRightShoulder = 0xFF; + ColorLeftShoulder = 0xFF; + ColorLeftItem = 0xFF; + Level = 0x01; // 1 + Flags = (byte)flags; + Act = 0x80; // normal act 1 + Unknown_3 = 0xFF; // i think this field is documented incorrectly (0x80 = never logged in, 0xFF = has logged in) + Unknown_4 = 0xFF; // i think this field is documented incorrectly (0x80 = never logged in, 0xFF = has logged in) + Ladder = (byte)ladder; + Unknown_5 = 0xFF; + Unknown_6 = 0xFF; + } + + private byte getRightWeapon(CharacterTypes type) + { + switch (type) + { + case CharacterTypes.Amazon: return 0x1B; + case CharacterTypes.Sorceress: return 0x25; + case CharacterTypes.Necromancer: return 0x09; + case CharacterTypes.Paladin: return 0x11; + case CharacterTypes.Barbarian: return 0x04; + case CharacterTypes.Druid: return 0x0C; + case CharacterTypes.Assassin: return 0x2D; + } + + return 0xFF; + } + + private byte getShield(CharacterTypes type) + { + switch (type) + { + case CharacterTypes.Amazon: + case CharacterTypes.Paladin: + case CharacterTypes.Barbarian: + case CharacterTypes.Druid: + case CharacterTypes.Assassin: + return 0x4F; + } + + return 0xFF; + } + + public byte[] ToBytes() + { + return new byte[] + { + Unknown_1, + Unknown_2, + Head, + Torso, + Legs, + RightArm, + LeftArm, + RightWeapon, + LeftWeapon, + Shield, + RightShoulder, + LeftShoulder, + LeftItem, + Type, + ColorHead, + ColorTorso, + ColorLegs, + ColorRightArm, + ColorLeftArm, + ColorRightWeapon, + ColorLeftWeapon, + ColorShield, + ColorRightShoulder, + ColorLeftShoulder, + ColorLeftItem, + Level, + Flags, + Act, + Unknown_3, + Unknown_4, + Ladder, + Unknown_5, + Unknown_6, + }; + } + } +} diff --git a/src/Atlasd/Battlenet/Protocols/MCP/RealmState.cs b/src/Atlasd/Battlenet/Protocols/MCP/RealmState.cs new file mode 100644 index 00000000..dddbd3bb --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/RealmState.cs @@ -0,0 +1,323 @@ +using Atlasd.Battlenet.Exceptions; +using Atlasd.Battlenet.Protocols.BNFTP; +using Atlasd.Battlenet.Protocols.MCP; +using Atlasd.Battlenet.Protocols.MCP.Models; +//using Atlasd.Battlenet.Protocols.Game.Messages; +using Atlasd.Daemon; +using Atlasd.Localization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Atlasd.Battlenet +{ + class RealmState + { + public bool Connected { get => Socket != null && Socket.Connected; } + public bool IsClosing { get; private set; } = false; + + public ClientState ClientState { get; set; } + public Character ActiveCharacter { get; set; } + public ProtocolType ProtocolType { get; private set; } + public EndPoint RemoteEndPoint { get; private set; } + public IPAddress RemoteIPAddress { get; private set; } + public Socket Socket { get; set; } + + protected byte[] ReceiveBuffer = new byte[0]; + protected byte[] SendBuffer = new byte[0]; + + protected Frame RealmGameFrame = new Frame(); + + private const int HEADER_SIZE = 3; + + public RealmState(Socket client) + { + Initialize(client); + } + + public void Close() + { + if (IsClosing) return; + IsClosing = true; + + Disconnect(); + + IsClosing = false; + } + + public void Disconnect(string reason = null) + { + Logging.WriteLine(Logging.LogLevel.Warning, Logging.LogType.Client, RemoteEndPoint, "TCP realm connection forcefully closed by server"); + + // Remove this from ActiveClientStates + //if (!Common.ActiveClientStates.TryRemove(this.Socket, out _)) + //{ + // Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Failed to remove client state [{RemoteEndPoint}] from active client state cache"); + //} + + // Close the connection + try + { + if (Socket != null && Socket.Connected) Socket.Shutdown(SocketShutdown.Send); + } + catch (Exception ex) + { + if (!(ex is SocketException || ex is ObjectDisposedException)) throw; + } + finally + { + if (Socket != null) Socket.Close(); + } + } + + protected void Initialize(Socket client) + { + //if (!Common.ActiveClientStates.TryAdd(client, this)) + //{ + // Logging.WriteLine(Logging.LogLevel.Error, Logging.LogType.Server, $"Failed to add client state [{this.Socket.RemoteEndPoint}] to active client state cache"); + //} + + RemoteEndPoint = client.RemoteEndPoint; + RemoteIPAddress = (client.RemoteEndPoint as IPEndPoint).Address; + Socket = client; + + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client, RemoteEndPoint, "TCP realm connection established"); + + client.NoDelay = Daemon.Common.TcpNoDelay; + client.ReceiveTimeout = 500; + client.SendTimeout = 500; + + if (client.ReceiveBufferSize < 0xFFFF) + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client, RemoteEndPoint, "Setting realm ReceiveBufferSize to [0xFFFF]"); + client.ReceiveBufferSize = 0xFFFF; + } + + if (client.SendBufferSize < 0xFFFF) + { + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client, RemoteEndPoint, "Setting realm SendBufferSize to [0xFFFF]"); + client.SendBufferSize = 0xFFFF; + } + } + + private void Invoke(SocketAsyncEventArgs e) + { + if (e.SocketError != SocketError.Success) return; + + var context = new MessageContext(this, Protocols.MessageDirection.ClientToServer); + + while (RealmGameFrame.Messages.TryDequeue(out var msg)) + { + if (!msg.Invoke(context)) + { + Disconnect(); + return; + } + } + } + + public void ProcessReceive(SocketAsyncEventArgs e) + { + // check if the remote host closed the connection + if (!(e.SocketError == SocketError.Success && e.BytesTransferred > 0)) + { + if (!IsClosing && Socket != null) + { + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client, RemoteEndPoint, $"TCP realm connection lost"); + Close(); + } + return; + } + + // Append received data to previously received data + lock (ReceiveBuffer) + { + var newBuffer = new byte[ReceiveBuffer.Length + e.BytesTransferred]; + Buffer.BlockCopy(ReceiveBuffer, 0, newBuffer, 0, ReceiveBuffer.Length); + Buffer.BlockCopy(e.Buffer, e.Offset, newBuffer, ReceiveBuffer.Length, e.BytesTransferred); + ReceiveBuffer = newBuffer; + } + + ReceiveProtocol(e); + } + + public void ProcessSend(SocketAsyncEventArgs e) + { + // check if the remote host closed the connection + if (e.SocketError != SocketError.Success) + { + if (!IsClosing && Socket != null) + { + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client, RemoteEndPoint, $"TCP realm connection lost"); + Close(); + } + return; + } + } + + public void ReceiveAsync() + { + if (Socket == null || !Socket.Connected) return; + + var readEventArgs = new SocketAsyncEventArgs(); + readEventArgs.Completed += new EventHandler(SocketIOCompleted); + readEventArgs.SetBuffer(new byte[1024], 0, 1024); + readEventArgs.UserToken = this; + + // As soon as the client is connected, post a receive to the connection + bool willRaiseEvent; + try + { + willRaiseEvent = Socket != null && Socket.Connected && Socket.ReceiveAsync(readEventArgs); + } + catch (ObjectDisposedException) + { + return; + } + + if (!willRaiseEvent) + { + SocketIOCompleted(this, readEventArgs); + } + } + + protected void ReceiveProtocol(SocketAsyncEventArgs e) + { + if (e.SocketError != SocketError.Success) return; + + if (ProtocolType == null) ReceiveProtocolType(e); + ReceiveProtocolMCP(e); + } + + protected void ReceiveProtocolType(SocketAsyncEventArgs e) + { + if (ProtocolType != null) return; + + ProtocolType = new ProtocolType((ProtocolType.Types)ReceiveBuffer[0]); + ReceiveBuffer = ReceiveBuffer[1..]; + + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Client, RemoteEndPoint, $"Set realm protocol type [0x{(byte)ProtocolType.Type:X2}] ({ProtocolType})"); + } + + protected void ReceiveProtocolMCP(SocketAsyncEventArgs e) + { + byte[] newBuffer; + + while (ReceiveBuffer.Length > 0) + { + if (ReceiveBuffer.Length < HEADER_SIZE) return; // Partial message header + + UInt16 messageLen = (UInt16)((ReceiveBuffer[1] << 8) + ReceiveBuffer[0]); + + if (ReceiveBuffer.Length < messageLen) return; // Partial message + + byte messageId = ReceiveBuffer[2]; + byte[] messageBuffer = new byte[messageLen - HEADER_SIZE]; + Buffer.BlockCopy(ReceiveBuffer, HEADER_SIZE, messageBuffer, 0, messageLen - HEADER_SIZE); + + // Pop message off the receive buffer + newBuffer = new byte[ReceiveBuffer.Length - messageLen]; + Buffer.BlockCopy(ReceiveBuffer, messageLen, newBuffer, 0, ReceiveBuffer.Length - messageLen); + ReceiveBuffer = newBuffer; + + // Push message onto stack + Message message = Message.FromByteArray(messageId, messageBuffer); + + if (message is Message) + { + RealmGameFrame.Messages.Enqueue(message); + continue; + } + else + { + throw new RealmProtocolException(ClientState, $"Received unknown MCP_0x{messageId:X2} ({messageLen} bytes)"); + } + } + + Invoke(e); + } + + public void Send(byte[] buffer) + { + if (Socket == null) return; + if (!Socket.Connected) return; + + var e = new SocketAsyncEventArgs(); + e.Completed += new EventHandler(SocketIOCompleted); + e.SetBuffer(buffer, 0, buffer.Length); + e.UserToken = this; + + bool willRaiseEvent; + try + { + willRaiseEvent = Socket.SendAsync(e); + } + catch (ObjectDisposedException) + { + return; + } + + if (!willRaiseEvent) + { + SocketIOCompleted(this, e); + } + } + + void SocketIOCompleted(object sender, SocketAsyncEventArgs e) + { + var realmState = e.UserToken as RealmState; + + try + { + // determine which type of operation just completed and call the associated handler + switch (e.LastOperation) + { + case SocketAsyncOperation.Receive: + realmState.ProcessReceive(e); + break; + case SocketAsyncOperation.Send: + realmState.ProcessSend(e); + break; + default: + throw new ArgumentException("The last operation completed on the socket was not a receive or send"); + } + } + catch (GameProtocolViolationException ex) + { + Logging.WriteLine(Logging.LogLevel.Warning, (Logging.LogType)ProtocolType.ProtocolTypeToLogType(ex.ProtocolType), realmState.RemoteEndPoint, "Protocol violation encountered!" + (ex.Message.Length > 0 ? $" {ex.Message}" : "")); + realmState.Close(); + } + catch (Exception ex) + { + Logging.WriteLine(Logging.LogLevel.Warning, Logging.LogType.Client, realmState.RemoteEndPoint, $"{ex.GetType().Name} error encountered!" + (ex.Message.Length > 0 ? $" {ex.Message}" : "")); + realmState.Close(); + } + finally + { + if (e.LastOperation == SocketAsyncOperation.Receive) + { + Task.Run(() => + { + ReceiveAsync(); + }); + } + } + } + + public void SocketIOCompleted_External(object sender, SocketAsyncEventArgs e) + { + var realmState = e.UserToken as RealmState; + if (realmState != this) + { + throw new NotSupportedException(); + } + + SocketIOCompleted(sender, e); + } + } +} diff --git a/src/Atlasd/Battlenet/RealmSocket.cs b/src/Atlasd/Battlenet/RealmSocket.cs new file mode 100644 index 00000000..8b6cce35 --- /dev/null +++ b/src/Atlasd/Battlenet/RealmSocket.cs @@ -0,0 +1,147 @@ +using Atlasd.Daemon; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace Atlasd.Battlenet +{ + class RealmSocket : IDisposable + { + private bool IsDisposing = false; + public bool IsListening { get; private set; } + public IPEndPoint LocalEndPoint { get; private set; } + public Socket Socket { get; private set; } + + public RealmSocket() + { + IsListening = false; + LocalEndPoint = null; + Socket = null; + } + + public RealmSocket(IPEndPoint localEndPoint) + { + IsListening = false; + Socket = null; + SetLocalEndPoint(localEndPoint); + } + + public void Dispose() /* part of IDisposable */ + { + if (IsDisposing) return; + IsDisposing = true; + + if (IsListening) + { + Stop(); + } + + if (Socket != null) + { + Socket = null; + } + + if (LocalEndPoint != null) + { + LocalEndPoint = null; + } + + IsDisposing = false; + } + + private void ProcessAccept(SocketAsyncEventArgs e) + { + var realmState = new RealmState(e.AcceptSocket); + + // Start the read loop on a new stack + Task.Run(() => + { + realmState.ReceiveAsync(); + }); + + // Accept the next connection request + StartAccept(e); + } + + public void SetLocalEndPoint(IPEndPoint localEndPoint) + { + if (IsListening) + { + throw new NotSupportedException("Cannot set LocalEndPoint while socket is listening"); + } + + LocalEndPoint = localEndPoint; + + if (Socket != null) + { + Socket.Close(); + } + + Socket = new Socket(localEndPoint.AddressFamily, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp) + { + ExclusiveAddressUse = true, + NoDelay = Daemon.Common.TcpNoDelay, + UseOnlyOverlappedIO = true, + }; + } + + public void Start(int backlog = 100) + { + if (IsListening) + { + Stop(); + } + + if (LocalEndPoint == null) + { + throw new NullReferenceException("LocalEndPoint must be set to an instance of IPEndPoint"); + } + + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Server, $"Starting TCP realm listener on [{LocalEndPoint}]"); + + Socket.Bind(LocalEndPoint); + Socket.Listen(backlog); + IsListening = true; + + StartAccept(null); + } + + private void StartAccept(SocketAsyncEventArgs acceptEventArg) + { + if (acceptEventArg == null) + { + acceptEventArg = new SocketAsyncEventArgs(); + acceptEventArg.Completed += new EventHandler(AcceptEventArg_Completed); + } + else + { + // socket must be cleared since the context object is being reused + acceptEventArg.AcceptSocket = null; + } + + bool willRaiseEvent = Socket.AcceptAsync(acceptEventArg); + if (!willRaiseEvent) + { + ProcessAccept(acceptEventArg); + } + } + + void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e) + { + ProcessAccept(e); + } + + public void Stop() + { + if (!IsListening) return; + + Logging.WriteLine(Logging.LogLevel.Info, Logging.LogType.Server, $"Stopping TCP listener on [{Socket.LocalEndPoint}]"); + + Socket.Close(); + Socket = null; + + IsListening = false; + } + } +} diff --git a/src/Atlasd/Daemon/Logging.cs b/src/Atlasd/Daemon/Logging.cs index 082d4db2..b86a858e 100644 --- a/src/Atlasd/Daemon/Logging.cs +++ b/src/Atlasd/Daemon/Logging.cs @@ -85,6 +85,7 @@ public static void WriteLine(LogLevel level, LogType type, string buffer) if (level > CurrentLogLevel) return; Console.Out.WriteLine($"[{DateTime.Now}] [{LogLevelToString(level)}] [{LogTypeToString(type).Replace("_", "] [")}] {buffer}"); + Console.Out.Flush(); } public static void WriteLine(LogLevel level, LogType type, EndPoint endp, string buffer) diff --git a/src/Atlasd/Program.cs b/src/Atlasd/Program.cs index 32cbd4d5..acf4eee5 100644 --- a/src/Atlasd/Program.cs +++ b/src/Atlasd/Program.cs @@ -56,6 +56,7 @@ public static async Task Main(string[] args) Battlenet.Common.Initialize(); Battlenet.Common.UdpListener.Start(); Battlenet.Common.Listener.Start(); + Battlenet.Common.RealmListener.Start(); Common.HttpListener.Start(); while (!Exit) diff --git a/src/Atlasd/Utilities/BytesHelper.cs b/src/Atlasd/Utilities/BytesHelper.cs new file mode 100644 index 00000000..ce1971fe --- /dev/null +++ b/src/Atlasd/Utilities/BytesHelper.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Net; +using System.Text; + +namespace Atlasd.Helpers +{ + public static class BytesHelper + { + public static byte[] GetBytes(this UInt32[] array) + { + byte[] byteArray = new byte[array.Length * sizeof(UInt32)]; + + for (int i = 0; i < array.Length; i++) + { + byte[] tempArray = BitConverter.GetBytes(array[i]); + Array.Copy(tempArray, 0, byteArray, i * sizeof(UInt32), tempArray.Length); + } + + return byteArray; + } + + public static byte[] GetBytes(this IPAddress value) + { + byte[] ipBytes = value.GetAddressBytes(); + int ipInt = BitConverter.ToInt32(ipBytes, 0); + int networkOrderInt = IPAddress.NetworkToHostOrder(ipInt); + return BitConverter.GetBytes(networkOrderInt).Reverse().ToArray(); + } + + public static string AsString(this byte[] array) + { + return Encoding.UTF8.GetString(array); + } + + public static byte[] ToBytes(this string value) + { + return Encoding.UTF8.GetBytes(value); + } + } +} diff --git a/src/Atlasd/Utilities/NetworkUtilities.cs b/src/Atlasd/Utilities/NetworkUtilities.cs new file mode 100644 index 00000000..d096d5d1 --- /dev/null +++ b/src/Atlasd/Utilities/NetworkUtilities.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Net; + +namespace Atlasd.Utilities +{ + class NetworkUtilities + { + public static string GetPublicAddress() + { + WebRequest request = WebRequest.Create("https://api.ipify.org"); + request.Method = "GET"; + + using (WebResponse response = request.GetResponse()) + { + using (Stream stream = response.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + } + } +}