From ecad90c7d41f6098931901fec5e5cc35259d6699 Mon Sep 17 00:00:00 2001 From: hondacrx Date: Mon, 15 Mar 2021 14:04:47 -0400 Subject: [PATCH] Core/RemoteAccess: Added support for remote access using email/password. --- Source/Framework/Logging/Log.cs | 1 + Source/Framework/Networking/AsyncAcceptor.cs | 18 ++ Source/Game/Chat/CommandHandler.cs | 59 +++++ Source/Game/Networking/RASocket.cs | 243 +++++++++++++++++++ Source/WorldServer/Server.cs | 13 + Source/WorldServer/WorldServer.conf.dist | 30 ++- Source/WorldServer/WorldServer.csproj | 2 +- 7 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 Source/Game/Networking/RASocket.cs diff --git a/Source/Framework/Logging/Log.cs b/Source/Framework/Logging/Log.cs index e4e6b9914..0d9c13060 100644 --- a/Source/Framework/Logging/Log.cs +++ b/Source/Framework/Logging/Log.cs @@ -402,6 +402,7 @@ public enum LogFilter ChatSystem, Cheat, Commands, + CommandsRA, Condition, Conversation, Garrison, diff --git a/Source/Framework/Networking/AsyncAcceptor.cs b/Source/Framework/Networking/AsyncAcceptor.cs index 6633848c6..1f0663dae 100644 --- a/Source/Framework/Networking/AsyncAcceptor.cs +++ b/Source/Framework/Networking/AsyncAcceptor.cs @@ -70,6 +70,24 @@ namespace Framework.Networking } } + public async void AsyncAccept() where T : ISocket + { + try + { + var socket = await _listener.AcceptSocketAsync(); + if (socket != null) + { + T newSocket = (T)Activator.CreateInstance(typeof(T), socket); + newSocket.Accept(); + + if (!_closed) + AsyncAccept(); + } + } + catch (ObjectDisposedException) + { } + } + public void Close() { if (_closed) diff --git a/Source/Game/Chat/CommandHandler.cs b/Source/Game/Chat/CommandHandler.cs index eb85d98d3..547a000c8 100644 --- a/Source/Game/Chat/CommandHandler.cs +++ b/Source/Game/Chat/CommandHandler.cs @@ -988,4 +988,63 @@ namespace Game.Chat return (byte)Global.WorldMgr.GetDefaultDbcLocale(); } } + + public class RemoteAccessHandler : CommandHandler + { + Action _reportToRA; + + public RemoteAccessHandler(Action reportToRA) : base() + { + _reportToRA = reportToRA; + } + + public override bool IsAvailable(ChatCommand cmd) + { + return cmd.AllowConsole; + } + + public override bool HasPermission(RBACPermissions permission) + { + return true; + } + + public override void SendSysMessage(string str, bool escapeCharacters) + { + _sentErrorMessage = true; + + _reportToRA(str); + } + + public override bool ParseCommand(string str) + { + if (str.IsEmpty()) + return false; + + // Console allows using commands both with and without leading indicator + if (str[0] == '.' || str[0] == '!') + str = str.Substring(1); + + return _ParseCommands(str); + } + + public override string GetNameLink() + { + return GetCypherString(CypherStrings.ConsoleCommand); + } + + public override bool NeedReportToTarget(Player chr) + { + return true; + } + + public override Locale GetSessionDbcLocale() + { + return Global.WorldMgr.GetDefaultDbcLocale(); + } + + public override byte GetSessionDbLocaleIndex() + { + return (byte)Global.WorldMgr.GetDefaultDbcLocale(); + } + } } \ No newline at end of file diff --git a/Source/Game/Networking/RASocket.cs b/Source/Game/Networking/RASocket.cs new file mode 100644 index 000000000..b9d7b5618 --- /dev/null +++ b/Source/Game/Networking/RASocket.cs @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2012-2020 CypherCore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +using Framework.Configuration; +using Framework.Constants; +using Framework.Cryptography; +using Framework.Database; +using Framework.Networking; +using Game.Chat; +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Game.Networking +{ + public class RASocket : ISocket + { + Socket _socket; + bool _closed; + IPAddress _remoteAddress; + byte[] _receiveBuffer; + + public RASocket(Socket socket) + { + _socket = socket; + _remoteAddress = ((IPEndPoint)_socket.RemoteEndPoint).Address; + _receiveBuffer = new byte[1024]; + } + + public void Accept() + { + // wait 1 second for active connections to send negotiation request + for (int counter = 0; counter < 10 && _socket.Available == 0; counter++) + Thread.Sleep(100); + + if (_socket.Available > 0) + { + // Handle subnegotiation + _socket.Receive(_receiveBuffer); + + // Send the end-of-negotiation packet + byte[] reply = { 0xFF, 0xF0 }; + _socket.Send(reply); + } + + Send("Authentication Required\r\n"); + Send("Email: "); + string userName = ReadString(); + if (userName.IsEmpty()) + { + CloseSocket(); + return; + } + + Log.outInfo(LogFilter.CommandsRA, $"Accepting RA connection from user {userName} (IP: {_remoteAddress})"); + + Send("Password: "); + string password = ReadString(); + if (password.IsEmpty()) + { + CloseSocket(); + return; + } + + if (!CheckAccessLevelAndPassword(userName, password)) + { + Send("Authentication failed\r\n"); + CloseSocket(); + return; + } + + Log.outInfo(LogFilter.CommandsRA, $"User {userName} (IP: {_remoteAddress}) authenticated correctly to RA"); + + // Authentication successful, send the motd + foreach (string line in Global.WorldMgr.GetMotd()) + Send(line); + + Send("\r\n"); + + // Read commands + for (; ; ) + { + Send("\r\nCypher>"); + string command = ReadString(); + + if (!ProcessCommand(command)) + break; + } + + CloseSocket(); + } + + public bool Update() + { + return IsOpen(); + } + + void Send(string str) + { + if (!IsOpen()) + return; + + _socket.Send(Encoding.UTF8.GetBytes(str)); + } + + string ReadString() + { + try + { + string str = ""; + do + { + int bytes = _socket.Receive(_receiveBuffer); + str = String.Concat(str, Encoding.UTF8.GetString(_receiveBuffer, 0, bytes)); + } + while (!str.Contains("\n")); + + return str.TrimEnd('\r', '\n'); + } + catch (Exception ex) + { + Log.outException(ex); + return ""; + } + } + + bool CheckAccessLevelAndPassword(string email, string password) + { + //"SELECT a.id, a.username FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id WHERE ba.email = ?" + PreparedStatement stmt = DB.Login.GetPreparedStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST); + stmt.AddValue(0, email); + SQLResult result = DB.Login.Query(stmt); + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.CommandsRA, $"User {email} does not exist in database"); + return false; + } + + uint accountId = result.Read(0); + string username = result.Read(1); + + stmt = DB.Login.GetPreparedStatement(LoginStatements.SEL_ACCOUNT_ACCESS_BY_ID); + stmt.AddValue(0, accountId); + result = DB.Login.Query(stmt); + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.CommandsRA, $"User {email} has no privilege to login"); + return false; + } + + //"SELECT SecurityLevel, RealmID FROM account_access WHERE AccountID = ? and (RealmID = ? OR RealmID = -1) ORDER BY SecurityLevel desc"); + if (result.Read(0) < ConfigMgr.GetDefaultValue("Ra.MinLevel", (byte)AccountTypes.Administrator)) + { + Log.outInfo(LogFilter.CommandsRA, $"User {email} has no privilege to login"); + return false; + } + else if (result.Read(1) != -1) + { + Log.outInfo(LogFilter.CommandsRA, $"User {email} has to be assigned on all realms (with RealmID = '-1')"); + return false; + } + + stmt = DB.Login.GetPreparedStatement(LoginStatements.SEL_CHECK_PASSWORD); + stmt.AddValue(0, accountId); + result = DB.Login.Query(stmt); + if (!result.IsEmpty()) + { + var salt = result.Read(0); + var verifier = result.Read(1); + + if (SRP6.CheckLogin(username, password, salt, verifier)) + return true; + } + + Log.outInfo(LogFilter.CommandsRA, $"Wrong password for user: {email}"); + return false; + } + + bool ProcessCommand(string command) + { + if (command.Length == 0) + return true; + + Log.outInfo(LogFilter.CommandsRA, $"Received command: {command}"); + + // handle quit, exit and logout commands to terminate connection + if (command == "quit" || command == "exit" || command == "logout") + { + Send("Closing\r\n"); + return false; + } + + RemoteAccessHandler cmd = new RemoteAccessHandler(CommandPrint); + cmd.ParseCommand(command); + + return true; + } + + public bool IsOpen() { return _socket.Connected; } + + public void CloseSocket() + { + if (_socket == null) + return; + + try + { + _socket.Shutdown(SocketShutdown.Both); + _socket.Close(); + } + catch (Exception ex) + { + Log.outDebug(LogFilter.Network, "WorldSocket.CloseSocket: {0} errored when shutting down socket: {1}", _remoteAddress.ToString(), ex.Message); + } + } + + void CommandPrint(string text) + { + if (text.IsEmpty()) + return; + + Send(text); + } + } +} + diff --git a/Source/WorldServer/Server.cs b/Source/WorldServer/Server.cs index e29f92bdb..66b1a5db3 100644 --- a/Source/WorldServer/Server.cs +++ b/Source/WorldServer/Server.cs @@ -18,6 +18,7 @@ using Framework.Configuration; using Framework.Constants; using Framework.Database; +using Framework.Networking; using Game; using Game.Chat; using Game.Networking; @@ -58,6 +59,18 @@ namespace WorldServer Global.WorldMgr.SetInitialWorldSettings(); + // Start the Remote Access port (acceptor) if enabled + if (ConfigMgr.GetDefaultValue("Ra.Enable", false)) + { + int raPort = ConfigMgr.GetDefaultValue("Ra.Port", 3443); + string raListener = ConfigMgr.GetDefaultValue("Ra.IP", "0.0.0.0"); + AsyncAcceptor raAcceptor = new(); + if (!raAcceptor.Start(raListener, raPort)) + Log.outError(LogFilter.Server, "Failed to initialize RemoteAccess Socket Server"); + else + raAcceptor.AsyncAccept(); + } + // Launch the worldserver listener socket int worldPort = WorldConfig.GetIntValue(WorldCfg.PortWorld); string worldListener = ConfigMgr.GetDefaultValue("BindIP", "0.0.0.0"); diff --git a/Source/WorldServer/WorldServer.conf.dist b/Source/WorldServer/WorldServer.conf.dist index 6537f821d..9c4191d75 100644 --- a/Source/WorldServer/WorldServer.conf.dist +++ b/Source/WorldServer/WorldServer.conf.dist @@ -2852,7 +2852,7 @@ Network.TcpNodelay = 1 ################################################################################################### ################################################################################################### -# CONSOLE +# CONSOLE AND REMOTE ACCESS # # Console.Enable (NYI) # Description: Enable console. @@ -2861,6 +2861,34 @@ Network.TcpNodelay = 1 Console.Enable = 1 +# +# Ra.Enable +# Description: Enable remote console (telnet). +# Default: 0 - (Disabled) +# 1 - (Enabled) + +Ra.Enable = 0 + +# +# Ra.IP +# Description: Bind remote access to IP/hostname. +# Default: "0.0.0.0" - (Bind to all IPs on the system) + +Ra.IP = "0.0.0.0" + +# +# Ra.Port +# Description: TCP port to reach the remote console. +# Default: 3443 + +Ra.Port = 3443 + +# +# Ra.MinLevel +# Description: Required security level to use the remote console. +# Default: 3 + +Ra.MinLevel = 3 # ################################################################################################### diff --git a/Source/WorldServer/WorldServer.csproj b/Source/WorldServer/WorldServer.csproj index 5c286628a..68c591753 100644 --- a/Source/WorldServer/WorldServer.csproj +++ b/Source/WorldServer/WorldServer.csproj @@ -5,7 +5,7 @@ net5.0 Blue.ico WorldServer.Server - 8.0 + 9.0