From f56e2bdda2311b097b153ac0ad99ccfc1908b9f1 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 20:36:44 -0400 Subject: [PATCH 01/12] feat(mcp): adding the basics of mcp and realm login --- etc/atlasd.sample.json | 85 ++--- src/Atlasd/Battlenet/ClientState.cs | 25 ++ src/Atlasd/Battlenet/Common.cs | 38 ++- .../Exceptions/RealmProtocolException.cs | 11 + .../Protocols/Game/Messages/SID_ENTERCHAT.cs | 21 +- .../Game/Messages/SID_LOGONREALMEX.cs | 90 ++++- .../Game/Messages/SID_QUERYREALMS2.cs | 13 +- src/Atlasd/Battlenet/Protocols/MCP/Frame.cs | 41 +++ src/Atlasd/Battlenet/Protocols/MCP/Message.cs | 73 ++++ .../Battlenet/Protocols/MCP/MessageContext.cs | 18 + .../MCP/{Messages.cs => MessageIds.cs} | 5 +- .../MCP/Messages/MCP_CANCELGAMECREATE.cs | 26 ++ .../Protocols/MCP/Messages/MCP_CHARCREATE.cs | 107 ++++++ .../Protocols/MCP/Messages/MCP_CHARDELETE.cs | 26 ++ .../Protocols/MCP/Messages/MCP_CHARLIST.cs | 26 ++ .../Protocols/MCP/Messages/MCP_CHARLIST2.cs | 98 ++++++ .../Protocols/MCP/Messages/MCP_CHARLOGON.cs | 95 ++++++ .../Protocols/MCP/Messages/MCP_CHARRANK.cs | 26 ++ .../Protocols/MCP/Messages/MCP_CHARUPGRADE.cs | 26 ++ .../Protocols/MCP/Messages/MCP_CREATEGAME.cs | 26 ++ .../Protocols/MCP/Messages/MCP_GAMEINFO.cs | 26 ++ .../Protocols/MCP/Messages/MCP_GAMELIST.cs | 26 ++ .../Protocols/MCP/Messages/MCP_JOINGAME.cs | 26 ++ .../Protocols/MCP/Messages/MCP_MOTD.cs | 66 ++++ .../Protocols/MCP/Messages/MCP_NULL.cs | 26 ++ .../MCP/Messages/MCP_REQUESTLADDERDATA.cs | 26 ++ .../Protocols/MCP/Messages/MCP_STARTUP.cs | 88 +++++ .../Protocols/MCP/Models/Character.cs | 25 ++ .../Protocols/MCP/Models/CharacterFlags.cs | 16 + .../Protocols/MCP/Models/CharacterTypes.cs | 17 + .../Protocols/MCP/Models/LadderTypes.cs | 12 + .../Battlenet/Protocols/MCP/Models/Realm.cs | 56 +++ .../Protocols/MCP/Models/Statstring.cs | 151 ++++++++ .../Battlenet/Protocols/MCP/RealmState.cs | 323 ++++++++++++++++++ src/Atlasd/Battlenet/RealmSocket.cs | 147 ++++++++ src/Atlasd/Daemon/Logging.cs | 1 + src/Atlasd/Program.cs | 1 + src/Atlasd/Utilities/BytesHelper.cs | 31 ++ src/Atlasd/Utilities/NetworkUtilities.cs | 25 ++ 39 files changed, 1907 insertions(+), 58 deletions(-) create mode 100644 src/Atlasd/Battlenet/Exceptions/RealmProtocolException.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Frame.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Message.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/MessageContext.cs rename src/Atlasd/Battlenet/Protocols/MCP/{Messages.cs => MessageIds.cs} (83%) create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CANCELGAMECREATE.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARRANK.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CREATEGAME.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMEINFO.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_GAMELIST.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_JOINGAME.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_NULL.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_REQUESTLADDERDATA.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterFlags.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/CharacterTypes.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/LadderTypes.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs create mode 100644 src/Atlasd/Battlenet/Protocols/MCP/RealmState.cs create mode 100644 src/Atlasd/Battlenet/RealmSocket.cs create mode 100644 src/Atlasd/Utilities/BytesHelper.cs create mode 100644 src/Atlasd/Utilities/NetworkUtilities.cs diff --git a/etc/atlasd.sample.json b/etc/atlasd.sample.json index a6a4087a..dad856e4 100644 --- a/etc/atlasd.sample.json +++ b/etc/atlasd.sample.json @@ -96,47 +96,52 @@ "locales": null } ], - "battlenet": { - "emulation": { - "auto_refresh_pings": false, - "chat_gateway": { - "auto_account_create": true, - "receive_ping_messages": true - }, - "enable_ip_address_in_chatevents": false, - "grant_sudo_to_spoofed_admins": true, - "mask_admins_in_broadcasts": false, - "mask_admins_in_ban_message": true, - "mask_admins_in_kick_message": true, - "required_game_key_count": { - "D2DV": 1, - "D2XP": 2, - "JSTR": 0, - "SEXP": 0, - "STAR": 0, - "W2BN": 1, - "W3XP": 2, - "WAR3": 1 - }, - "statstring_updates": { - "D2DV": true, - "D2XP": true, - "DRTL": true, - "DSHR": true, - "W3XP": true, - "WAR3": true - } + "battlenet": { + "emulation": { + "auto_refresh_pings": false, + "chat_gateway": { + "auto_account_create": true, + "receive_ping_messages": true + }, + "enable_ip_address_in_chatevents": false, + "grant_sudo_to_spoofed_admins": true, + "mask_admins_in_broadcasts": false, + "mask_admins_in_ban_message": true, + "mask_admins_in_kick_message": true, + "required_game_key_count": { + "D2DV": 1, + "D2XP": 2, + "JSTR": 0, + "SEXP": 0, + "STAR": 0, + "W2BN": 1, + "W3XP": 2, + "WAR3": 1 + }, + "statstring_updates": { + "D2DV": true, + "D2XP": true, + "DRTL": true, + "DSHR": true, + "W3XP": true, + "WAR3": true + } + }, + "listener": { + "interface": "0.0.0.0", + "port": 6112, + "tcp_nodelay": true + }, + "realm_listener": { + "interface": "0.0.0.0", + "port": 6113, + "tcp_nodelay": true + }, + "realm": { + "host": "Anonymous", + "name": "Olympus" + } }, - "listener": { - "interface": "0.0.0.0", - "port": 6112, - "tcp_nodelay": true - }, - "realm": { - "host": "Anonymous", - "name": "Atlas" - } - }, "bnftp": { "root": "G:\\Projects\\Visual Studio\\Atlas\\var\\bnftp" }, diff --git a/src/Atlasd/Battlenet/ClientState.cs b/src/Atlasd/Battlenet/ClientState.cs index 227c34f7..4d49a024 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,11 +23,14 @@ 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; } public Socket Socket { get; set; } + public UInt32 RealmCookie { get; set; } + protected byte[] ReceiveBuffer = new byte[0]; protected byte[] SendBuffer = new byte[0]; @@ -563,5 +567,26 @@ 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[][] + { + 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 91d5e700..6752ab06 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.Collections.Generic; using System.IO; 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..5aad8853 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,87 @@ 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); - w.Write((UInt32)clientToken); - w.Write((UInt32)Statuses.RealmUnavailable); // Atlas does not have realm/MCP servers implemented yet. + 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); + byte[] ipBytes = ipAddress.GetAddressBytes(); + int ipInt = BitConverter.ToInt32(ipBytes, 0); + int networkOrderInt = IPAddress.NetworkToHostOrder(ipInt); + byte[] bytes = BitConverter.GetBytes(networkOrderInt).Reverse().ToArray(); + + w.Write(bytes); + + 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); + + 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("")); + + Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)"); + + // 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..3d7943dd --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs @@ -0,0 +1,73 @@ +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_MOTD => new MCP_MOTD(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..c3e7a4d2 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs @@ -0,0 +1,107 @@ +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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 5) + throw new GameProtocolViolationException(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(); + + 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..2b6512d9 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARDELETE : Message + { + 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) + { + 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..ff713638 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +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) + { + return false; + } + } +} 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..7d74cf55 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs @@ -0,0 +1,98 @@ +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 enum Statuses : UInt32 + { + Success = 1, + Unavailable = 2, + }; + + 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 4) + throw new GameProtocolViolationException(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..d7af4801 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Atlasd.Battlenet.Exceptions; +using Atlasd.Daemon; +using Atlasd.Helpers; + +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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length < 2) + throw new GameProtocolViolationException(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) + { + 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..362dc005 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Atlasd.Battlenet.Protocols.MCP.Messages +{ + class MCP_CHARUPGRADE : Message + { + 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) + { + 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..624cf68c --- /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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + + if (Buffer.Length != 0) + throw new GameProtocolViolationException(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..cf5ccbbe --- /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 GameProtocolViolationException(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 GameProtocolViolationException(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..0f1ed66f --- /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; private 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..5784a8d2 --- /dev/null +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs @@ -0,0 +1,56 @@ +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; + } + + 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..635fd916 --- /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 = 0x8F; + 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..96a8e76a --- /dev/null +++ b/src/Atlasd/Utilities/BytesHelper.cs @@ -0,0 +1,31 @@ +using System; +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 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(); + } + } + } + } + } +} From 9b662284229b227651cf660660490af250993f85 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 20:39:37 -0400 Subject: [PATCH 02/12] chore(mcp): nixing the realm cookie from client state, superfluous --- src/Atlasd/Battlenet/ClientState.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Atlasd/Battlenet/ClientState.cs b/src/Atlasd/Battlenet/ClientState.cs index 4d49a024..461abbac 100644 --- a/src/Atlasd/Battlenet/ClientState.cs +++ b/src/Atlasd/Battlenet/ClientState.cs @@ -29,8 +29,6 @@ class ClientState public IPAddress RemoteIPAddress { get; private set; } public Socket Socket { get; set; } - public UInt32 RealmCookie { get; set; } - protected byte[] ReceiveBuffer = new byte[0]; protected byte[] SendBuffer = new byte[0]; From 066acfd3d6e92303073fe38831a5d78312f8619f Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 20:42:04 -0400 Subject: [PATCH 03/12] chore(mcp): moving to RealmProtocolException for MCP packets --- src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs | 4 ++-- src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs | 4 ++-- src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs | 4 ++-- src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs | 4 ++-- src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs index c3e7a4d2..260255e9 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs @@ -42,10 +42,10 @@ public override bool Invoke(MessageContext context) 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); if (Buffer.Length < 5) - throw new GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be at least 5 bytes, got {Buffer.Length}"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be at least 5 bytes, got {Buffer.Length}"); //(UINT32) Character class //(UINT16) Character flags diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs index 7d74cf55..4abfdcab 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs @@ -40,10 +40,10 @@ public override bool Invoke(MessageContext context) 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); if (Buffer.Length < 4) - throw new GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be at least 4 bytes, got {Buffer.Length}"); + 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); diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs index d7af4801..328a888b 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs @@ -44,10 +44,10 @@ public override bool Invoke(MessageContext context) 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); if (Buffer.Length < 2) - throw new GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be at least 2 bytes, got {Buffer.Length}"); + 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); diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs index 624cf68c..4c3a7800 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_MOTD.cs @@ -34,10 +34,10 @@ public override bool Invoke(MessageContext context) 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); if (Buffer.Length != 0) - throw new GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be 0 bytes, got {Buffer.Length}"); + throw new RealmProtocolException(realmState.ClientState, $"{MessageName(Id)} must be 0 bytes, got {Buffer.Length}"); return new MCP_MOTD().Invoke(new MessageContext(realmState, MessageDirection.ServerToClient)); } diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs index cf5ccbbe..f4fa45de 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_STARTUP.cs @@ -38,7 +38,7 @@ public override bool Invoke(MessageContext context) 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 GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be at least 65 bytes, got {Buffer.Length}"); + 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); @@ -55,7 +55,7 @@ public override bool Invoke(MessageContext context) realmState.ClientState = clientState; if (!Product.IsDiabloII(clientState.GameState.Product)) - throw new GameProtocolViolationException(realmState.ClientState, $"{MessageName(Id)} must be sent from D2DV or D2XP"); + 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 } })); From aae0c86091e5ed283d9e823eea481d323c64773b Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 20:47:10 -0400 Subject: [PATCH 04/12] chore(mcp): refactoring network byte order into helper method --- .../Protocols/Game/Messages/SID_LOGONREALMEX.cs | 7 ++----- src/Atlasd/Utilities/BytesHelper.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs index 5aad8853..0145c5d7 100644 --- a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs +++ b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs @@ -104,12 +104,8 @@ public override bool Invoke(MessageContext context) #endif IPAddress ipAddress = IPAddress.Parse(ipString); - byte[] ipBytes = ipAddress.GetAddressBytes(); - int ipInt = BitConverter.ToInt32(ipBytes, 0); - int networkOrderInt = IPAddress.NetworkToHostOrder(ipInt); - byte[] bytes = BitConverter.GetBytes(networkOrderInt).Reverse().ToArray(); - w.Write(bytes); + w.Write(ipAddress.GetBytes()); Settings.State.RootElement.TryGetProperty("battlenet", out var battlenetJson); battlenetJson.TryGetProperty("realm_listener", out var listenerJson); @@ -118,6 +114,7 @@ public override bool Invoke(MessageContext context) portJson.TryGetUInt16(out var port); + // strange protocol design ushort hostOrderPort = port; UInt32 networkOrderPort = (UInt32)IPAddress.HostToNetworkOrder((short)hostOrderPort); diff --git a/src/Atlasd/Utilities/BytesHelper.cs b/src/Atlasd/Utilities/BytesHelper.cs index 96a8e76a..ce1971fe 100644 --- a/src/Atlasd/Utilities/BytesHelper.cs +++ b/src/Atlasd/Utilities/BytesHelper.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Net; using System.Text; namespace Atlasd.Helpers @@ -18,6 +20,14 @@ public static byte[] GetBytes(this UInt32[] array) 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); From 36a9b570d24226d3d21d943649ff321ad9e050e9 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 20:51:10 -0400 Subject: [PATCH 05/12] fix(mcp): correcting placement of logging statement in logonrealmex --- .../Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs index 0145c5d7..148fcecb 100644 --- a/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs +++ b/src/Atlasd/Battlenet/Protocols/Game/Messages/SID_LOGONREALMEX.cs @@ -140,8 +140,6 @@ public override bool Invoke(MessageContext context) w.Write(chunk2); w.WriteByteString(Encoding.UTF8.GetBytes("")); - Logging.WriteLine(Logging.LogLevel.Debug, Logging.LogType.Client_Game, context.Client.RemoteEndPoint, $"[{Common.DirectionToString(context.Direction)}] {MessageName(Id)} ({4 + Buffer.Length} bytes)"); - // to allow associating game and realm connections after MCP_STARTUP Battlenet.Common.RealmClientStates.TryAdd(cookie, context.Client); } @@ -155,6 +153,8 @@ public override bool Invoke(MessageContext context) 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; } From 6bfdd2c160f77d2b6c679519701f72d6043e754e Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 21:02:12 -0400 Subject: [PATCH 06/12] fix(mcp): setting realm name in statstring --- src/Atlasd/Battlenet/ClientState.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Atlasd/Battlenet/ClientState.cs b/src/Atlasd/Battlenet/ClientState.cs index 461abbac..1cc9a119 100644 --- a/src/Atlasd/Battlenet/ClientState.cs +++ b/src/Atlasd/Battlenet/ClientState.cs @@ -575,6 +575,7 @@ public byte[] GenerateDiabloIIStatstring() { var partial = new byte[][] { + "Olympus".ToBytes(), new byte[] { 0x2C }, //comma RealmState.ActiveCharacter.Name.ToBytes(), new byte[] { 0x2C }, //comma From e69b7b1c7f2b0db66be9687aa46c1847c11c9e9b Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 21:05:01 -0400 Subject: [PATCH 07/12] chore(mcp): minor formatting tweak --- .../Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs index 328a888b..9b393467 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs @@ -13,10 +13,10 @@ class MCP_CHARLOGON : Message { public enum Statuses : UInt32 { - Success = 0x00, - NotFound = 0x46, - Failed = 0x7A, - Expired = 0x7B, + Success = 0x00, + NotFound = 0x46, + Failed = 0x7A, + Expired = 0x7B, }; // set gamestate char name From dd1a49c3f9f3696ab1c395904dc3fc2e198cf853 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Sun, 2 Apr 2023 21:13:07 -0400 Subject: [PATCH 08/12] fix(etc): reverting a change to the default realm name --- etc/atlasd.sample.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/atlasd.sample.json b/etc/atlasd.sample.json index dad856e4..c2f17971 100644 --- a/etc/atlasd.sample.json +++ b/etc/atlasd.sample.json @@ -139,7 +139,7 @@ }, "realm": { "host": "Anonymous", - "name": "Olympus" + "name": "Atlas" } }, "bnftp": { From 69d4575475fe4db51947616d72f224c6764f99cb Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Mon, 3 Apr 2023 22:20:34 -0400 Subject: [PATCH 09/12] feat(mcp): adding chardelete and charlogon packet support, adding guard for logging onto expansion character with d2dv --- src/Atlasd/Battlenet/Channel.cs | 6 +- src/Atlasd/Battlenet/Protocols/MCP/Message.cs | 2 + .../Protocols/MCP/Messages/MCP_CHARCREATE.cs | 6 ++ .../Protocols/MCP/Messages/MCP_CHARDELETE.cs | 63 ++++++++++++++++ .../Protocols/MCP/Messages/MCP_CHARLOGON.cs | 21 ++++-- .../Protocols/MCP/Messages/MCP_CHARUPGRADE.cs | 73 +++++++++++++++++++ .../Protocols/MCP/Models/Character.cs | 2 +- .../Battlenet/Protocols/MCP/Models/Realm.cs | 6 ++ .../Protocols/MCP/Models/Statstring.cs | 2 +- 9 files changed, 171 insertions(+), 10 deletions(-) diff --git a/src/Atlasd/Battlenet/Channel.cs b/src/Atlasd/Battlenet/Channel.cs index 68aebc07..5ce9cd93 100644 --- a/src/Atlasd/Battlenet/Channel.cs +++ b/src/Atlasd/Battlenet/Channel.cs @@ -799,7 +799,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); } } } @@ -919,7 +920,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/Protocols/MCP/Message.cs b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs index 3d7943dd..a8b73a06 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Message.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs @@ -31,6 +31,8 @@ public static Message FromByteArray(byte id, byte[] 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), diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs index 260255e9..15b3267e 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARCREATE.cs @@ -58,6 +58,12 @@ public override bool Invoke(MessageContext context) 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 } })); diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs index 2b6512d9..647d65ea 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARDELETE.cs @@ -1,11 +1,21 @@ 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; @@ -20,6 +30,59 @@ public MCP_CHARDELETE(byte[] 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_CHARLOGON.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs index 9b393467..b17da874 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLOGON.cs @@ -4,8 +4,10 @@ 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 { @@ -59,12 +61,19 @@ public override bool Invoke(MessageContext context) Statuses status; if (character != null) { - status = Statuses.Success; - - realmState.ActiveCharacter = character; - realmState.ClientState.GameState.CharacterName = character.Name.ToBytes(); - var statstring = realmState.ClientState.GenerateDiabloIIStatstring(); - gameState.Statstring = statstring; + 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 { diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs index 362dc005..390b6cd9 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARUPGRADE.cs @@ -1,11 +1,25 @@ 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; @@ -20,6 +34,65 @@ public MCP_CHARUPGRADE(byte[] 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/Models/Character.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs index 0f1ed66f..5ae2339f 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Character.cs @@ -8,7 +8,7 @@ class Character { public string Name { get; private set; } public CharacterTypes Type { get; private set; } - public CharacterFlags Flags { get; private set; } + public CharacterFlags Flags { get; set; } public LadderTypes Ladder { get; private set; } public Statstring Statstring { get; private set; } diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs index 5784a8d2..cd9f8f54 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Realm.cs @@ -35,6 +35,12 @@ public Character GetCharacter(string username, string name) 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; diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs b/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs index 635fd916..16d7dcb9 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Models/Statstring.cs @@ -42,7 +42,7 @@ class Statstring public Statstring(CharacterTypes type, CharacterFlags flags, LadderTypes ladder) { - Unknown_1 = 0x8F; + Unknown_1 = 0x84; Unknown_2 = 0x80; Head = 0xFF; Torso = 0xFF; From 20d81811915ae3517f6b0385d1c12d03c59ca230 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Mon, 3 Apr 2023 22:27:10 -0400 Subject: [PATCH 10/12] feat(mcp): adding legacy charlist packet support --- src/Atlasd/Battlenet/Protocols/MCP/Message.cs | 1 + .../Protocols/MCP/Messages/MCP_CHARLIST.cs | 67 ++++++++++++++++++- .../Protocols/MCP/Messages/MCP_CHARLIST2.cs | 6 -- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Message.cs b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs index a8b73a06..b2fbecab 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Message.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Message.cs @@ -36,6 +36,7 @@ public static Message FromByteArray(byte id, byte[] 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), diff --git a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs index ff713638..a8b0f6a2 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST.cs @@ -1,6 +1,10 @@ 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 { @@ -20,7 +24,68 @@ public MCP_CHARLIST(byte[] 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 index 4abfdcab..34d017fd 100644 --- a/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs +++ b/src/Atlasd/Battlenet/Protocols/MCP/Messages/MCP_CHARLIST2.cs @@ -10,12 +10,6 @@ namespace Atlasd.Battlenet.Protocols.MCP.Messages { class MCP_CHARLIST2 : Message { - public enum Statuses : UInt32 - { - Success = 1, - Unavailable = 2, - }; - public MCP_CHARLIST2() { Id = (byte)MessageIds.MCP_CHARLIST2; From 0c66edc43b1ee3dc67cd8238efe6e05aef118076 Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Mon, 3 Apr 2023 22:33:48 -0400 Subject: [PATCH 11/12] chore(etc): fixing indentation to match original --- etc/atlasd.sample.json | 90 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/etc/atlasd.sample.json b/etc/atlasd.sample.json index c2f17971..84a1cb86 100644 --- a/etc/atlasd.sample.json +++ b/etc/atlasd.sample.json @@ -96,52 +96,52 @@ "locales": null } ], - "battlenet": { - "emulation": { - "auto_refresh_pings": false, - "chat_gateway": { - "auto_account_create": true, - "receive_ping_messages": true - }, - "enable_ip_address_in_chatevents": false, - "grant_sudo_to_spoofed_admins": true, - "mask_admins_in_broadcasts": false, - "mask_admins_in_ban_message": true, - "mask_admins_in_kick_message": true, - "required_game_key_count": { - "D2DV": 1, - "D2XP": 2, - "JSTR": 0, - "SEXP": 0, - "STAR": 0, - "W2BN": 1, - "W3XP": 2, - "WAR3": 1 - }, - "statstring_updates": { - "D2DV": true, - "D2XP": true, - "DRTL": true, - "DSHR": true, - "W3XP": true, - "WAR3": true - } - }, - "listener": { - "interface": "0.0.0.0", - "port": 6112, - "tcp_nodelay": true - }, - "realm_listener": { - "interface": "0.0.0.0", - "port": 6113, - "tcp_nodelay": true - }, - "realm": { - "host": "Anonymous", - "name": "Atlas" - } + "battlenet": { + "emulation": { + "auto_refresh_pings": false, + "chat_gateway": { + "auto_account_create": true, + "receive_ping_messages": true + }, + "enable_ip_address_in_chatevents": false, + "grant_sudo_to_spoofed_admins": true, + "mask_admins_in_broadcasts": false, + "mask_admins_in_ban_message": true, + "mask_admins_in_kick_message": true, + "required_game_key_count": { + "D2DV": 1, + "D2XP": 2, + "JSTR": 0, + "SEXP": 0, + "STAR": 0, + "W2BN": 1, + "W3XP": 2, + "WAR3": 1 + }, + "statstring_updates": { + "D2DV": true, + "D2XP": true, + "DRTL": true, + "DSHR": true, + "W3XP": true, + "WAR3": true + } + }, + "listener": { + "interface": "0.0.0.0", + "port": 6112, + "tcp_nodelay": true }, + "realm_listener": { + "interface": "0.0.0.0", + "port": 6113, + "tcp_nodelay": true + }, + "realm": { + "host": "Anonymous", + "name": "Olympus" + } + }, "bnftp": { "root": "G:\\Projects\\Visual Studio\\Atlas\\var\\bnftp" }, From fde20f7c2232f7e047c824f7cdcde70a5c9f695c Mon Sep 17 00:00:00 2001 From: Richard Pianka Date: Mon, 3 Apr 2023 22:35:01 -0400 Subject: [PATCH 12/12] chore(etc): reverting incorrect realm name change --- etc/atlasd.sample.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/atlasd.sample.json b/etc/atlasd.sample.json index 84a1cb86..dab3dc47 100644 --- a/etc/atlasd.sample.json +++ b/etc/atlasd.sample.json @@ -139,7 +139,7 @@ }, "realm": { "host": "Anonymous", - "name": "Olympus" + "name": "Atlas" } }, "bnftp": {