diff --git a/Source/BNetServer/Managers/Global.cs b/Source/BNetServer/Managers/Global.cs index ec618b59d..458b558a6 100644 --- a/Source/BNetServer/Managers/Global.cs +++ b/Source/BNetServer/Managers/Global.cs @@ -2,9 +2,13 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using BNetServer; +using BNetServer.Networking; +using BNetServer.REST; public static class Global { public static RealmManager RealmMgr { get { return RealmManager.Instance; } } + public static SessionManager SessionMgr { get { return SessionManager.Instance; } } + public static LoginRESTService LoginService { get { return LoginRESTService.Instance; } } public static LoginServiceManager LoginServiceMgr { get { return LoginServiceManager.Instance; } } } \ No newline at end of file diff --git a/Source/BNetServer/Managers/LoginServiceManager.cs b/Source/BNetServer/Managers/LoginServiceManager.cs index c6ed03226..46e286b06 100644 --- a/Source/BNetServer/Managers/LoginServiceManager.cs +++ b/Source/BNetServer/Managers/LoginServiceManager.cs @@ -4,13 +4,11 @@ using BNetServer.Networking; using Framework.Configuration; using Framework.Constants; -using Framework.Web; using Google.Protobuf; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq.Expressions; -using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; @@ -19,67 +17,12 @@ namespace BNetServer public class LoginServiceManager : Singleton { ConcurrentDictionary<(uint ServiceHash, uint MethodId), BnetServiceHandler> serviceHandlers = new(); - FormInputs formInputs = new(); - string[] _hostnames = new string[2]; - IPAddress[] _addresses = new IPAddress[2]; - int _port; X509Certificate2 certificate; - LoginServiceManager() { } public void Initialize() { - _port = ConfigMgr.GetDefaultValue("LoginREST.Port", 8081); - if (_port < 0 || _port > 0xFFFF) - { - Log.outError(LogFilter.Network, $"Specified login service port ({_port}) out of allowed range (1-65535), defaulting to 8081"); - _port = 8081; - } - - _hostnames[0] = ConfigMgr.GetDefaultValue("LoginREST.ExternalAddress", "127.0.0.1"); - var externalAddress = Dns.GetHostAddresses(_hostnames[0], System.Net.Sockets.AddressFamily.InterNetwork); - if (externalAddress == null || externalAddress.Empty()) - { - Log.outError(LogFilter.Network, $"Could not resolve LoginREST.ExternalAddress {_hostnames[0]}"); - return; - } - - _addresses[0] = externalAddress[0]; - - _hostnames[1] = ConfigMgr.GetDefaultValue("LoginREST.LocalAddress", "127.0.0.1"); - var localAddress = Dns.GetHostAddresses(_hostnames[1], System.Net.Sockets.AddressFamily.InterNetwork); - if (localAddress == null || localAddress.Empty()) - { - Log.outError(LogFilter.Network, $"Could not resolve LoginREST.ExternalAddress {_hostnames[1]}"); - return; - } - - _addresses[1] = localAddress[0]; - - // set up form inputs - formInputs.Type = "LOGIN_FORM"; - - var input = new FormInput(); - input.Id = "account_name"; - input.Type = "text"; - input.Label = "E-mail"; - input.MaxLength = 320; - formInputs.Inputs.Add(input); - - input = new FormInput(); - input.Id = "password"; - input.Type = "password"; - input.Label = "Password"; - input.MaxLength = 16; - formInputs.Inputs.Add(input); - - input = new FormInput(); - input.Id = "log_in_submit"; - input.Type = "submit"; - input.Label = "Log In"; - formInputs.Inputs.Add(input); - certificate = new X509Certificate2(ConfigMgr.GetDefaultValue("CertificatesFile", "./BNetServer.pfx")); Assembly currentAsm = Assembly.GetExecutingAssembly(); @@ -117,21 +60,6 @@ namespace BNetServer return serviceHandlers.LookupByKey((serviceHash, methodId)); } - public string GetHostnameForClient(IPEndPoint address) - { - if (IPAddress.IsLoopback(address.Address)) - return _hostnames[1]; - - return _hostnames[0]; - } - - public int GetPort() { return _port; } - - public FormInputs GetFormInput() - { - return formInputs; - } - public X509Certificate2 GetCertificate() { return certificate; diff --git a/Source/BNetServer/Networking/RestSession.cs b/Source/BNetServer/Networking/RestSession.cs deleted file mode 100644 index 87d18d5ed..000000000 --- a/Source/BNetServer/Networking/RestSession.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) CypherCore All rights reserved. -// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. - -using Framework.Configuration; -using Framework.Constants; -using Framework.Database; -using Framework.Networking; -using Framework.Serialization; -using Framework.Web; -using System; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; - -namespace BNetServer.Networking -{ - public class RestSession : SSLSocket - { - public RestSession(Socket socket) : base(socket) { } - - public override void Accept() - { - AsyncHandshake(Global.LoginServiceMgr.GetCertificate()); - } - - public async override void ReadHandler(byte[] data, int receivedLength) - { - var httpRequest = HttpHelper.ParseRequest(data, receivedLength); - if (httpRequest == null) - return; - - switch (httpRequest.Method) - { - case "GET": - default: - SendResponse(HttpCode.Ok, Global.LoginServiceMgr.GetFormInput()); - break; - case "POST": - HandleLoginRequest(httpRequest); - return; - } - - await AsyncRead(); - } - - public void HandleLoginRequest(HttpHeader request) - { - LogonData loginForm = Json.CreateObject(request.Content); - LogonResult loginResult = new(); - if (loginForm == null) - { - loginResult.AuthenticationState = "LOGIN"; - loginResult.ErrorCode = "UNABLE_TO_DECODE"; - loginResult.ErrorMessage = "There was an internal error while connecting to Battle.net. Please try again later."; - SendResponse(HttpCode.BadRequest, loginResult); - return; - } - - string login = ""; - string password = ""; - - for (int i = 0; i < loginForm.Inputs.Count; ++i) - { - switch (loginForm.Inputs[i].Id) - { - case "account_name": - login = loginForm.Inputs[i].Value; - break; - case "password": - password = loginForm.Inputs[i].Value; - break; - } - } - - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelBnetAuthentication); - stmt.AddValue(0, login); - - SQLResult result = DB.Login.Query(stmt); - if (!result.IsEmpty()) - { - uint accountId = result.Read(0); - string pass_hash = result.Read(1); - uint failedLogins = result.Read(2); - string loginTicket = result.Read(3); - uint loginTicketExpiry = result.Read(4); - bool isBanned = result.Read(5) != 0; - - if (CalculateShaPassHash(login.ToUpper(), password.ToUpper()) == pass_hash) - { - if (loginTicket.IsEmpty() || loginTicketExpiry < Time.UnixTime) - { - byte[] ticket = Array.Empty().GenerateRandomKey(20); - loginTicket = "TC-" + ticket.ToHexString(); - } - - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UpdBnetAuthentication); - stmt.AddValue(0, loginTicket); - stmt.AddValue(1, Time.UnixTime + 3600); - stmt.AddValue(2, accountId); - - DB.Login.Execute(stmt); - loginResult.LoginTicket = loginTicket; - } - else if (!isBanned) - { - uint maxWrongPassword = ConfigMgr.GetDefaultValue("WrongPass.MaxCount", 0u); - - if (ConfigMgr.GetDefaultValue("WrongPass.Logging", false)) - Log.outDebug(LogFilter.Network, $"[{request.Host}, Account {login}, Id {accountId}] Attempted to connect with wrong password!"); - - if (maxWrongPassword != 0) - { - SQLTransaction trans = new(); - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UpdBnetFailedLogins); - stmt.AddValue(0, accountId); - trans.Append(stmt); - - ++failedLogins; - - Log.outDebug(LogFilter.Network, $"MaxWrongPass : {maxWrongPassword}, failed_login : {accountId}"); - - if (failedLogins >= maxWrongPassword) - { - BanMode banType = ConfigMgr.GetDefaultValue("WrongPass.BanType", BanMode.IP); - int banTime = ConfigMgr.GetDefaultValue("WrongPass.BanTime", 600); - - if (banType == BanMode.Account) - { - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.InsBnetAccountAutoBanned); - stmt.AddValue(0, accountId); - } - else - { - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.InsIpAutoBanned); - stmt.AddValue(0, request.Host); - } - - stmt.AddValue(1, banTime); - trans.Append(stmt); - - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UpdBnetResetFailedLogins); - stmt.AddValue(0, accountId); - trans.Append(stmt); - } - - DB.Login.CommitTransaction(trans); - } - } - - loginResult.AuthenticationState = "DONE"; - SendResponse(HttpCode.Ok, loginResult); - } - else - { - loginResult.AuthenticationState = "LOGIN"; - loginResult.ErrorCode = "UNABLE_TO_DECODE"; - loginResult.ErrorMessage = "There was an internal error while connecting to Battle.net. Please try again later."; - SendResponse(HttpCode.BadRequest, loginResult); - } - } - - async void SendResponse(HttpCode code, T response) - { - await AsyncWrite(HttpHelper.CreateResponse(code, Json.CreateString(response))); - } - - string CalculateShaPassHash(string name, string password) - { - SHA256 sha256 = SHA256.Create(); - var email = sha256.ComputeHash(Encoding.UTF8.GetBytes(name)); - return sha256.ComputeHash(Encoding.UTF8.GetBytes(email.ToHexString() + ":" + password)).ToHexString(true); - } - } -} diff --git a/Source/BNetServer/Networking/Services/Authentication.cs b/Source/BNetServer/Networking/Services/Authentication.cs index 91606784a..0fdd44b07 100644 --- a/Source/BNetServer/Networking/Services/Authentication.cs +++ b/Source/BNetServer/Networking/Services/Authentication.cs @@ -39,11 +39,9 @@ namespace BNetServer.Networking os = logonRequest.Platform; build = (uint)logonRequest.ApplicationVersion; - var hostname = Global.LoginServiceMgr.GetHostnameForClient(GetRemoteIpEndPoint()); - ChallengeExternalRequest externalChallenge = new(); externalChallenge.PayloadType = "web_auth_url"; - externalChallenge.Payload = ByteString.CopyFromUtf8($"https://{Global.LoginServiceMgr.GetHostnameForClient(GetRemoteIpEndPoint())}:{Global.LoginServiceMgr.GetPort()}/bnetserver/login/"); + externalChallenge.Payload = ByteString.CopyFromUtf8($"https://{Global.LoginService.GetHostnameForClient(GetRemoteIpAddress())}:{Global.LoginService.GetPort()}/bnetserver/login/"); SendRequest((uint)OriginalHash.ChallengeListener, 3, externalChallenge); return BattlenetRpcErrorCode.Ok; @@ -55,7 +53,7 @@ namespace BNetServer.Networking if (verifyWebCredentialsRequest.WebCredentials.IsEmpty) return BattlenetRpcErrorCode.Denied; - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelBnetAccountInfo); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_ACCOUNT_INFO); stmt.AddValue(0, verifyWebCredentialsRequest.WebCredentials.ToStringUtf8()); SQLResult result = DB.Login.Query(stmt); @@ -81,7 +79,7 @@ namespace BNetServer.Networking } while (characterCountsResult.NextRow()); } - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelBnetLastPlayerCharacters); + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_LAST_PLAYER_CHARACTERS); stmt.AddValue(0, accountInfo.Id); SQLResult lastPlayerCharactersResult = DB.Login.Query(stmt); @@ -102,7 +100,7 @@ namespace BNetServer.Networking } while (lastPlayerCharactersResult.NextRow()); } - string ip_address = GetRemoteIpEndPoint().ToString(); + string ip_address = GetRemoteIpAddress().ToString(); // If the IP is 'locked', check that the player comes indeed from the correct IP address if (accountInfo.IsLockedToIP) diff --git a/Source/BNetServer/Networking/Services/GameUtilities.cs b/Source/BNetServer/Networking/Services/GameUtilities.cs index dbb456814..ca6efd24b 100644 --- a/Source/BNetServer/Networking/Services/GameUtilities.cs +++ b/Source/BNetServer/Networking/Services/GameUtilities.cs @@ -5,11 +5,14 @@ using Bgs.Protocol; using Bgs.Protocol.GameUtilities.V1; using Framework.Constants; using Framework.Database; -using Framework.Serialization; +using Framework.IO; using Framework.Web; +using Framework.Web.Rest.Realmlist; using Google.Protobuf; using System; using System.Collections.Generic; +using System.Text; +using System.Text.Json; namespace BNetServer.Networking { @@ -36,7 +39,7 @@ namespace BNetServer.Networking { Bgs.Protocol.Attribute attr = request.Attribute[i]; if (attr.Name.Contains("Command_")) - { + { command = attr; Params[removeSuffix(attr.Name)] = attr.Value; } @@ -80,10 +83,15 @@ namespace BNetServer.Networking Variant identity = Params.LookupByKey("Param_Identity"); if (identity != null) { - var realmListTicketIdentity = Json.CreateObject(identity.BlobValue.ToStringUtf8(), true); - var gameAccount = accountInfo.GameAccounts.LookupByKey(realmListTicketIdentity.GameAccountId); - if (gameAccount != null) - gameAccountInfo = gameAccount; + string jsonString = identity.BlobValue.ToStringUtf8().TrimEnd('\0'); + int jsonStart = jsonString.IndexOf(':'); + if (jsonStart != -1) + { + var realmListTicketIdentity = JsonSerializer.Deserialize(jsonString.Substring(jsonStart + 1)); + var gameAccount = accountInfo.GameAccounts.LookupByKey(realmListTicketIdentity.GameAccountId); + if (gameAccount != null) + gameAccountInfo = gameAccount; + } } if (gameAccountInfo == null) @@ -98,18 +106,23 @@ namespace BNetServer.Networking Variant clientInfo = Params.LookupByKey("Param_ClientInfo"); if (clientInfo != null) { - var realmListTicketClientInformation = Json.CreateObject(clientInfo.BlobValue.ToStringUtf8(), true); - clientInfoOk = true; - int i = 0; - foreach (byte b in realmListTicketClientInformation.Info.Secret) - clientSecret[i++] = b; + string jsonString = clientInfo.BlobValue.ToStringUtf8().Trim('\0'); + int jsonStart = jsonString.IndexOf(':'); + if (jsonStart != -1) + { + var realmListTicketClientInformation = JsonSerializer.Deserialize(jsonString.Substring(jsonStart + 1)); + clientInfoOk = true; + int i = 0; + foreach (byte b in realmListTicketClientInformation.Info.Secret) + clientSecret[i++] = b; + } } if (!clientInfoOk) return BattlenetRpcErrorCode.WowServicesDeniedRealmListTicket; - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UpdBnetLastLoginInfo); - stmt.AddValue(0, GetRemoteIpEndPoint().ToString()); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_LAST_LOGIN_INFO); + stmt.AddValue(0, GetRemoteIpAddress().ToString()); stmt.AddValue(1, (byte)locale.ToEnum()); stmt.AddValue(2, os); stmt.AddValue(3, accountInfo.Id); @@ -119,7 +132,7 @@ namespace BNetServer.Networking var attribute = new Bgs.Protocol.Attribute(); attribute.Name = "Param_RealmListTicket"; attribute.Value = new Variant(); - attribute.Value.BlobValue = ByteString.CopyFrom("AuthRealmListTicket", System.Text.Encoding.UTF8); + attribute.Value.BlobValue = ByteString.CopyFrom("AuthRealmListTicket", Encoding.UTF8); response.Attribute.Add(attribute); return BattlenetRpcErrorCode.Ok; @@ -197,7 +210,10 @@ namespace BNetServer.Networking realmCharacterCounts.Counts.Add(countEntry); } - compressed = Json.Deflate("JSONRealmCharacterCountList", realmCharacterCounts); + var jsonData = Encoding.UTF8.GetBytes("JSONRealmCharacterCountList:" + JsonSerializer.Serialize(realmCharacterCounts) + "\0"); + var compressedData = ZLib.Compress(jsonData); + + compressed = BitConverter.GetBytes(jsonData.Length).Combine(compressedData); attribute = new Bgs.Protocol.Attribute(); attribute.Name = "Param_CharacterCountList"; @@ -211,9 +227,9 @@ namespace BNetServer.Networking { Variant realmAddress = Params.LookupByKey("Param_RealmAddress"); if (realmAddress != null) - return Global.RealmMgr.JoinRealm((uint)realmAddress.UintValue, build, GetRemoteIpEndPoint().Address, clientSecret, (Locale)Enum.Parse(typeof(Locale), locale), os, gameAccountInfo.Name, response); + return Global.RealmMgr.JoinRealm((uint)realmAddress.UintValue, build, GetRemoteIpAddress(), clientSecret, (Locale)Enum.Parse(typeof(Locale), locale), os, _timezoneOffset, gameAccountInfo.Name, response); return BattlenetRpcErrorCode.WowServicesInvalidJoinTicket; } } -} +} \ No newline at end of file diff --git a/Source/BNetServer/Networking/Session.cs b/Source/BNetServer/Networking/Session.cs index 0525dc75d..9322ec2bb 100644 --- a/Source/BNetServer/Networking/Session.cs +++ b/Source/BNetServer/Networking/Session.cs @@ -12,10 +12,11 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; namespace BNetServer.Networking { - partial class Session : SSLSocket + public partial class Session : SSLSocket { AccountInfo accountInfo; GameAccountInfo gameAccountInfo; @@ -23,6 +24,7 @@ namespace BNetServer.Networking string locale; string os; uint build; + TimeSpan _timezoneOffset; string ipCountry; byte[] clientSecret; @@ -39,19 +41,19 @@ namespace BNetServer.Networking responseCallbacks = new Dictionary>(); } - public override void Accept() + public override void Start() { - string ipAddress = GetRemoteIpEndPoint().ToString(); + string ipAddress = GetRemoteIpAddress().ToString(); Log.outInfo(LogFilter.Network, $"{GetClientInfo()} Connection Accepted."); // Verify that this IP is not in the ip_banned table - DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DelExpiredIpBans)); + DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DEL_EXPIRED_IP_BANS)); - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelIpInfo); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_IP_INFO); stmt.AddValue(0, ipAddress); - stmt.AddValue(1, BitConverter.ToUInt32(GetRemoteIpEndPoint().Address.GetAddressBytes(), 0)); + stmt.AddValue(1, BitConverter.ToUInt32(GetRemoteIpAddress().GetAddressBytes(), 0)); - queryProcessor.AddCallback(DB.Login.AsyncQuery(stmt).WithCallback(async result => + queryProcessor.AddCallback(DB.Login.AsyncQuery(stmt).WithCallback(async result => { if (!result.IsEmpty()) { @@ -78,6 +80,18 @@ namespace BNetServer.Networking })); } + public async override Task HandshakeHandler(Exception ex = null) + { + if (ex != null) + { + Log.outError(LogFilter.Session, $"{GetClientInfo()} SSL Handshake failed {ex.Message}"); + CloseSocket(); + return; + } + + await AsyncRead(); + } + public override bool Update() { if (!base.Update()) @@ -103,7 +117,7 @@ namespace BNetServer.Networking header.MergeFrom(data, readPos, headerLength); readPos += headerLength; - var stream = new CodedInputStream(data, readPos, (int)header.Size); + var stream = new CodedInputStream(data, readPos, (int)header.Size); readPos += (int)header.Size; if (header.ServiceId != 0xFE && header.ServiceHash != 0) @@ -181,7 +195,7 @@ namespace BNetServer.Networking { var size = (ushort)header.CalculateSize(); byte[] bytes = new byte[2]; - bytes[0] = (byte)((size >> 8) & 0xff); + bytes[0] = (byte)(size >> 8 & 0xff); bytes[1] = (byte)(size & 0xff); var headerSizeBytes = BitConverter.GetBytes((ushort)header.CalculateSize()); @@ -192,7 +206,7 @@ namespace BNetServer.Networking public string GetClientInfo() { - string stream = '[' + GetRemoteIpEndPoint().ToString(); + string stream = '[' + GetRemoteIpAddress().ToString(); if (accountInfo != null && !accountInfo.Login.IsEmpty()) stream += ", Account: " + accountInfo.Login; @@ -206,7 +220,7 @@ namespace BNetServer.Networking } public class AccountInfo - { + { public uint Id; public string Login; public bool IsLockedToIP; diff --git a/Source/BNetServer/Networking/SessionManager.cs b/Source/BNetServer/Networking/SessionManager.cs new file mode 100644 index 000000000..b8fc21910 --- /dev/null +++ b/Source/BNetServer/Networking/SessionManager.cs @@ -0,0 +1,27 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Networking; +using System.Net.Sockets; + +namespace BNetServer.Networking +{ + public class SessionManager : SocketManager + { + public static SessionManager Instance { get; } = new SessionManager(); + + public override bool StartNetwork(string bindIp, int port, int threadCount = 1) + { + if (!base.StartNetwork(bindIp, port, threadCount)) + return false; + + Acceptor.AsyncAcceptSocket(OnSocketAccept); + return true; + } + + static void OnSocketAccept(Socket sock) + { + Global.SessionMgr.OnSocketOpen(sock); + } + } +} \ No newline at end of file diff --git a/Source/BNetServer/REST/LoginHttpSession.cs b/Source/BNetServer/REST/LoginHttpSession.cs new file mode 100644 index 000000000..409d3d5d5 --- /dev/null +++ b/Source/BNetServer/REST/LoginHttpSession.cs @@ -0,0 +1,109 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Constants; +using Framework.Cryptography; +using Framework.Database; +using Framework.Networking.Http; +using System; +using System.Linq; +using System.Net.Sockets; + +namespace BNetServer.REST +{ + public class LoginSessionState : SessionState + { + public BnetSRP6Base Srp; + } + + public class LoginHttpSession : SslSocket + { + public static string SESSION_ID_COOKIE = "JSESSIONID"; + + public LoginHttpSession(Socket socket) : base(socket) { } + + public override void Start() + { + string ip_address = GetRemoteIpAddress().ToString(); + Log.outTrace(LogFilter.Http, $"{GetClientInfo()} Accepted connection"); + + // Verify that this IP is not in the ip_banned table + DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DEL_EXPIRED_IP_BANS)); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_IP_INFO); + stmt.AddValue(0, ip_address); + + _queryProcessor.AddCallback(DB.Login.AsyncQuery(stmt).WithCallback(CheckIpCallback)); + } + + async void CheckIpCallback(SQLResult result) + { + if (!result.IsEmpty()) + { + bool banned = false; + do + { + if (result.Read(0) != 0) + banned = true; + + } while (result.NextRow()); + + if (banned) + { + Log.outDebug(LogFilter.Http, $"{GetClientInfo()} tries to log in using banned IP!"); + CloseSocket(); + return; + } + } + + await AsyncHandshake(Global.LoginServiceMgr.GetCertificate()); + } + + public override RequestHandlerResult RequestHandler(RequestContext context) + { + return Global.LoginService.GetDispatcherService().HandleRequest(this, context); + } + + public override SessionState ObtainSessionState(RequestContext context) + { + SessionState state = null; + + var cookieString = context.request.Cookie; + if (!cookieString.IsEmpty()) + { + var cookies = cookieString.Split(';', StringSplitOptions.RemoveEmptyEntries); + int eq = 0; + var sessionIdItr = cookies.FirstOrDefault(cookie => + { + string name = cookie; + eq = cookie.IndexOf('='); + if (eq != -1) + name = cookie.Substring(0, eq); + + return name == SESSION_ID_COOKIE; + }); + if (!sessionIdItr.IsEmpty()) + { + string value = sessionIdItr.Substring(eq + 1); + state = Global.LoginService.GetSessionService().FindAndRefreshSessionState(value, GetRemoteIpAddress()); + } + } + + if (state == null) + { + state = Global.LoginService.CreateNewSessionState(GetRemoteIpAddress()); + + string host = context.request.Host; + int port = host.IndexOf(':'); + if (port != -1) + host = host.Remove(host.Length - port); + + context.response.Cookie = $"{SESSION_ID_COOKIE}={state.Id}; Path=/bnetserver; Domain={host}; Secure; HttpOnly; SameSite=None"; + } + + return state; + } + + public LoginSessionState GetSessionState() { return _state as LoginSessionState; } + } +} diff --git a/Source/BNetServer/REST/LoginRESTService.cs b/Source/BNetServer/REST/LoginRESTService.cs new file mode 100644 index 000000000..323c1c0b8 --- /dev/null +++ b/Source/BNetServer/REST/LoginRESTService.cs @@ -0,0 +1,584 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Configuration; +using Framework.Constants; +using Framework.Cryptography; +using Framework.Database; +using Framework.Networking.Http; +using Framework.Web; +using Framework.Web.Rest.Login; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace BNetServer.REST +{ + public class LoginRESTService : HttpService + { + public static LoginRESTService Instance { get; } = new LoginRESTService(); + + FormInputs _formInputs = new(); + int _port; + string[] _hostnames = new string[2]; + IPAddress[] _addresses = new IPAddress[2]; + uint _loginTicketDuration; + + LoginRESTService() : base(LogFilter.Http) { } + + public override bool StartNetwork(string bindIp, int port, int threadCount = 1) + { + if (!base.StartNetwork(bindIp, port, threadCount)) + return false; + + RegisterHandler(HttpMethod.Get, "/bnetserver/login/", HandleGetForm); + RegisterHandler(HttpMethod.Get, "/bnetserver/gameAccounts/", HandleGetGameAccounts); + RegisterHandler(HttpMethod.Get, "/bnetserver/portal/", HandleGetPortal); + RegisterHandler(HttpMethod.Post, "/bnetserver/login/", HandlePostLogin, RequestHandlerFlag.DoNotLogRequestContent); + RegisterHandler(HttpMethod.Post, "/bnetserver/login/srp/", HandlePostLoginSrpChallenge); + RegisterHandler(HttpMethod.Post, "/bnetserver/refreshLoginTicket/", HandlePostRefreshLoginTicket); + + _port = port; + + _hostnames[0] = ConfigMgr.GetDefaultValue("LoginREST.ExternalAddress", "127.0.0.1"); + var externalAddress = Dns.GetHostAddresses(_hostnames[0], System.Net.Sockets.AddressFamily.InterNetwork); + if (externalAddress == null || externalAddress.Empty()) + { + Log.outError(LogFilter.Network, $"Could not resolve LoginREST.ExternalAddress {_hostnames[0]}"); + return false; + } + + _addresses[0] = externalAddress[0]; + + _hostnames[1] = ConfigMgr.GetDefaultValue("LoginREST.LocalAddress", "127.0.0.1"); + var localAddress = Dns.GetHostAddresses(_hostnames[1], AddressFamily.InterNetwork); + if (localAddress == null || localAddress.Empty()) + { + Log.outError(LogFilter.Http, $"Could not resolve LoginREST.LocalAddress {_hostnames[1]}"); + return false; + } + + _addresses[1] = localAddress[0]; + + // set up form inputs + _formInputs.Type = "LOGIN_FORM"; + + var input = new FormInput(); + input.Id = "account_name"; + input.Type = "text"; + input.Label = "E-mail"; + input.MaxLength = 320; + _formInputs.Inputs.Add(input); + + input = new FormInput(); + input.Id = "password"; + input.Type = "password"; + input.Label = "Password"; + input.MaxLength = 128; + _formInputs.Inputs.Add(input); + + input = new FormInput(); + input.Id = "log_in_submit"; + input.Type = "submit"; + input.Label = "Log In"; + _formInputs.Inputs.Add(input); + + _loginTicketDuration = ConfigMgr.GetDefaultValue("LoginREST.TicketDuration", 3600u); + + MigrateLegacyPasswordHashes(); + + Acceptor.AsyncAcceptSocket(OnSocketAccept); + + return true; + } + + public string GetHostnameForClient(IPAddress address) + { + if (IPAddress.IsLoopback(address)) + return _hostnames[1]; + + return _hostnames[0]; + } + + string ExtractAuthorization(HttpHeader request) + { + if (request.Authorization.IsEmpty()) + return null; + + string BASIC_PREFIX = "Basic "; + + string authorization = request.Authorization; + if (authorization.StartsWith(BASIC_PREFIX)) + authorization = authorization.Remove(0, BASIC_PREFIX.Length); + + byte[] decoded = Convert.FromBase64String(authorization); + if (decoded.Empty()) + return null; + + string decodedHeader = Encoding.UTF8.GetString(decoded); + int ticketEnd = decodedHeader.IndexOf(':'); + if (ticketEnd != -1) + decodedHeader = decodedHeader.Remove(decodedHeader.Length - ticketEnd); + + return decodedHeader; + } + + RequestHandlerResult HandleGetForm(LoginHttpSession session, RequestContext context) + { + FormInputs form = _formInputs; + form.SrpUrl = $"https://{GetHostnameForClient(session.GetRemoteIpAddress())}:{_port}/bnetserver/login/srp/"; + + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(form); + return RequestHandlerResult.Handled; + } + + RequestHandlerResult HandleGetGameAccounts(LoginHttpSession session, RequestContext context) + { + string ticket = ExtractAuthorization(context.request); + if (ticket.IsEmpty()) + return dispatcherService.HandleUnauthorized(session, context); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST); + stmt.AddValue(0, ticket); + session.QueueQuery(DB.Login.AsyncQuery(stmt).WithCallback(result => + { + GameAccountList gameAccounts = new(); + if (!result.IsEmpty()) + { + string formatDisplayName(string name) + { + var hashPos = name.IndexOf('#'); + if (hashPos != -1) + return "WoW" + name.Substring(++hashPos); + else + return name; + }; + + long now = Time.UnixTime; + do + { + GameAccountInfo gameAccount = new(); + gameAccount.DisplayName = formatDisplayName(result.Read(0)); + gameAccount.Expansion = result.Read(1); + if (!result.IsNull(2)) + { + uint banDate = result.Read(2); + uint unbanDate = result.Read(3); + gameAccount.IsSuspended = unbanDate > now; + gameAccount.IsBanned = banDate == unbanDate; + gameAccount.SuspensionReason = result.Read(4); + gameAccount.SuspensionExpires = unbanDate; + } + + gameAccounts.GameAccounts.Add(gameAccount); + } while (result.NextRow()); + } + + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(gameAccounts); + session.SendResponse(context); + })); + + return RequestHandlerResult.Async; + } + + RequestHandlerResult HandleGetPortal(LoginHttpSession session, RequestContext context) + { + context.response.ContentType = "text/plain"; + context.response.Content = $"{session.GetRemoteIpAddress()}:{ConfigMgr.GetDefaultValue("BattlenetPort", 1119)}"; + return RequestHandlerResult.Handled; + } + + RequestHandlerResult HandlePostLogin(LoginHttpSession session, RequestContext context) + { + LoginForm loginForm = JsonSerializer.Deserialize(context.request.Content); + if (loginForm == null) + { + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.LOGIN.ToString(); + loginResult.ErrorCode = "UNABLE_TO_DECODE"; + loginResult.ErrorMessage = "There was an internal error while connecting to Battle.net. Please try again later."; + + context.response.Status = HttpStatusCode.BadRequest; + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + + return RequestHandlerResult.Handled; + } + + string getInputValue(LoginForm loginForm, string inputId) + { + for (int i = 0; i < loginForm.Inputs.Count; ++i) + if (loginForm.Inputs[i].Id == inputId) + return loginForm.Inputs[i].Value; + return ""; + }; + + string login = getInputValue(loginForm, "account_name").ToUpper(); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_AUTHENTICATION); + stmt.AddValue(0, login); + + session.QueueQuery(DB.Login.AsyncQuery(stmt).WithChainingCallback((callback, result) => + { + if (result.IsEmpty()) + { + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.DONE.ToString(); + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + return; + } + + string login = getInputValue(loginForm, "account_name").ToUpper(); + bool passwordCorrect = false; + string serverM2 = null; + + uint accountId = result.Read(0); + if (session.GetSessionState().Srp == null) + { + SrpVersion version = (SrpVersion)result.Read(1); + string srpUsername = SHA256.HashData(Encoding.UTF8.GetBytes(login)).ToHexString(); + byte[] s = result.Read(2); + byte[] v = result.Read(3); + session.GetSessionState().Srp = CreateSrpImplementation(version, SrpHashFunction.Sha256, srpUsername, s, v); + + string password = getInputValue(loginForm, "password"); + if (version == SrpVersion.v1) + password = password.ToUpper(); + + passwordCorrect = session.GetSessionState().Srp.CheckCredentials(srpUsername, password); + } + else + { + BigInteger A = new(getInputValue(loginForm, "public_A").ToByteArray(), true, true); + BigInteger M1 = new(getInputValue(loginForm, "client_evidence_M1").ToByteArray(), true, true); + + var sessionKey = session.GetSessionState().Srp.VerifyClientEvidence(A, M1); + if (sessionKey.HasValue) + { + passwordCorrect = true; + serverM2 = session.GetSessionState().Srp.CalculateServerEvidence(A, M1, sessionKey.Value).ToByteArray(true, true).ToHexString(); + } + } + + uint failedLogins = result.Read(4); + string loginTicket = result.Read(5); + uint loginTicketExpiry = result.Read(6); + bool isBanned = result.Read(7) != 0; + + if (!passwordCorrect) + { + if (!isBanned) + { + string ip_address = session.GetRemoteIpAddress().ToString(); + uint maxWrongPassword = ConfigMgr.GetDefaultValue("WrongPass.MaxCount", 0u); + + if (ConfigMgr.GetDefaultValue("WrongPass.Logging", false)) + Log.outDebug(LogFilter.Http, $"[{ip_address}, Account {login}, Id {accountId}] Attempted to connect with wrong password!"); + + if (maxWrongPassword != 0) + { + SQLTransaction trans = new(); + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_FAILED_LOGINS); + stmt.AddValue(0, accountId); + trans.Append(stmt); + + ++failedLogins; + + Log.outDebug(LogFilter.Http, $"MaxWrongPass : {maxWrongPassword}, failed_login : {accountId}"); + + if (failedLogins >= maxWrongPassword) + { + BanMode banType = ConfigMgr.GetDefaultValue("WrongPass.BanType", BanMode.IP); + int banTime = ConfigMgr.GetDefaultValue("WrongPass.BanTime", 600); + + if (banType == BanMode.Account) + { + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.INS_BNET_ACCOUNT_AUTO_BANNED); + stmt.AddValue(0, accountId); + } + else + { + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.INS_IP_AUTO_BANNED); + stmt.AddValue(0, ip_address); + } + + stmt.AddValue(1, banTime); + trans.Append(stmt); + + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_RESET_FAILED_LOGINS); + stmt.AddValue(0, accountId); + trans.Append(stmt); + } + + DB.Login.CommitTransaction(trans); + } + } + + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.DONE.ToString(); + + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + return; + } + + if (loginTicket.IsEmpty() || loginTicketExpiry < Time.UnixTime) + loginTicket = "TC-" + RandomHelper.GetRandomBytes(20).ToHexString(); + + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_AUTHENTICATION); + stmt.AddValue(0, loginTicket); + stmt.AddValue(1, Time.UnixTime + _loginTicketDuration); + stmt.AddValue(2, accountId); + callback.WithCallback(_ => + { + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.DONE.ToString(); + loginResult.LoginTicket = loginTicket; + if (!serverM2.IsEmpty()) + loginResult.ServerEvidenceM2 = serverM2; + + context.response.ContentType = "application/json;charset=utf-8"; + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + }).SetNextQuery(DB.Login.AsyncQuery(stmt)); + })); + + return RequestHandlerResult.Async; + } + + RequestHandlerResult HandlePostLoginSrpChallenge(LoginHttpSession session, RequestContext context) + { + LoginForm loginForm = JsonSerializer.Deserialize(context.request.Content); + if (loginForm == null) + { + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.LOGIN.ToString(); + loginResult.ErrorCode = "UNABLE_TO_DECODE"; + loginResult.ErrorMessage = "There was an internal error while connecting to Battle.net. Please try again later."; + + context.response.Status = HttpStatusCode.BadRequest; + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + + return RequestHandlerResult.Handled; + } + + string login = ""; + + for (int i = 0; i < loginForm.Inputs.Count; ++i) + if (loginForm.Inputs[i].Id == "account_name") + login = loginForm.Inputs[i].Value; + + login = login.ToUpper(); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_CHECK_PASSWORD_BY_EMAIL); + stmt.AddValue(0, login); + + session.QueueQuery(DB.Login.AsyncQuery(stmt).WithCallback(result => + { + if (result.IsEmpty()) + { + LoginResult loginResult = new(); + loginResult.AuthenticationState = AuthenticationState.DONE.ToString(); + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginResult); + session.SendResponse(context); + return; + } + + SrpVersion version = (SrpVersion)result.Read(0); + SrpHashFunction hashFunction = SrpHashFunction.Sha256; + string srpUsername = SHA256.HashData(Encoding.UTF8.GetBytes(login)).ToHexString(); + byte[] s = result.Read(1); + byte[] v = result.Read(2); + + session.GetSessionState().Srp = CreateSrpImplementation(version, hashFunction, srpUsername, s, v); + if (session.GetSessionState().Srp == null) + { + context.response.Status = HttpStatusCode.InternalServerError; + session.SendResponse(context); + return; + } + + SrpLoginChallenge challenge = new(); + challenge.Version = session.GetSessionState().Srp.GetVersion(); + challenge.Iterations = (int)session.GetSessionState().Srp.GetXIterations(); + challenge.Modulus = session.GetSessionState().Srp.GetN().ToByteArray(true, true).ToHexString(); + challenge.Generator = session.GetSessionState().Srp.Getg().ToByteArray(true, true).ToHexString(); + + string hash = ""; + switch (hashFunction) + { + case SrpHashFunction.Sha256: + hash = "SHA-256"; + break; + case SrpHashFunction.Sha512: + hash = "SHA-512"; + break; + } + + challenge.HashFunction = hash; + challenge.Username = srpUsername; + challenge.Salt = session.GetSessionState().Srp.s.ToHexString(); + challenge.PublicB = session.GetSessionState().Srp.B.ToByteArray(true, true).ToHexString(); + + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(challenge); + session.SendResponse(context); + })); + + return RequestHandlerResult.Async; + } + + RequestHandlerResult HandlePostRefreshLoginTicket(LoginHttpSession session, RequestContext context) + { + string ticket = ExtractAuthorization(context.request); + if (ticket.IsEmpty()) + return dispatcherService.HandleUnauthorized(session, context); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_EXISTING_AUTHENTICATION); + stmt.AddValue(0, ticket); + session.QueueQuery(DB.Login.AsyncQuery(stmt).WithCallback(result => + { + LoginRefreshResult loginRefreshResult = new(); + if (!result.IsEmpty()) + { + uint loginTicketExpiry = result.Read(0); + long now = Time.UnixTime; + if (loginTicketExpiry > now) + { + loginRefreshResult.LoginTicketExpiry = (now + _loginTicketDuration); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_EXISTING_AUTHENTICATION); + stmt.AddValue(0, (uint)(now + _loginTicketDuration)); + stmt.AddValue(1, ticket); + DB.Login.Execute(stmt); + } + else + loginRefreshResult.IsExpired = true; + } + else + loginRefreshResult.IsExpired = true; + + context.response.ContentType = "application/json;charset=utf-8"; + context.response.Content = JsonSerializer.Serialize(loginRefreshResult); + session.SendResponse(context); + })); + + return RequestHandlerResult.Async; + } + + public BnetSRP6Base CreateSrpImplementation(SrpVersion version, SrpHashFunction hashFunction, string username, byte[] salt, byte[] verifier) + { + if (version == SrpVersion.v2) + { + if (hashFunction == SrpHashFunction.Sha256) + return new BnetSRP6v2Hash256(username, salt, verifier); + if (hashFunction == SrpHashFunction.Sha512) + return new BnetSRP6v2Hash512(username, salt, verifier); + } + + if (version == SrpVersion.v1) + { + if (hashFunction == SrpHashFunction.Sha256) + return new BnetSRP6v1Hash256(username, salt, verifier); + if (hashFunction == SrpHashFunction.Sha512) + return new BnetSRP6v1Hash512(username, salt, verifier); + } + + return null; + } + + public override SessionState CreateNewSessionState(IPAddress address) + { + var state = new LoginSessionState(); + sessionService.InitAndStoreSessionState(state, address); + return state; + } + + public static void OnSocketAccept(Socket sock) + { + Global.LoginService.OnSocketOpen(sock); + } + + void MigrateLegacyPasswordHashes() + { + if (DB.Login.Query("SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'battlenet_accounts' AND COLUMN_NAME = 'sha_pass_hash'").IsEmpty()) + return; + + Log.outInfo(_logger, "Updating password hashes..."); + uint start = Time.GetMSTime(); + // the auth update query nulls salt/verifier if they cannot be converted + // if they are non-null but s/v have been cleared, that means a legacy tool touched our auth DB (otherwise, the core might've done it itself, it used to use those hacks too) + SQLResult result = DB.Login.Query("SELECT id, sha_pass_hash, IF((salt IS null) OR (verifier IS null), 0, 1) AS shouldWarn FROM battlenet_accounts WHERE sha_pass_hash != DEFAULT(sha_pass_hash) OR salt IS NULL OR verifier IS NULL"); + if (result.IsEmpty()) + { + Log.outInfo(_logger, $"No password hashes to update - this took us {Time.GetMSTimeDiffToNow(start)} ms to realize"); + return; + } + + bool hadWarning = false; + uint count = 0; + SQLTransaction tx = new SQLTransaction(); + do + { + uint id = result.Read(0); + + byte[] salt = RandomHelper.GetRandomBytes(SRP6.SaltLength); + BigInteger x = new BigInteger(SHA256.HashData(salt.Combine(result.Read(1).ToByteArray(true))), true); + byte[] verifier = BigInteger.ModPow(BnetSRP6v1Base.g, x, BnetSRP6v1Base.N).ToByteArray(true, true); + + if (result.Read(2) != 0) + { + if (!hadWarning) + { + hadWarning = true; + Log.outWarn(_logger, + " ========\n" + + "(!) You appear to be using an outdated external account management tool.\n" + + "(!) Update your external tool.\n" + + "(!!) If no update is available, refer your tool's developer to https://github.com/TrinityCore/TrinityCore/issues/25157.\n" + + " ========"); + } + } + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_LOGON); + stmt.AddValue(0, (sbyte)SrpVersion.v1); + stmt.AddValue(1, salt); + stmt.AddValue(2, verifier); + stmt.AddValue(3, id); + tx.Append(stmt); + + tx.Append($"UPDATE battlenet_accounts SET sha_pass_hash = DEFAULT(sha_pass_hash) WHERE id = {id}"); + + if (tx.GetSize() >= 10000) + { + DB.Login.CommitTransaction(tx); + tx = new SQLTransaction(); + } + + ++count; + } while (result.NextRow()); + DB.Login.CommitTransaction(tx); + + Log.outInfo(_logger, $"{count} password hashes updated in {Time.GetMSTimeDiffToNow(start)} ms"); + } + + public int GetPort() { return _port; } + } +} \ No newline at end of file diff --git a/Source/BNetServer/Server.cs b/Source/BNetServer/Server.cs index 28f169989..730cd2bf2 100644 --- a/Source/BNetServer/Server.cs +++ b/Source/BNetServer/Server.cs @@ -27,36 +27,36 @@ namespace BNetServer if (!StartDB()) ExitNow(); - string bindIp = ConfigMgr.GetDefaultValue("BindIP", "0.0.0.0"); - - var restSocketServer = new SocketManager(); - int restPort = ConfigMgr.GetDefaultValue("LoginREST.Port", 8081); - if (restPort < 0 || restPort > 0xFFFF) + string httpBindIp = ConfigMgr.GetDefaultValue("BindIP", "0.0.0.0"); + int httpPort = ConfigMgr.GetDefaultValue("LoginREST.Port", 8081); + if (httpPort <= 0 || httpPort > 0xFFFF) { - Log.outError(LogFilter.Network, $"Specified login service port ({restPort}) out of allowed range (1-65535), defaulting to 8081"); - restPort = 8081; - } - - if (!restSocketServer.StartNetwork(bindIp, restPort)) - { - Log.outError(LogFilter.Server, "Failed to initialize Rest Socket Server"); + Log.outError(LogFilter.Server, $"Specified login service port ({httpPort}) out of allowed range (1-65535)"); ExitNow(); } - // Get the list of realms for the server - Global.RealmMgr.Initialize(ConfigMgr.GetDefaultValue("RealmsStateUpdateDelay", 10)); - Global.LoginServiceMgr.Initialize(); + if (!Global.LoginService.StartNetwork(httpBindIp, httpPort)) + { + Log.outError(LogFilter.Server, "Failed to initialize login service"); + ExitNow(); + } - var sessionSocketServer = new SocketManager(); // Start the listening port (acceptor) for auth connections int bnPort = ConfigMgr.GetDefaultValue("BattlenetPort", 1119); - if (bnPort < 0 || bnPort > 0xFFFF) + if (bnPort <= 0 || bnPort > 0xFFFF) { Log.outError(LogFilter.Server, $"Specified battle.net port ({bnPort}) out of allowed range (1-65535)"); ExitNow(); } - if (!sessionSocketServer.StartNetwork(bindIp, bnPort)) + // Get the list of realms for the server + Global.RealmMgr.Initialize(ConfigMgr.GetDefaultValue("RealmsStateUpdateDelay", 10)); + + Global.LoginServiceMgr.Initialize(); + + string bindIp = ConfigMgr.GetDefaultValue("BindIP", "0.0.0.0"); + + if (!Global.SessionMgr.StartNetwork(bindIp, bnPort)) { Log.outError(LogFilter.Network, "Failed to start BnetServer Network"); ExitNow(); @@ -89,9 +89,9 @@ namespace BNetServer static void BanExpiryCheckTimer_Elapsed(object sender, ElapsedEventArgs e) { - DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DelExpiredIpBans)); - DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.UpdExpiredAccountBans)); - DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DelBnetExpiredAccountBanned)); + DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DEL_EXPIRED_IP_BANS)); + DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.UPD_EXPIRED_ACCOUNT_BANS)); + DB.Login.Execute(LoginDatabase.GetPreparedStatement(LoginStatements.DEL_BNET_EXPIRED_ACCOUNT_BANNED)); } static Timer _banExpiryCheckTimer; diff --git a/Source/Framework/Constants/Authentication/AuthConst.cs b/Source/Framework/Constants/Authentication/AuthConst.cs index 249e5a9c6..a7db7be62 100644 --- a/Source/Framework/Constants/Authentication/AuthConst.cs +++ b/Source/Framework/Constants/Authentication/AuthConst.cs @@ -133,4 +133,16 @@ namespace Framework.Constants NameTakenByThisAccount = 4, Unknown = 5 } + + public enum SrpVersion + { + v1 = 1, + v2 = 2 + } + + public enum SrpHashFunction + { + Sha256 = 0, + Sha512 = 1 + } } diff --git a/Source/Framework/Constants/Network/NetworkConst.cs b/Source/Framework/Constants/Network/NetworkConst.cs index e35fae2f9..55ac019e2 100644 --- a/Source/Framework/Constants/Network/NetworkConst.cs +++ b/Source/Framework/Constants/Network/NetworkConst.cs @@ -18,4 +18,18 @@ namespace Framework.Constants ThreadUnsafe, //packet is not thread-safe - process it in World.UpdateSessions() ThreadSafe //packet is thread-safe - process it in Map.Update() } + + public enum RequestHandlerFlag + { + None = 0x0, + DoNotLogRequestContent = 0x1, + DoNotLogResponseContent = 0x2, + } + + public enum RequestHandlerResult + { + Handled, + Error, + Async, + } } diff --git a/Source/Framework/Cryptography/SRP6.cs b/Source/Framework/Cryptography/SRP6.cs index e8b4e4323..613b939a7 100644 --- a/Source/Framework/Cryptography/SRP6.cs +++ b/Source/Framework/Cryptography/SRP6.cs @@ -2,44 +2,400 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System; +using System.Linq; using System.Numerics; using System.Security.Cryptography; using System.Text; namespace Framework.Cryptography { - public class SRP6 + public abstract class SRP6 { - static SHA1 _sha1; - static BigInteger _g; - static BigInteger _N; + public static int SaltLength = 32; - static SRP6() + public byte[] s = new byte[SaltLength]; // s - the user's password salt, random, used to calculate v on registration + + protected BigInteger I; // H(I) - the username, all uppercase + protected BigInteger b; // b - randomly chosen by the server, same length as N, never given out + protected BigInteger v; // v - the user's password verifier, derived from s + H(USERNAME || ":" || PASSWORD) + + public BigInteger B; // B = k*v + g^b + + bool _used; // a single instance can only be used to verify once + + public SRP6() { } + public SRP6(BigInteger i, byte[] salt, byte[] verifier, BigInteger N, BigInteger g, BigInteger k) { - _sha1 = SHA1.Create(); - _g = new BigInteger(7); - _N = new BigInteger(new byte[] - { - 0x89, 0x4B, 0x64, 0x5E, 0x89, 0xE1, 0x53, 0x5B, 0xBD, 0xAD, 0x5B, 0x8B, 0x29, 0x06, 0x50, 0x53, - 0x08, 0x01, 0xB1, 0x8E, 0xBF, 0xBF, 0x5E, 0x8F, 0xAB, 0x3C, 0x82, 0x87, 0x2A, 0x3E, 0x9B, 0xB7, - }, true, true); + s = salt; + I = i; + b = CalculatePrivateB(N); + v = new BigInteger(verifier, true); + B = CalculatePublicB(N, g, k); } - public static (byte[] Salt, byte[] Verifier) MakeRegistrationData(string username, string password) + public BigInteger? VerifyClientEvidence(BigInteger A, BigInteger clientM1) { - var salt = new byte[0].GenerateRandomKey(32); // random salt - return (salt, CalculateVerifier(username, password, salt)); + Cypher.Assert(!_used, "A single SRP6 object must only ever be used to verify ONCE!"); + _used = true; + + return DoVerifyClientEvidence(A, clientM1); } - - public static byte[] CalculateVerifier(string username, string password, byte[] salt) + + public bool CheckCredentials(string username, string password) + { + return v == new BigInteger(CalculateVerifier(username, password, s), true); + } + + BigInteger CalculatePrivateB(BigInteger N) + { + BigInteger b = new BigInteger(RandomHelper.GetRandomBytes((int)N.GetBitLength()), true); + b = b % (N - 1); + return b; + } + + BigInteger CalculatePublicB(BigInteger N, BigInteger g, BigInteger k) + { + return (BigInteger.ModPow(g, b, N) + (v * k)) % N; + } + + byte[] CalculateVerifier(string username, string password, byte[] salt) { // v = g ^ H(s || H(u || ':' || p)) mod N - return BigInteger.ModPow(_g, new BigInteger(_sha1.ComputeHash(salt.Combine(_sha1.ComputeHash(Encoding.UTF8.GetBytes(username.ToUpperInvariant() + ":" + password.ToUpperInvariant())))), true), _N).ToByteArray(); + return BigInteger.ModPow(Getg(), CalculateX(username, password, salt), GetN()).ToByteArray(); } - public static bool CheckLogin(string username, string password, byte[] salt, byte[] verifier) + public abstract BigInteger GetN(); + public abstract BigInteger Getg(); + + public abstract BigInteger CalculateServerEvidence(BigInteger A, BigInteger clientM1, BigInteger K); + + public static (byte[], byte[]) MakeAccountRegistrationData(string username, string password) where T : new() { - return verifier.Compare(CalculateVerifier(username, password, salt)); + GruntSRP6 impl = new(); + return (impl.s, impl.CalculateVerifier(username, password, impl.s)); + } + + public static (byte[], byte[]) MakeBNetRegistrationData(string username, string password) where T : new() + { + BnetSRP6v2Hash256 impl = new(); + return (impl.s, impl.CalculateVerifier(username, password, impl.s)); + } + + public abstract BigInteger CalculateX(string username, string password, byte[] salt); + + + public abstract BigInteger? DoVerifyClientEvidence(BigInteger A, BigInteger clientM1); + } + + public class GruntSRP6 : SRP6 + { + static BigInteger N;// the modulus, an algorithm parameter; all operations are mod this + static BigInteger g;// a [g]enerator for the ring of integers mod N, algorithm parameter + + static SHA1 _sha1 = SHA1.Create(); + + static GruntSRP6() + { + N = new BigInteger("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7".ToByteArray(), true, true); + g = new BigInteger(7u); + } + + public GruntSRP6() : base() { } + public GruntSRP6(string username, byte[] salt, byte[] verifier) : base(new BigInteger(_sha1.ComputeHash(Encoding.UTF8.GetBytes(username)), true, true), salt, verifier, N, g, 3) { } + + public override BigInteger CalculateServerEvidence(BigInteger A, BigInteger clientM1, BigInteger K) + { + return new BigInteger(_sha1.ComputeHash(A.ToByteArray().Combine(clientM1.ToByteArray().Combine(K.ToByteArray()))), true, true); + } + + public override BigInteger CalculateX(string username, string password, byte[] salt) + { + return new BigInteger(_sha1.ComputeHash(salt.Combine(_sha1.ComputeHash(Encoding.UTF8.GetBytes(username + ":" + password)))), true, true); + } + + public override BigInteger? DoVerifyClientEvidence(BigInteger A, BigInteger clientM1) + { + if ((A % N).IsZero) + return null; + + BigInteger u = new(_sha1.ComputeHash(A.ToByteArray().Combine(B.ToByteArray())), false, true); + byte[] S = BigInteger.ModPow(A * BigInteger.ModPow(v, u, N), b, N).ToByteArray(); + + byte[] K = SHA1Interleave(S); + + // NgHash = H(N) xor H(g) + byte[] NHash = _sha1.ComputeHash(N.ToByteArray()); + byte[] gHash = _sha1.ComputeHash(g.ToByteArray()); + byte[] NgHash = NHash.Select((x, i) => (byte)(x ^ gHash[i])).ToArray(); + + BigInteger ourM = new BigInteger(_sha1.ComputeHash(NgHash.Combine(I.ToByteArray().Combine(s.Combine(A.ToByteArray().Combine(B.ToByteArray().Combine(K)))))), true, true); + if (ourM == clientM1) + return new BigInteger(K, true, true); + + return null; + } + + byte[] SHA1Interleave(byte[] S) + { + // split S into two buffers + byte[] buf0 = new byte[32 / 2]; + byte[] buf1 = new byte[32 / 2]; + for (int i = 0; i < S.Length / 2; ++i) + { + buf0[i] = S[2 * i + 0]; + buf1[i] = S[2 * i + 1]; + } + + // find position of first nonzero byte + int p = 0; + while (p < S.Length && S[p] == 0) + ++p; + + if ((p & 1) != 0) + ++p; // skip one extra byte if p is odd + p /= 2; // offset into buffers + + // hash each of the halves, starting at the first nonzero byte + byte[] hash0 = _sha1.ComputeHash(buf0[p..].Combine(S[(S.Length / 2 - p)..])); + byte[] hash1 = _sha1.ComputeHash(buf1[p..].Combine(S[(S.Length / 2 - p)..])); + + // stick the two hashes back together + byte[] K = new byte[SHA1.HashSizeInBytes * 2]; + for (int i = 0; i < SHA1.HashSizeInBytes; ++i) + { + K[2 * i + 0] = hash0[i]; + K[2 * i + 1] = hash1[i]; + } + return K; + } + + public override BigInteger GetN() { return N; } + + public override BigInteger Getg() { return g; } + } + + public abstract class BnetSRP6Base : SRP6 + { + protected static SHA256 _sha256 = SHA256.Create(); + protected static SHA512 _sha512 = SHA512.Create(); + + public BnetSRP6Base() : base() { } + public BnetSRP6Base(BigInteger i, byte[] salt, byte[] verifier, BigInteger N, BigInteger g, BigInteger k) : base(i, salt, verifier, N, g, k) { } + + public override BigInteger CalculateServerEvidence(BigInteger A, BigInteger clientM1, BigInteger K) + { + return DoCalculateEvidence(A, clientM1, K); + } + + public override BigInteger? DoVerifyClientEvidence(BigInteger A, BigInteger clientM1) + { + BigInteger N = GetN(); + if ((A % N).IsZero) + return null; + + BigInteger u = CalculateU(A); + if ((u % N).IsZero) + return null; + + BigInteger S = BigInteger.ModPow(A * BigInteger.ModPow(v, u, N), b, N); + + BigInteger ourM = DoCalculateEvidence(A, B, S); + + if (ourM != clientM1) + return null; + + return S; + } + + byte[] GetBrokenEvidenceVector(BigInteger bn) + { + int bytes = (int)(bn.GetBitLength() + 8) >> 3; + var byteArray = bn.ToByteArray(true, true); + return new byte[bytes - byteArray.Length].Combine(byteArray); + } + + public abstract byte GetVersion(); + public abstract uint GetXIterations(); + + public virtual BigInteger CalculateU(BigInteger A) { return default; } + + public virtual BigInteger DoCalculateEvidence(params BigInteger[] bns) { return default; } + + public BigInteger DoCalculateEvidenceHash256(params BigInteger[] bns) + { + for (var i = 0; i < bns.Length; ++i) + { + var bytes = GetBrokenEvidenceVector(bns[i]); + if (i == bns.Length - 1) + { + _sha256.TransformFinalBlock(bytes, 0, bytes.Length); + break; + } + + _sha256.TransformBlock(bytes, 0, bytes.Length, null, 0); + } + + return new BigInteger(_sha256.Hash, true, true); + } + + public BigInteger DoCalculateEvidenceHash512(params BigInteger[] bns) + { + for (var i = 0; i < bns.Length; ++i) + { + var bytes = GetBrokenEvidenceVector(bns[i]); + if (i == bns.Length - 1) + { + _sha512.TransformFinalBlock(bytes, 0, bytes.Length); + break; + } + + _sha512.TransformBlock(bytes, 0, bytes.Length, null, 0); + } + + return new BigInteger(_sha512.Hash, true, true); } } -} + + public class BnetSRP6v1Base : BnetSRP6Base + { + /// + /// // the modulus, an algorithm parameter; all operations are mod this + /// + public static BigInteger N; + + /// + /// // a [g]enerator for the ring of integers mod N, algorithm parameter + /// + public static BigInteger g; + + static BnetSRP6v1Base() + { + g = new BigInteger(2u); + N = new BigInteger("86A7F6DEEB306CE519770FE37D556F29944132554DED0BD68205E27F3231FEF5A10108238A3150C59CAF7B0B6478691C13A6ACF5E1B5ADAFD4A943D4A21A142B800E8A55F8BFBAC700EB77A7235EE5A609E350EA9FC19F10D921C2FA832E4461B7125D38D254A0BE873DFC27858ACB3F8B9F258461E4373BC3A6C2A9634324AB".ToByteArray(), true, true); + } + + public BnetSRP6v1Base() : base() { } + public BnetSRP6v1Base(string username, byte[] salt, byte[] verifier, BigInteger k) : base(new BigInteger(SHA256.HashData(Encoding.UTF8.GetBytes(username)), true), salt, verifier, N, g, k) { } + + public override BigInteger CalculateX(string username, string password, byte[] salt) + { + return new BigInteger(SHA256.HashData(salt.Combine(SHA256.HashData(Encoding.UTF8.GetBytes(username + ":" + password)))), true); + } + + public override BigInteger GetN() { return N; } + public override BigInteger Getg() { return g; } + + public override byte GetVersion() { return 1; } + public override uint GetXIterations() { return 1; } + } + + public class BnetSRP6v2Base : BnetSRP6Base + { + /// + /// // the modulus, an algorithm parameter; all operations are mod this + /// + protected static BigInteger N; + + /// + /// // a [g]enerator for the ring of integers mod N, algorithm parameter + /// + protected static BigInteger g; + + static BnetSRP6v2Base() + { + N = new BigInteger("AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73".ToByteArray(), true, true); + g = new BigInteger(2u); + } + + public BnetSRP6v2Base() : base() { } + public BnetSRP6v2Base(string username, byte[] salt, byte[] verifier, BigInteger k) : base(new BigInteger(SHA256.HashData(Encoding.UTF8.GetBytes(username)), true), salt, verifier, N, g, k) { } + + public override BigInteger CalculateX(string username, string password, byte[] salt) + { + string tmp = username + ":" + password; + Rfc2898DeriveBytes rfc = new Rfc2898DeriveBytes(tmp, salt, (int)GetXIterations(), HashAlgorithmName.SHA512); + byte[] xBytes = rfc.GetBytes(SHA512.HashSizeInBytes); + BigInteger x = new(xBytes, true, true); + if ((xBytes[0] & 0x80) != 0) + { + byte[] fix = new byte[65]; + fix[64] = 1; + x -= new BigInteger(fix, true); + } + + return x % (N - 1); + } + + public override BigInteger GetN() { return N; } + public override BigInteger Getg() { return g; } + + public override byte GetVersion() { return 2; } + public override uint GetXIterations() { return 15000; } + } + + public class BnetSRP6v1Hash256 : BnetSRP6v1Base + { + static byte[] dummyBytes = new byte[127]; + + public BnetSRP6v1Hash256() : base() { } + public BnetSRP6v1Hash256(string username, byte[] salt, byte[] verifier) : base(username, salt, verifier, new BigInteger(SHA256.HashData(N.ToByteArray(true, true).Combine(dummyBytes.Combine(g.ToByteArray(true, true)))), true, true)) { } + + public override BigInteger CalculateU(BigInteger A) + { + return new BigInteger(SHA256.HashData(A.ToByteArray(true, true).Combine(B.ToByteArray(true, true))), true, true); + } + + public override BigInteger DoCalculateEvidence(params BigInteger[] bns) + { + return DoCalculateEvidenceHash256(bns); + } + } + + public class BnetSRP6v1Hash512 : BnetSRP6v1Base + { + public BnetSRP6v1Hash512() : base() { } + public BnetSRP6v1Hash512(string username, byte[] salt, byte[] verifier) : base(username, salt, verifier, new BigInteger(SHA512.HashData(N.ToByteArray(true, true).Combine(g.ToByteArray(true, true))), true, true)) { } + + public override BigInteger CalculateU(BigInteger A) + { + return new BigInteger(_sha512.ComputeHash(A.ToByteArray(true, true).Combine(B.ToByteArray(true, true))), true, true); + } + + public override BigInteger DoCalculateEvidence(params BigInteger[] bns) + { + return DoCalculateEvidenceHash512(bns); + } + } + + public class BnetSRP6v2Hash256 : BnetSRP6v2Base + { + public BnetSRP6v2Hash256() : base() { } + public BnetSRP6v2Hash256(string username, byte[] salt, byte[] verifier) : base(username, salt, verifier, new BigInteger(_sha256.ComputeHash(N.ToByteArray(true, false).Combine(g.ToByteArray(true, false))), true)) { } + + public override BigInteger CalculateU(BigInteger A) + { + return new BigInteger(_sha256.ComputeHash(A.ToByteArray(true, true).Combine(B.ToByteArray(true, true))), true, true); + } + + public override BigInteger DoCalculateEvidence(params BigInteger[] bns) + { + return DoCalculateEvidenceHash256(bns); + } + } + + public class BnetSRP6v2Hash512 : BnetSRP6v2Base + { + public BnetSRP6v2Hash512() : base() { } + public BnetSRP6v2Hash512(string username, byte[] salt, byte[] verifier) : base(username, salt, verifier, new BigInteger(_sha512.ComputeHash(N.ToByteArray(true, true).Combine(g.ToByteArray(true, true))), true, true)) { } + + public override BigInteger CalculateU(BigInteger A) + { + return new BigInteger(_sha512.ComputeHash(A.ToByteArray(true, true).Combine(B.ToByteArray(true, true))), true, true); + } + + public override BigInteger DoCalculateEvidence(params BigInteger[] bns) + { + return DoCalculateEvidenceHash512(bns); + } + } +} \ No newline at end of file diff --git a/Source/Framework/Database/Databases/LoginDatabase.cs b/Source/Framework/Database/Databases/LoginDatabase.cs index 7f91e1042..60d3f7cd4 100644 --- a/Source/Framework/Database/Databases/LoginDatabase.cs +++ b/Source/Framework/Database/Databases/LoginDatabase.cs @@ -7,15 +7,14 @@ namespace Framework.Database { public override void PreparedStatements() { - const string BnetAccountInfo = "ba.id, ba.email, ba.locked, ba.lock_country, ba.last_ip, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, bab.unbandate = bab.bandate"; + const string BnetAccountInfo = "ba.id, UPPER(ba.email), ba.locked, ba.lock_country, ba.last_ip, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, bab.unbandate = bab.bandate"; const string BnetGameAccountInfo = "a.id, a.username, ab.unbandate, ab.unbandate = ab.bandate, aa.SecurityLevel"; PrepareStatement(LoginStatements.SEL_REALMLIST, "SELECT id, name, address, localAddress, port, icon, flag, timezone, allowedSecurityLevel, population, gamebuild, Region, Battlegroup FROM realmlist WHERE flag <> 3 ORDER BY name"); - PrepareStatement(LoginStatements.DelExpiredIpBans, "DELETE FROM ip_banned WHERE unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); - PrepareStatement(LoginStatements.UpdExpiredAccountBans, "UPDATE account_banned SET active = 0 WHERE active = 1 AND unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); - PrepareStatement(LoginStatements.SelIpInfo, "SELECT unbandate > UNIX_TIMESTAMP() OR unbandate = bandate AS banned, NULL as country FROM ip_banned WHERE ip = ?"); - - PrepareStatement(LoginStatements.InsIpAutoBanned, "INSERT INTO ip_banned (ip, bandate, unbandate, bannedby, banreason) VALUES (?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'Cypher Auth', 'Failed login autoban')"); + PrepareStatement(LoginStatements.DEL_EXPIRED_IP_BANS, "DELETE FROM ip_banned WHERE unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); + PrepareStatement(LoginStatements.UPD_EXPIRED_ACCOUNT_BANS, "UPDATE account_banned SET active = 0 WHERE active = 1 AND unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); + PrepareStatement(LoginStatements.SEL_IP_INFO, "SELECT unbandate > UNIX_TIMESTAMP() OR unbandate = bandate AS banned, NULL as country FROM ip_banned WHERE ip = ?"); + PrepareStatement(LoginStatements.INS_IP_AUTO_BANNED, "INSERT INTO ip_banned (ip, bandate, unbandate, bannedby, banreason) VALUES (?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'Trinity Auth', 'Failed login autoban')"); PrepareStatement(LoginStatements.SEL_IP_BANNED_ALL, "SELECT ip, bandate, unbandate, bannedby, banreason FROM ip_banned WHERE (bandate = unbandate OR unbandate > UNIX_TIMESTAMP()) ORDER BY unbandate"); PrepareStatement(LoginStatements.SEL_IP_BANNED_BY_IP, "SELECT ip, bandate, unbandate, bannedby, banreason FROM ip_banned WHERE (bandate = unbandate OR unbandate > UNIX_TIMESTAMP()) AND ip LIKE CONCAT('%%', ?, '%%') ORDER BY unbandate"); PrepareStatement(LoginStatements.SEL_ACCOUNT_BANNED_ALL, "SELECT account.id, username FROM account, account_banned WHERE account.id = account_banned.id AND active = 1 GROUP BY account.id"); @@ -27,12 +26,11 @@ namespace Framework.Database PrepareStatement(LoginStatements.UPD_LOGON, "UPDATE account SET salt = ?, verifier = ? WHERE id = ?"); PrepareStatement(LoginStatements.SEL_ACCOUNT_ID_BY_NAME, "SELECT id FROM account WHERE username = ?"); PrepareStatement(LoginStatements.SEL_ACCOUNT_LIST_BY_NAME, "SELECT id, username FROM account WHERE username = ?"); - PrepareStatement(LoginStatements.SEL_ACCOUNT_INFO_BY_NAME, "SELECT a.id, a.session_key_bnet, ba.last_ip, ba.locked, ba.lock_country, a.expansion, a.mutetime, ba.locale, a.recruiter, a.os, ba.id, aa.SecurityLevel, " + - "bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, r.id " + + PrepareStatement(LoginStatements.SEL_ACCOUNT_INFO_BY_NAME, "SELECT a.id, a.session_key_bnet, ba.last_ip, ba.locked, ba.lock_country, a.expansion, a.mutetime, ba.locale, a.recruiter, a.os, a.timezone_offset, ba.id, aa.SecurityLevel, " + + "bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, r.id " + "FROM account a LEFT JOIN account r ON a.id = r.recruiter LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id " + "LEFT JOIN account_access aa ON a.id = aa.AccountID AND aa.RealmID IN (-1, ?) LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 " + "WHERE a.username = ? AND LENGTH(a.session_key_bnet) = 64 ORDER BY aa.RealmID DESC LIMIT 1"); - PrepareStatement(LoginStatements.SEL_ACCOUNT_LIST_BY_EMAIL, "SELECT id, username FROM account WHERE email = ?"); PrepareStatement(LoginStatements.SEL_ACCOUNT_BY_IP, "SELECT id, username FROM account WHERE last_ip = ?"); PrepareStatement(LoginStatements.SEL_ACCOUNT_BY_ID, "SELECT 1 FROM account WHERE id = ?"); @@ -43,7 +41,7 @@ namespace Framework.Database PrepareStatement(LoginStatements.DEL_REALM_CHARACTERS, "DELETE FROM realmcharacters WHERE acctid = ?"); PrepareStatement(LoginStatements.REP_REALM_CHARACTERS, "REPLACE INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)"); PrepareStatement(LoginStatements.SEL_SUM_REALM_CHARACTERS, "SELECT SUM(numchars) FROM realmcharacters WHERE acctid = ?"); - PrepareStatement(LoginStatements.INS_ACCOUNT, "INSERT INTO account (username, salt, verifier, reg_mail, email, joindate, battlenet_account, battlenet_index) VALUES(?, ?, ?, ?, ?, NOW(), ?, ?)"); + PrepareStatement(LoginStatements.INS_ACCOUNT, "INSERT INTO account(username, salt, verifier, reg_mail, email, joindate, battlenet_account, battlenet_index) VALUES(?, ?, ?, ?, ?, NOW(), ?, ?)"); PrepareStatement(LoginStatements.INS_REALM_CHARACTERS_INIT, "INSERT INTO realmcharacters (realmid, acctid, numchars) SELECT realmlist.id, account.id, 0 FROM realmlist, account LEFT JOIN realmcharacters ON acctid = account.id WHERE acctid IS NULL"); PrepareStatement(LoginStatements.UPD_EXPANSION, "UPDATE account SET expansion = ? WHERE id = ?"); PrepareStatement(LoginStatements.UPD_ACCOUNT_LOCK, "UPDATE account SET locked = ? WHERE id = ?"); @@ -80,14 +78,14 @@ namespace Framework.Database PrepareStatement(LoginStatements.DEL_ACCOUNT, "DELETE FROM account WHERE id = ?"); PrepareStatement(LoginStatements.SEL_AUTOBROADCAST, "SELECT id, weight, text FROM autobroadcast WHERE realmid = ? OR realmid = -1"); PrepareStatement(LoginStatements.GET_EMAIL_BY_ID, "SELECT email FROM account WHERE id = ?"); - // 0: uint32, 1: uint32, 2: uint8, 3: uint32, 4: string // Complete name: "Login_Insert_AccountLoginDeLete_IP_Logging" - PrepareStatement(LoginStatements.INS_ALDL_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id,character_guid,type,ip,systemnote,unixtime,time) VALUES (?, ?, ?, (SELECT last_ip FROM account WHERE id = ?), ?, unix_timestamp(NOW()), NOW())"); - // 0: uint32, 1: uint32, 2: uint8, 3: uint32, 4: string // Complete name: "Login_Insert_FailedAccountLogin_IP_Logging" - PrepareStatement(LoginStatements.INS_FACL_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id,character_guid,type,ip,systemnote,unixtime,time) VALUES (?, ?, ?, (SELECT last_attempt_ip FROM account WHERE id = ?), ?, unix_timestamp(NOW()), NOW())"); - // 0: uint32, 1: uint32, 2: uint8, 3: string, 4: string // Complete name: "Login_Insert_CharacterDelete_IP_Logging" - PrepareStatement(LoginStatements.INS_CHAR_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id,character_guid,type,ip,systemnote,unixtime,time) VALUES (?, ?, ?, ?, ?, unix_timestamp(NOW()), NOW())"); - // 0: string, 1: string, 2: string // Complete name: "Login_Insert_Failed_Account_Login_due_password_IP_Logging" - PrepareStatement(LoginStatements.INS_FALP_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id,character_guid,type,ip,systemnote,unixtime,time) VALUES (?, 0, 1, ?, ?, unix_timestamp(NOW()), NOW())"); + // 0: uint32, 1: uint32, 2: uint32, 3: uint8, 4: uint32, 5: string // Complete name: "Insert_AccountLoginDeLete_IP_Logging" + PrepareStatement(LoginStatements.INS_ALDL_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id, character_guid, realm_id, type, ip, systemnote, unixtime, time) VALUES (?, ?, ?, ?, (SELECT last_ip FROM account WHERE id = ?), ?, unix_timestamp(NOW()), NOW())"); + // 0: uint32, 1: uint32, 2: uint32, 3: uint8, 4: uint32, 5: string // Complete name: "Insert_FailedAccountIP_Logging" + PrepareStatement(LoginStatements.INS_FACL_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id, character_guid, realm_id, type, ip, systemnote, unixtime, time) VALUES (?, ?, ?, ?, (SELECT last_attempt_ip FROM account WHERE id = ?), ?, unix_timestamp(NOW()), NOW())"); + // 0: uint32, 1: uint32, 2: uint32, 3: uint8, 4: string, 5: string // Complete name: "Insert_CharacterDelete_IP_Logging" + PrepareStatement(LoginStatements.INS_CHAR_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id, character_guid, realm_id, type, ip, systemnote, unixtime, time) VALUES (?, ?, ?, ?, ?, ?, unix_timestamp(NOW()), NOW())"); + // 0: uint32, 1: string, 2: string // Complete name: "Insert_Failed_Account_due_password_IP_Logging" + PrepareStatement(LoginStatements.INS_FALP_IP_LOGGING, "INSERT INTO logs_ip_actions (account_id, character_guid, realm_id, type, ip, systemnote, unixtime, time) VALUES (?, 0, 0, 1, ?, ?, unix_timestamp(NOW()), NOW())"); PrepareStatement(LoginStatements.SEL_ACCOUNT_ACCESS_BY_ID, "SELECT SecurityLevel, RealmID FROM account_access WHERE AccountID = ? and (RealmID = ? OR RealmID = -1) ORDER BY SecurityLevel desc"); PrepareStatement(LoginStatements.SEL_RBAC_ACCOUNT_PERMISSIONS, "SELECT permissionId, granted FROM rbac_account_permissions WHERE accountId = ? AND (realmId = ? OR realmId = -1) ORDER BY permissionId, realmId"); @@ -98,35 +96,46 @@ namespace Framework.Database PrepareStatement(LoginStatements.SEL_ACCOUNT_MUTE_INFO, "SELECT mutedate, mutetime, mutereason, mutedby FROM account_muted WHERE guid = ? ORDER BY mutedate ASC"); PrepareStatement(LoginStatements.DEL_ACCOUNT_MUTED, "DELETE FROM account_muted WHERE guid = ?"); - PrepareStatement(LoginStatements.SelBnetAuthentication, "SELECT ba.id, ba.sha_pass_hash, ba.failed_logins, ba.LoginTicket, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id WHERE email = ?"); - PrepareStatement(LoginStatements.UpdBnetAuthentication, "UPDATE battlenet_accounts SET LoginTicket = ?, LoginTicketExpiry = ? WHERE id = ?"); + PrepareStatement(LoginStatements.SEL_SECRET_DIGEST, "SELECT digest FROM secret_digest WHERE id = ?"); + PrepareStatement(LoginStatements.INS_SECRET_DIGEST, "INSERT INTO secret_digest (id, digest) VALUES (?,?)"); + PrepareStatement(LoginStatements.DEL_SECRET_DIGEST, "DELETE FROM secret_digest WHERE id = ?"); + + PrepareStatement(LoginStatements.SEL_ACCOUNT_TOTP_SECRET, "SELECT totp_secret FROM account WHERE id = ?"); + PrepareStatement(LoginStatements.UPD_ACCOUNT_TOTP_SECRET, "UPDATE account SET totp_secret = ? WHERE id = ?"); + + PrepareStatement(LoginStatements.SEL_BNET_AUTHENTICATION, "SELECT ba.id, ba.srp_version, COALESCE(ba.salt, 0x0000000000000000000000000000000000000000000000000000000000000000), ba.verifier, ba.failed_logins, ba.LoginTicket, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id WHERE email = ?"); + PrepareStatement(LoginStatements.UPD_BNET_AUTHENTICATION, "UPDATE battlenet_accounts SET LoginTicket = ?, LoginTicketExpiry = ? WHERE id = ?"); + PrepareStatement(LoginStatements.SEL_BNET_EXISTING_AUTHENTICATION, "SELECT LoginTicketExpiry FROM battlenet_accounts WHERE LoginTicket = ?"); PrepareStatement(LoginStatements.SEL_BNET_EXISTING_AUTHENTICATION_BY_ID, "SELECT LoginTicket FROM battlenet_accounts WHERE id = ?"); - PrepareStatement(LoginStatements.SelBnetAccountInfo, "SELECT " + BnetAccountInfo + ", " + BnetGameAccountInfo + - " FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id LEFT JOIN account a ON ba.id = a.battlenet_account" + + PrepareStatement(LoginStatements.UPD_BNET_EXISTING_AUTHENTICATION, "UPDATE battlenet_accounts SET LoginTicketExpiry = ? WHERE LoginTicket = ?"); + PrepareStatement(LoginStatements.SEL_BNET_ACCOUNT_INFO, $"SELECT {BnetAccountInfo}, {BnetGameAccountInfo}" + + " FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id LEFT JOIN account a ON ba.id = a.battlenet_account" + " LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account_access aa ON a.id = aa.AccountID AND aa.RealmID = -1 WHERE ba.LoginTicket = ? ORDER BY a.id"); - PrepareStatement(LoginStatements.UpdBnetLastLoginInfo, "UPDATE battlenet_accounts SET last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE id = ?"); - PrepareStatement(LoginStatements.UPD_BNET_GAME_ACCOUNT_LOGIN_INFO, "UPDATE account SET session_key_bnet = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?"); - PrepareStatement(LoginStatements.SelBnetCharacterCountsByAccountId, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id WHERE rc.acctid = ?"); + PrepareStatement(LoginStatements.UPD_BNET_LAST_LOGIN_INFO, "UPDATE battlenet_accounts SET last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE id = ?"); + PrepareStatement(LoginStatements.UPD_BNET_GAME_ACCOUNT_LOGIN_INFO, "UPDATE account SET session_key_bnet = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ?, timezone_offset = ? WHERE username = ?"); + PrepareStatement(LoginStatements.SEL_BNET_CHARACTER_COUNTS_BY_ACCOUNT_ID, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id WHERE rc.acctid = ?"); PrepareStatement(LoginStatements.SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id LEFT JOIN account a ON rc.acctid = a.id WHERE a.battlenet_account = ?"); - PrepareStatement(LoginStatements.SelBnetLastPlayerCharacters, "SELECT lpc.accountId, lpc.region, lpc.battlegroup, lpc.realmId, lpc.characterName, lpc.characterGUID, lpc.lastPlayedTime FROM account_last_played_character lpc LEFT JOIN account a ON lpc.accountId = a.id WHERE a.battlenet_account = ?"); + PrepareStatement(LoginStatements.SEL_BNET_LAST_PLAYER_CHARACTERS, "SELECT lpc.accountId, lpc.region, lpc.battlegroup, lpc.realmId, lpc.characterName, lpc.characterGUID, lpc.lastPlayedTime FROM account_last_played_character lpc LEFT JOIN account a ON lpc.accountId = a.id WHERE a.battlenet_account = ?"); PrepareStatement(LoginStatements.DEL_BNET_LAST_PLAYER_CHARACTERS, "DELETE FROM account_last_played_character WHERE accountId = ? AND region = ? AND battlegroup = ?"); PrepareStatement(LoginStatements.INS_BNET_LAST_PLAYER_CHARACTERS, "INSERT INTO account_last_played_character (accountId, region, battlegroup, realmId, characterName, characterGUID, lastPlayedTime) VALUES (?,?,?,?,?,?,?)"); - PrepareStatement(LoginStatements.INS_BNET_ACCOUNT, "INSERT INTO battlenet_accounts (`email`,`sha_pass_hash`) VALUES (?, ?)"); + PrepareStatement(LoginStatements.INS_BNET_ACCOUNT, "INSERT INTO battlenet_accounts (`email`,`srp_version`,`salt`,`verifier`) VALUES (?, ?, ?, ?)"); PrepareStatement(LoginStatements.SEL_BNET_ACCOUNT_EMAIL_BY_ID, "SELECT email FROM battlenet_accounts WHERE id = ?"); PrepareStatement(LoginStatements.SEL_BNET_ACCOUNT_ID_BY_EMAIL, "SELECT id FROM battlenet_accounts WHERE email = ?"); - PrepareStatement(LoginStatements.UPD_BNET_PASSWORD, "UPDATE battlenet_accounts SET sha_pass_hash = ? WHERE id = ?"); - PrepareStatement(LoginStatements.SEL_BNET_CHECK_PASSWORD, "SELECT 1 FROM battlenet_accounts WHERE id = ? AND sha_pass_hash = ?"); + PrepareStatement(LoginStatements.UPD_BNET_LOGON, "UPDATE battlenet_accounts SET srp_version = ?, salt = ?, verifier = ? WHERE id = ?"); + PrepareStatement(LoginStatements.SEL_BNET_CHECK_PASSWORD, "SELECT srp_version, COALESCE(salt, 0x0000000000000000000000000000000000000000000000000000000000000000), verifier FROM battlenet_accounts WHERE id = ?"); + PrepareStatement(LoginStatements.SEL_BNET_CHECK_PASSWORD_BY_EMAIL, "SELECT srp_version, COALESCE(salt, 0x0000000000000000000000000000000000000000000000000000000000000000), verifier FROM battlenet_accounts WHERE email = ?"); PrepareStatement(LoginStatements.UPD_BNET_ACCOUNT_LOCK, "UPDATE battlenet_accounts SET locked = ? WHERE id = ?"); PrepareStatement(LoginStatements.UPD_BNET_ACCOUNT_LOCK_CONTRY, "UPDATE battlenet_accounts SET lock_country = ? WHERE id = ?"); PrepareStatement(LoginStatements.SEL_BNET_ACCOUNT_ID_BY_GAME_ACCOUNT, "SELECT battlenet_account FROM account WHERE id = ?"); PrepareStatement(LoginStatements.UPD_BNET_GAME_ACCOUNT_LINK, "UPDATE account SET battlenet_account = ?, battlenet_index = ? WHERE id = ?"); PrepareStatement(LoginStatements.SEL_BNET_MAX_ACCOUNT_INDEX, "SELECT MAX(battlenet_index) FROM account WHERE battlenet_account = ?"); - PrepareStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST, "SELECT a.id, a.username FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id WHERE ba.email = ?"); + PrepareStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST_SMALL, "SELECT a.id, a.username FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id WHERE ba.email = ?"); + PrepareStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST, "SELECT a.username, a.expansion, ab.bandate, ab.unbandate, ab.banreason FROM account AS a LEFT JOIN account_banned AS ab ON a.id = ab.id AND ab.active = 1 INNER JOIN battlenet_accounts AS ba ON a.battlenet_account = ba.id WHERE ba.LoginTicket = ? ORDER BY a.id"); - PrepareStatement(LoginStatements.UpdBnetFailedLogins, "UPDATE battlenet_accounts SET failed_logins = failed_logins + 1 WHERE id = ?"); - PrepareStatement(LoginStatements.InsBnetAccountAutoBanned, "INSERT INTO battlenet_account_bans(id, bandate, unbandate, bannedby, banreason) VALUES(?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'Trinity Auth', 'Failed login autoban')"); - PrepareStatement(LoginStatements.DelBnetExpiredAccountBanned, "DELETE FROM battlenet_account_bans WHERE unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); - PrepareStatement(LoginStatements.UpdBnetResetFailedLogins, "UPDATE battlenet_accounts SET failed_logins = 0 WHERE id = ?"); + PrepareStatement(LoginStatements.UPD_BNET_FAILED_LOGINS, "UPDATE battlenet_accounts SET failed_logins = failed_logins + 1 WHERE id = ?"); + PrepareStatement(LoginStatements.INS_BNET_ACCOUNT_AUTO_BANNED, "INSERT INTO battlenet_account_bans(id, bandate, unbandate, bannedby, banreason) VALUES(?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'Trinity Auth', 'Failed login autoban')"); + PrepareStatement(LoginStatements.DEL_BNET_EXPIRED_ACCOUNT_BANNED, "DELETE FROM battlenet_account_bans WHERE unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()"); + PrepareStatement(LoginStatements.UPD_BNET_RESET_FAILED_LOGINS, "UPDATE battlenet_accounts SET failed_logins = 0 WHERE id = ?"); PrepareStatement(LoginStatements.SEL_LAST_CHAR_UNDELETE, "SELECT LastCharacterUndelete FROM battlenet_accounts WHERE Id = ?"); PrepareStatement(LoginStatements.UPD_LAST_CHAR_UNDELETE, "UPDATE battlenet_accounts SET LastCharacterUndelete = UNIX_TIMESTAMP() WHERE Id = ?"); @@ -157,22 +166,24 @@ namespace Framework.Database // Transmog collection PrepareStatement(LoginStatements.SEL_BNET_ITEM_APPEARANCES, "SELECT blobIndex, appearanceMask FROM battlenet_item_appearances WHERE battlenetAccountId = ? ORDER BY blobIndex DESC"); - PrepareStatement(LoginStatements.INS_BNET_ITEM_APPEARANCES, "INSERT INTO battlenet_item_appearances (battlenetAccountId, blobIndex, appearanceMask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE appearanceMask = appearanceMask | VALUES(appearanceMask)"); + PrepareStatement(LoginStatements.INS_BNET_ITEM_APPEARANCES, "INSERT INTO battlenet_item_appearances (battlenetAccountId, blobIndex, appearanceMask) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE appearanceMask = appearanceMask | VALUES(appearanceMask)"); PrepareStatement(LoginStatements.SEL_BNET_ITEM_FAVORITE_APPEARANCES, "SELECT itemModifiedAppearanceId FROM battlenet_item_favorite_appearances WHERE battlenetAccountId = ?"); PrepareStatement(LoginStatements.INS_BNET_ITEM_FAVORITE_APPEARANCE, "INSERT INTO battlenet_item_favorite_appearances (battlenetAccountId, itemModifiedAppearanceId) VALUES (?, ?)"); PrepareStatement(LoginStatements.DEL_BNET_ITEM_FAVORITE_APPEARANCE, "DELETE FROM battlenet_item_favorite_appearances WHERE battlenetAccountId = ? AND itemModifiedAppearanceId = ?"); PrepareStatement(LoginStatements.SEL_BNET_TRANSMOG_ILLUSIONS, "SELECT blobIndex, illusionMask FROM battlenet_account_transmog_illusions WHERE battlenetAccountId = ? ORDER BY blobIndex DESC"); - PrepareStatement(LoginStatements.INS_BNET_TRANSMOG_ILLUSIONS, "INSERT INTO battlenet_account_transmog_illusions (battlenetAccountId, blobIndex, illusionMask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE illusionMask = illusionMask | VALUES(illusionMask)"); + PrepareStatement(LoginStatements.INS_BNET_TRANSMOG_ILLUSIONS, "INSERT INTO battlenet_account_transmog_illusions (battlenetAccountId, blobIndex, illusionMask) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE illusionMask = illusionMask | VALUES(illusionMask)"); } } public enum LoginStatements { SEL_REALMLIST, - DelExpiredIpBans, - UpdExpiredAccountBans, - SelIpInfo, - InsIpAutoBanned, + DEL_EXPIRED_IP_BANS, + UPD_EXPIRED_ACCOUNT_BANS, + SEL_IP_INFO, + INS_IP_AUTO_BANNED, SEL_ACCOUNT_BANNED_ALL, SEL_ACCOUNT_BANNED_BY_FILTER, SEL_ACCOUNT_BANNED_BY_USERNAME, @@ -225,12 +236,9 @@ namespace Framework.Database SEL_ACCOUNT_INFO, SEL_ACCOUNT_ACCESS_SECLEVEL_TEST, SEL_ACCOUNT_ACCESS, - SEL_ACCOUNT_RECRUITER, - SEL_BANS, SEL_ACCOUNT_WHOIS, SEL_REALMLIST_SECURITY_LEVEL, DEL_ACCOUNT, - SEL_IP2NATION_COUNTRY, SEL_AUTOBROADCAST, SEL_LAST_ATTEMPT_IP, SEL_LAST_IP, @@ -249,34 +257,44 @@ namespace Framework.Database SEL_ACCOUNT_MUTE_INFO, DEL_ACCOUNT_MUTED, - SelBnetAuthentication, - UpdBnetAuthentication, + SEL_SECRET_DIGEST, + INS_SECRET_DIGEST, + DEL_SECRET_DIGEST, + + SEL_ACCOUNT_TOTP_SECRET, + UPD_ACCOUNT_TOTP_SECRET, + + SEL_BNET_AUTHENTICATION, + UPD_BNET_AUTHENTICATION, + SEL_BNET_EXISTING_AUTHENTICATION, SEL_BNET_EXISTING_AUTHENTICATION_BY_ID, - SelBnetAccountInfo, - UpdBnetLastLoginInfo, + UPD_BNET_EXISTING_AUTHENTICATION, + SEL_BNET_ACCOUNT_INFO, + UPD_BNET_LAST_LOGIN_INFO, UPD_BNET_GAME_ACCOUNT_LOGIN_INFO, - SelBnetCharacterCountsByAccountId, + SEL_BNET_CHARACTER_COUNTS_BY_ACCOUNT_ID, SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID, - SelBnetLastPlayerCharacters, + SEL_BNET_LAST_PLAYER_CHARACTERS, DEL_BNET_LAST_PLAYER_CHARACTERS, INS_BNET_LAST_PLAYER_CHARACTERS, INS_BNET_ACCOUNT, SEL_BNET_ACCOUNT_EMAIL_BY_ID, SEL_BNET_ACCOUNT_ID_BY_EMAIL, - UPD_BNET_PASSWORD, - SEL_BNET_ACCOUNT_SALT_BY_ID, + UPD_BNET_LOGON, SEL_BNET_CHECK_PASSWORD, + SEL_BNET_CHECK_PASSWORD_BY_EMAIL, UPD_BNET_ACCOUNT_LOCK, UPD_BNET_ACCOUNT_LOCK_CONTRY, SEL_BNET_ACCOUNT_ID_BY_GAME_ACCOUNT, UPD_BNET_GAME_ACCOUNT_LINK, SEL_BNET_MAX_ACCOUNT_INDEX, + SEL_BNET_GAME_ACCOUNT_LIST_SMALL, SEL_BNET_GAME_ACCOUNT_LIST, - UpdBnetFailedLogins, - InsBnetAccountAutoBanned, - DelBnetExpiredAccountBanned, - UpdBnetResetFailedLogins, + UPD_BNET_FAILED_LOGINS, + INS_BNET_ACCOUNT_AUTO_BANNED, + DEL_BNET_EXPIRED_ACCOUNT_BANNED, + UPD_BNET_RESET_FAILED_LOGINS, SEL_LAST_CHAR_UNDELETE, UPD_LAST_CHAR_UNDELETE, diff --git a/Source/Framework/Database/SQLTransaction.cs b/Source/Framework/Database/SQLTransaction.cs index ef401464c..4a7462f13 100644 --- a/Source/Framework/Database/SQLTransaction.cs +++ b/Source/Framework/Database/SQLTransaction.cs @@ -30,6 +30,8 @@ namespace Framework.Database { commands.Add(new MySqlCommand(string.Format(sql, args))); } + + public int GetSize() { return commands.Count; } } class TransactionTask : ISqlOperation diff --git a/Source/Framework/Logging/Log.cs b/Source/Framework/Logging/Log.cs index 5bd394da1..d1e22d91c 100644 --- a/Source/Framework/Logging/Log.cs +++ b/Source/Framework/Logging/Log.cs @@ -382,7 +382,6 @@ public enum LogLevel public enum LogFilter { - Misc, Achievement, Addon, Ahbot, @@ -404,11 +403,13 @@ public enum LogFilter Garrison, Gameevent, Guild, + Http, Instance, Lfg, Loot, MapsScript, Maps, + Misc, Movement, Network, Outdoorpvp, diff --git a/Source/Framework/Networking/AsyncAcceptor.cs b/Source/Framework/Networking/AsyncAcceptor.cs index 62f607384..846a01fb2 100644 --- a/Source/Framework/Networking/AsyncAcceptor.cs +++ b/Source/Framework/Networking/AsyncAcceptor.cs @@ -64,7 +64,7 @@ namespace Framework.Networking if (socket != null) { T newSocket = (T)Activator.CreateInstance(typeof(T), socket); - newSocket.Accept(); + newSocket.Start(); if (!_closed) AsyncAccept(); diff --git a/Source/Framework/Networking/Http/BaseHttpSocket.cs b/Source/Framework/Networking/Http/BaseHttpSocket.cs new file mode 100644 index 000000000..546735c6c --- /dev/null +++ b/Source/Framework/Networking/Http/BaseHttpSocket.cs @@ -0,0 +1,118 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Constants; +using Framework.Database; +using Framework.Web; +using System; +using System.Net.Sockets; + +namespace Framework.Networking.Http +{ + public interface IAbstractSocket : ISocket + { + public void SendResponse(RequestContext context); + + public void QueueQuery(QueryCallback queryCallback); + + public string GetClientInfo(); + + public Guid? GetSessionId(); + } + + public abstract class BaseSocket : SSLSocket, IAbstractSocket + { + public BaseSocket(Socket socket) : base(socket) { } + + public async override void ReadHandler(byte[] data, int receivedLength) + { + if (!IsOpen()) + return; + + if (!HttpHelper.ParseRequest(data, receivedLength, out var httpRequest)) + { + CloseSocket(); + return; + } + + if (!HandleMessage(httpRequest)) + { + CloseSocket(); + return; + } + + await AsyncRead(); + } + + bool HandleMessage(HttpHeader httpRequest) + { + RequestContext context = new() { request = httpRequest }; + + if (_state == null) + _state = ObtainSessionState(context); + + RequestHandlerResult status = RequestHandler(context); + + if (status != RequestHandlerResult.Async) + SendResponse(context); + + return status != RequestHandlerResult.Error; + } + + public virtual RequestHandlerResult RequestHandler(RequestContext context) { return 0; } + + public async void SendResponse(RequestContext context) + { + Log.outDebug(LogFilter.Http, $"{GetClientInfo()} Request {context.request.Method} {context.request.Path} done, status {context.response.Status}"); + + { + bool canLogRequestContent = context.handler == null || !context.handler.Flags.HasFlag(RequestHandlerFlag.DoNotLogRequestContent); + bool canLogResponseContent = context.handler == null || !context.handler.Flags.HasFlag(RequestHandlerFlag.DoNotLogResponseContent); + Log.outTrace(LogFilter.Http, $"{GetClientInfo()} Request: {(canLogRequestContent ? context.request.Content : "")}"); + Log.outTrace(LogFilter.Http, $"{GetClientInfo()} Response: {(canLogResponseContent ? context.response.Content : "")}"); + } + + await AsyncWrite(HttpHelper.CreateResponse(context)); + + if (!context.response.KeepAlive) + CloseSocket(); + } + + public void QueueQuery(QueryCallback queryCallback) + { + _queryProcessor.AddCallback(queryCallback); + } + + public override bool Update() + { + if (!base.Update()) + return false; + + this._queryProcessor.ProcessReadyCallbacks(); + return true; + } + + public string GetClientInfo() + { + string info = GetRemoteIpAddress().ToString(); + if (_state != null) + info += $", Session Id: {_state.Id}"; + + info += "]"; + return info; + } + + public Guid? GetSessionId() + { + if (_state != null) + return _state.Id; + + return null; + } + + public virtual SessionState ObtainSessionState(RequestContext context) { return null; } + + protected AsyncCallbackProcessor _queryProcessor = new(); + protected SessionState _state; + } +} \ No newline at end of file diff --git a/Source/Framework/Networking/Http/HttpService.cs b/Source/Framework/Networking/Http/HttpService.cs new file mode 100644 index 000000000..f99e93dce --- /dev/null +++ b/Source/Framework/Networking/Http/HttpService.cs @@ -0,0 +1,321 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Constants; +using Framework.Web; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace Framework.Networking.Http +{ + public class DispatcherService + { + public DispatcherService(LogFilter logFilter) + { + _logger = logFilter; + } + + public RequestHandlerResult HandleRequest(IAbstractSocket session, RequestContext context) + { + Log.outDebug(_logger, $"{session.GetClientInfo()} Starting request {context.request.Method} {context.request.Path}"); + + string path = new Func(() => + { + string path = context.request.Path; + int queryIndex = path.IndexOf('?'); + if (queryIndex != -1) + path = path.Substring(0, queryIndex); + return path; + })(); + + context.handler = new Func(() => + { + switch (context.request.Method) + { + case "GET": + case "HEAD": + return _getHandlers.LookupByKey(path); + case "POST": + return _postHandlers.LookupByKey(path); + default: + return null; + } + })(); + + DateTime responseDate = DateTime.Now; + //context.response.Date = responseDate - Timezone.GetSystemZoneOffsetAt(responseDate); + //context.response.Server.Add(BOOST_BEAST_VERSION_STRING); + context.response.KeepAlive = context.request.KeepAlive; + + if (context.handler == null) + return HandlePathNotFound(session, context); + + return context.handler.Func(session, context); + } + + RequestHandlerResult HandleBadRequest(IAbstractSocket session, RequestContext context) + { + context.response.Status = HttpStatusCode.BadRequest; + return RequestHandlerResult.Handled; + } + + public RequestHandlerResult HandleUnauthorized(IAbstractSocket session, RequestContext context) + { + context.response.Status = HttpStatusCode.Unauthorized; + return RequestHandlerResult.Handled; + } + + RequestHandlerResult HandlePathNotFound(IAbstractSocket session, RequestContext context) + { + context.response.Status = HttpStatusCode.NotFound; + return RequestHandlerResult.Handled; + } + + public void RegisterHandler(HttpMethod method, string path, Func handler, RequestHandlerFlag flags = RequestHandlerFlag.None) + { + var handlerMap = new Func>(() => + { + switch (method.Method) + { + case "GET": + return _getHandlers; + case "POST": + return _postHandlers; + default: + //ABORT_MSG($"Tried to register a handler for unsupported HTTP method {method}"); + return null; + } + })(); + + handlerMap[path] = new RequestHandler() { Func = handler, Flags = flags }; + Log.outInfo(_logger, $"Registered new handler for {method} {path}"); + } + + Dictionary _getHandlers = new(); + Dictionary _postHandlers = new(); + + LogFilter _logger; + } + + public class SessionService + { + public SessionService(LogFilter logFilter) + { + _logger = logFilter; + } + + public void InitAndStoreSessionState(SessionState state, IPAddress address) + { + state.RemoteAddress = address; + + // Generate session id + lock (_sessionsMutex) + { + while (state.Id == Guid.Empty || _sessions.ContainsKey(state.Id)) + state.Id = new(new byte[0].GenerateRandomKey(16)); + + Log.outDebug(_logger, $"Client at {address} created new session {state.Id}"); + _sessions[state.Id] = state; + } + } + + public void Start() + { + _inactiveSessionsKillTimer = new System.Timers.Timer(TimeSpan.FromMinutes(1)); + _inactiveSessionsKillTimer.Elapsed += (_, _) => KillInactiveSessions(); + _inactiveSessionsKillTimer.Start(); + } + + public void Stop() + { + _inactiveSessionsKillTimer = null; + + lock (_sessionsMutex) + _sessions.Clear(); + + lock (_inactiveSessionsMutex) + _inactiveSessions.Clear(); + } + + public SessionState FindAndRefreshSessionState(string id, IPAddress address) + { + SessionState state; + + lock (_sessionsMutex) + { + if (!_sessions.TryGetValue(Guid.Parse(id), out state)) + { + Log.outDebug(_logger, $"Client at {address} attempted to use a session {id} that was expired"); + return null; // no session + } + } + + if (!state.RemoteAddress.Equals(address)) + { + Log.outError(_logger, $"Client at {address} attempted to use a session {id} that was last accessed from {state.RemoteAddress}, denied access"); + return null; + } + + lock (_inactiveSessionsMutex) + { + _inactiveSessions.Remove(state.Id); + } + + return state; + } + + public void MarkSessionInactive(Guid id) + { + bool wasActive = true; + lock (_inactiveSessionsMutex) + { + wasActive = !_inactiveSessions.Contains(id); + if (wasActive) + _inactiveSessions.Add(id); + } + + if (wasActive) + { + lock (_sessionsMutex) + { + var itr = _sessions.LookupByKey(id); + if (itr != null) + { + itr.InactiveTimestamp = DateTime.Now + TimeSpan.FromMinutes(5); + Log.outTrace(_logger, $"Session {id} marked as inactive"); + } + } + } + } + + void KillInactiveSessions() + { + lock (_inactiveSessionsMutex) + { + List inactiveSessions = new(_inactiveSessions); + _inactiveSessions.Clear(); + + + DateTime now = DateTime.Now; + int inactiveSessionsCount = inactiveSessions.Count; + + lock (_sessionsMutex) + { + foreach (var guid in inactiveSessions.ToList()) + { + if (!_sessions.TryGetValue(guid, out var sessionState) || sessionState.InactiveTimestamp < now) + { + _sessions.Remove(guid); + inactiveSessions.Remove(guid); + } + } + } + + Log.outDebug(_logger, $"Killed {inactiveSessionsCount - inactiveSessions.Count} inactive sessions"); + + // restore sessions not killed to inactive queue + foreach (var guid in inactiveSessions.ToList()) + { + _inactiveSessions.Add(guid); + inactiveSessions.Remove(guid); + } + } + } + + object _sessionsMutex = new(); + Dictionary _sessions = new(); + + object _inactiveSessionsMutex = new(); + List _inactiveSessions = new(); + System.Timers.Timer _inactiveSessionsKillTimer; + + LogFilter _logger; + } + + public class HttpService : SocketManager where SessionImpl : IAbstractSocket + { + protected DispatcherService dispatcherService; + protected SessionService sessionService; + + public HttpService(LogFilter logFilter) : base() + { + dispatcherService = new(logFilter); + sessionService = new(logFilter); + _logger = logFilter; + } + + public override bool StartNetwork(string bindIp, int port, int threadCount = 1) + { + if (!base.StartNetwork(bindIp, port, threadCount)) + return false; + + sessionService.Start(); + return true; + } + + public override void StopNetwork() + { + sessionService.Stop(); + base.StopNetwork(); + } + + // http handling + public delegate RequestHandlerResult HttpRequestHandler(SessionImpl session, RequestContext context); + public void RegisterHandler(HttpMethod method, string path, HttpRequestHandler handler, RequestHandlerFlag flags = RequestHandlerFlag.None) + { + dispatcherService.RegisterHandler(method, path, (session, context) => + { + return handler((SessionImpl)session, context); + }, flags); + } + + // session tracking + public virtual SessionState CreateNewSessionState(IPAddress address) + { + var state = new SessionState(); + sessionService.InitAndStoreSessionState(state, address); + return state; + } + + class Thread : NetworkThread + { + protected override void SocketRemoved(SessionImpl session) + { + var id = session.GetSessionId(); + if (id.HasValue) + _service.MarkSessionInactive(id.Value); + } + + public SessionService _service; + } + + public NetworkThread[] CreateThreads() + { + Thread[] threads = new Thread[GetNetworkThreadCount()]; + for (int i = 0; i < GetNetworkThreadCount(); ++i) + threads[i]._service = sessionService; + return threads; + } + + public DispatcherService GetDispatcherService() { return dispatcherService; } + public SessionService GetSessionService() { return sessionService; } + + protected LogFilter _logger; + } + + public class RequestHandler + { + public Func Func; + public RequestHandlerFlag Flags; + } + + public class RequestContext + { + public HttpHeader request = new(); + public HttpHeader response = new(); + public RequestHandler handler; + } +} \ No newline at end of file diff --git a/Source/Framework/Networking/Http/HttpSessionState.cs b/Source/Framework/Networking/Http/HttpSessionState.cs new file mode 100644 index 000000000..230637b7b --- /dev/null +++ b/Source/Framework/Networking/Http/HttpSessionState.cs @@ -0,0 +1,15 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System; +using System.Net; + +namespace Framework.Networking.Http +{ + public class SessionState + { + public Guid Id; + public IPAddress RemoteAddress; + public DateTime InactiveTimestamp = DateTime.MaxValue; + } +} \ No newline at end of file diff --git a/Source/Framework/Networking/Http/HttpSslSocket.cs b/Source/Framework/Networking/Http/HttpSslSocket.cs new file mode 100644 index 000000000..f85cf4589 --- /dev/null +++ b/Source/Framework/Networking/Http/HttpSslSocket.cs @@ -0,0 +1,39 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Security.Cryptography.X509Certificates; +using Framework.Configuration; + +namespace Framework.Networking.Http +{ + public class SslSocket : BaseSocket + { + X509Certificate2 _certificate; + + public SslSocket(Socket socket) : base(socket) + { + _certificate = new X509Certificate2(ConfigMgr.GetDefaultValue("CertificatesFile", "./BNetServer.pfx")); + } + + public async override void Start() + { + await AsyncHandshake(_certificate); + } + + public async override Task HandshakeHandler(Exception exception = null) + { + if (exception != null) + { + Log.outError(LogFilter.Http, $"{GetClientInfo()} SSL Handshake failed {exception.Message}"); + CloseSocket(); + return; + } + + await AsyncRead(); + } + } +} diff --git a/Source/Framework/Networking/SSLSocket.cs b/Source/Framework/Networking/SSLSocket.cs index 771a9be50..817cafd7e 100644 --- a/Source/Framework/Networking/SSLSocket.cs +++ b/Source/Framework/Networking/SSLSocket.cs @@ -33,16 +33,16 @@ namespace Framework.Networking _stream.Dispose(); } - public abstract void Accept(); + public abstract void Start(); public virtual bool Update() { return _socket.Connected; } - public IPEndPoint GetRemoteIpEndPoint() + public IPAddress GetRemoteIpAddress() { - return _remoteEndPoint; + return _remoteEndPoint.Address; } public async Task AsyncRead() @@ -63,7 +63,7 @@ namespace Framework.Networking } catch (Exception ex) { - Log.outException(ex); + Log.outDebug(LogFilter.Network, ex.Message); } } @@ -73,16 +73,17 @@ namespace Framework.Networking { await _stream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls12, false); } - catch(Exception ex) + catch (Exception ex) { - Log.outException(ex); - CloseSocket(); + await HandshakeHandler(ex); return; } - await AsyncRead(); + await HandshakeHandler(); } + public abstract Task HandshakeHandler(Exception ex = null); + public abstract void ReadHandler(byte[] data, int receivedLength); public async Task AsyncWrite(byte[] data) @@ -109,7 +110,7 @@ namespace Framework.Networking } catch (Exception ex) { - Log.outDebug(LogFilter.Network, $"WorldSocket.CloseSocket: {GetRemoteIpEndPoint()} errored when shutting down socket: {ex.Message}"); + Log.outDebug(LogFilter.Network, $"WorldSocket.CloseSocket: {GetRemoteIpAddress()} errored when shutting down socket: {ex.Message}"); } } diff --git a/Source/Framework/Networking/SocketBase.cs b/Source/Framework/Networking/SocketBase.cs index e756c9267..bdfe185ba 100644 --- a/Source/Framework/Networking/SocketBase.cs +++ b/Source/Framework/Networking/SocketBase.cs @@ -9,7 +9,7 @@ namespace Framework.Networking { public interface ISocket { - void Accept(); + void Start(); bool Update(); bool IsOpen(); void CloseSocket(); @@ -43,7 +43,7 @@ namespace Framework.Networking _socket.Dispose(); } - public abstract void Accept(); + public virtual void Start() { } public virtual bool Update() { diff --git a/Source/Framework/Networking/SocketManager.cs b/Source/Framework/Networking/SocketManager.cs index 80dc16882..b0e9c9ec4 100644 --- a/Source/Framework/Networking/SocketManager.cs +++ b/Source/Framework/Networking/SocketManager.cs @@ -32,8 +32,6 @@ namespace Framework.Networking _threads[i].Start(); } - Acceptor.AsyncAcceptSocket(OnSocketOpen); - return true; } @@ -64,7 +62,7 @@ namespace Framework.Networking try { TSocketType newSocket = (TSocketType)Activator.CreateInstance(typeof(TSocketType), sock); - newSocket.Accept(); + newSocket.Start(); _threads[SelectThreadWithMinConnections()].AddSocket(newSocket); } @@ -87,4 +85,4 @@ namespace Framework.Networking return min; } } -} +} \ No newline at end of file diff --git a/Source/Framework/Realm/RealmManager.cs b/Source/Framework/Realm/RealmManager.cs index 8e599b356..2f44721a8 100644 --- a/Source/Framework/Realm/RealmManager.cs +++ b/Source/Framework/Realm/RealmManager.cs @@ -3,15 +3,18 @@ using Framework.Constants; using Framework.Database; +using Framework.IO; +using Framework.Realm; using Framework.Web; -using Framework.Serialization; +using Framework.Web.Rest.Realmlist; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text; +using System.Text.Json; using System.Timers; -using System.Collections.Concurrent; -using Framework.Realm; public class RealmManager : Singleton { @@ -71,7 +74,7 @@ public class RealmManager : Singleton { var oldRealm = _realms.LookupByKey(realm.Id); if (oldRealm != null && oldRealm == realm) - return; + return; _realms[realm.Id] = realm; } @@ -154,7 +157,7 @@ public class RealmManager : Singleton return true; } -public RealmBuildInfo GetBuildInfo(uint build) + public RealmBuildInfo GetBuildInfo(uint build) { foreach (var clientBuild in _builds) if (clientBuild.Build == build) @@ -217,7 +220,10 @@ public RealmBuildInfo GetBuildInfo(uint build) realmEntry.CfgConfigsID = (int)realm.GetConfigId(); realmEntry.CfgLanguagesID = 1; - compressed = Json.Deflate("JamJSONRealmEntry", realmEntry); + var jsonData = Encoding.UTF8.GetBytes("JamJSONRealmEntry:" + JsonSerializer.Serialize(realmEntry) + "\0"); + var compressedData = ZLib.Compress(jsonData); + + compressed = BitConverter.GetBytes(jsonData.Length).Combine(compressedData); } } @@ -269,10 +275,13 @@ public RealmBuildInfo GetBuildInfo(uint build) realmList.Updates.Add(realmListUpdate); } - return Json.Deflate("JSONRealmListUpdates", realmList); + var jsonData = Encoding.UTF8.GetBytes("JSONRealmListUpdates:" + JsonSerializer.Serialize(realmList) + "\0"); + var compressedData = ZLib.Compress(jsonData); + + return BitConverter.GetBytes(jsonData.Length).Combine(compressedData); } - public BattlenetRpcErrorCode JoinRealm(uint realmAddress, uint build, IPAddress clientAddress, byte[] clientSecret, Locale locale, string os, string accountName, Bgs.Protocol.GameUtilities.V1.ClientResponse response) + public BattlenetRpcErrorCode JoinRealm(uint realmAddress, uint build, IPAddress clientAddress, byte[] clientSecret, Locale locale, string os, TimeSpan timezoneOffset, string accountName, Bgs.Protocol.GameUtilities.V1.ClientResponse response) { Realm realm = GetRealm(new RealmId(realmAddress)); if (realm != null) @@ -290,17 +299,21 @@ public RealmBuildInfo GetBuildInfo(uint build) addressFamily.Addresses.Add(address); serverAddresses.Families.Add(addressFamily); - byte[] compressed = Json.Deflate("JSONRealmListServerIPAddresses", serverAddresses); + var jsonData = Encoding.UTF8.GetBytes("JSONRealmListServerIPAddresses:" + JsonSerializer.Serialize(serverAddresses) + "\0"); + var compressedData = ZLib.Compress(jsonData); + + byte[] compressed = BitConverter.GetBytes(jsonData.Length).Combine(compressedData); byte[] serverSecret = new byte[0].GenerateRandomKey(32); - byte[] keyData = clientSecret.ToArray().Combine(serverSecret); + byte[] keyData = clientSecret.Combine(serverSecret); PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_GAME_ACCOUNT_LOGIN_INFO); stmt.AddValue(0, keyData); stmt.AddValue(1, clientAddress.ToString()); stmt.AddValue(2, (byte)locale); stmt.AddValue(3, os); - stmt.AddValue(4, accountName); + stmt.AddValue(4, (short)timezoneOffset.TotalMinutes); + stmt.AddValue(5, accountName); DB.Login.DirectExecute(stmt); Bgs.Protocol.Attribute attribute = new(); diff --git a/Source/Framework/Serialization/Json.cs b/Source/Framework/Serialization/Json.cs deleted file mode 100644 index 42fb2ad90..000000000 --- a/Source/Framework/Serialization/Json.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) CypherCore All rights reserved. -// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. - -using System; -using System.IO; -using System.Runtime.Serialization.Json; -using System.Text; - -namespace Framework.Serialization -{ - public class Json - { - public static string CreateString(T dataObject) - { - return Encoding.UTF8.GetString(CreateArray(dataObject)); - } - - public static byte[] CreateArray(T dataObject) - { - var serializer = new DataContractJsonSerializer(typeof(T)); - var stream = new MemoryStream(); - - serializer.WriteObject(stream, dataObject); - - return stream.ToArray(); - } - - public static T CreateObject(Stream jsonData) - { - var serializer = new DataContractJsonSerializer(typeof(T)); - - return (T)serializer.ReadObject(jsonData); - } - - public static T CreateObject(string jsonData, bool split = false) - { - return CreateObject(Encoding.UTF8.GetBytes(split ? jsonData.Split(new[] { ':' }, 2)[1] : jsonData)); - } - - public static T CreateObject(byte[] jsonData) => CreateObject(new MemoryStream(jsonData)); - - public static object CreateObject(Stream jsonData, Type type) - { - var serializer = new DataContractJsonSerializer(type); - - return serializer.ReadObject(jsonData); - } - - public static object CreateObject(string jsonData, Type type, bool split = false) - { - return CreateObject(Encoding.UTF8.GetBytes(split ? jsonData.Split(new[] { ':' }, 2)[1] : jsonData), type); - } - - public static object CreateObject(byte[] jsonData, Type type) => CreateObject(new MemoryStream(jsonData), type); - - // Used for protobuf json strings. - public static byte[] Deflate(string name, T data) - { - var jsonData = Encoding.UTF8.GetBytes(name + ":" + CreateString(data) + "\0"); - var compressedData = IO.ZLib.Compress(jsonData); - - return BitConverter.GetBytes(jsonData.Length).Combine(compressedData); - } - } -} diff --git a/Source/Framework/Util/Extensions.cs b/Source/Framework/Util/Extensions.cs index 77eed0f4e..311752735 100644 --- a/Source/Framework/Util/Extensions.cs +++ b/Source/Framework/Util/Extensions.cs @@ -6,10 +6,10 @@ using System.IO; using System.Linq; using System.Numerics; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; -using System.Runtime.CompilerServices; namespace System { @@ -29,8 +29,8 @@ namespace System else return byteArray.Aggregate("", (current, b) => current + b.ToString("X2")); } - - public static byte[] ToByteArray(this string str) + + public static byte[] ToByteArray(this string str, bool reverse = false) { str = str.Replace(" ", String.Empty); @@ -40,7 +40,7 @@ namespace System string temp = String.Concat(str[i * 2], str[i * 2 + 1]); res[i] = Convert.ToByte(temp, 16); } - return res; + return reverse ? res.Reverse().ToArray() : res; } public static byte[] ToByteArray(this string value, char separator) @@ -124,7 +124,7 @@ namespace System public static uint[] SerializeObject(this T obj) { //if (obj.GetType()() == null) - //return null; + //return null; var size = Marshal.SizeOf(typeof(T)); var ptr = Marshal.AllocHGlobal(size); @@ -233,7 +233,7 @@ namespace System float invSqrt = 1.0f / MathF.Sqrt(lenSquared); return new Vector3(vector.X * invSqrt, vector.Y * invSqrt, vector.Z * invSqrt); } - + public static Vector3 directionOrZero(this Vector3 vector) { float mag = vector.LengthSquared(); @@ -282,7 +282,7 @@ namespace System x = 0.0f; } } - + public static Matrix4x4 fromEulerAnglesZYX(float fYAngle, float fPAngle, float fRAngle) { float fCos = MathF.Cos(fYAngle); @@ -318,7 +318,7 @@ namespace System public static string ConvertFormatSyntax(this string str) { string pattern = @"(%\W*\d*[a-zA-Z]*)"; - + int count = 0; string result = Regex.Replace(str, pattern, m => string.Concat("{", count++, "}")); diff --git a/Source/Framework/Util/RandomHelper.cs b/Source/Framework/Util/RandomHelper.cs index 5e4c13e96..81bcaae47 100644 --- a/Source/Framework/Util/RandomHelper.cs +++ b/Source/Framework/Util/RandomHelper.cs @@ -84,6 +84,13 @@ public class RandomHelper rand.NextBytes(buffer); } + public static byte[] GetRandomBytes(int length) + { + byte[] buffer = new byte[length]; + rand.NextBytes(buffer); + return buffer; + } + public static T RAND(params T[] args) { int randIndex = IRand(0, args.Length - 1); diff --git a/Source/Framework/Web/API/ApiRequest.cs b/Source/Framework/Web/API/ApiRequest.cs deleted file mode 100644 index dabf03d14..000000000 --- a/Source/Framework/Web/API/ApiRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Framework.Web.API -{ - public class ApiRequest - { - public uint? SearchId { get; set; } - public Func SearchFunc { get; set; } - } -} diff --git a/Source/Framework/Web/Http.cs b/Source/Framework/Web/Http.cs index fd0ec1774..91409122e 100644 --- a/Source/Framework/Web/Http.cs +++ b/Source/Framework/Web/Http.cs @@ -1,10 +1,12 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. +using Framework.Networking.Http; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Reflection; using System.Text; @@ -15,14 +17,14 @@ namespace Framework.Web public string Method { get; set; } public string Path { get; set; } public string Type { get; set; } - public string Host { get; set; } - public string DeviceId { get; set; } + public string Authorization { get; set; } public string ContentType { get; set; } public int ContentLength { get; set; } - public string AcceptLanguage { get; set; } - public string Accept { get; set; } - public string UserAgent { get; set; } - public string Content { get; set; } + public string Content { get; set; } = ""; + public HttpStatusCode Status { get; set; } = HttpStatusCode.OK; + public string Cookie { get; set; } + public bool KeepAlive { get; set; } + public string Host { get; set; } } public enum HttpCode @@ -35,43 +37,41 @@ namespace Framework.Web public class HttpHelper { - public static byte[] CreateResponse(HttpCode httpCode, string content, bool closeConnection = false) + public static byte[] CreateResponse(RequestContext requestContext) { var sb = new StringBuilder(); using (var sw = new StringWriter(sb)) { - sw.WriteLine($"HTTP/1.1 {(int)httpCode} {httpCode}"); + sw.WriteLine($"HTTP/1.1 {(int)requestContext.response.Status} {requestContext.response.Status}"); + sw.WriteLine($"Content-Length: {requestContext.response.Content.Length}"); - //sw.WriteLine($"Date: {DateTime.Now.ToUniversalTime():r}"); - //sw.WriteLine("Server: Arctium-Emulation"); - //sw.WriteLine("Retry-After: 600"); - sw.WriteLine($"Content-Length: {content.Length}"); - //sw.WriteLine("Vary: Accept-Encoding"); - - if (closeConnection) + if (!requestContext.response.KeepAlive) sw.WriteLine("Connection: close"); - sw.WriteLine("Content-Type: application/json;charset=UTF-8"); + if (!requestContext.response.Cookie.IsEmpty()) + sw.WriteLine($"Set-Cookie: {requestContext.response.Cookie}"); + + sw.WriteLine($"Content-Type: {requestContext.response.ContentType}"); sw.WriteLine(); - sw.WriteLine(content); + sw.WriteLine(requestContext.response.Content); } return Encoding.UTF8.GetBytes(sb.ToString()); } - public static HttpHeader ParseRequest(byte[] data, int length) + public static bool ParseRequest(byte[] data, int length, out HttpHeader httpHeader) { var headerValues = new Dictionary(); - var header = new HttpHeader(); + httpHeader = new HttpHeader(); using (var sr = new StreamReader(new MemoryStream(data, 0, length))) { var info = sr.ReadLine().Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries); if (info.Length != 3) - return null; + return false; headerValues.Add("method", info[0]); headerValues.Add("path", info[1]); @@ -113,13 +113,13 @@ namespace Framework.Web if (headerValues.TryGetValue(f.Name.ToLower(), out val)) { if (f.PropertyType == typeof(int)) - f.SetValue(header, Convert.ChangeType(Convert.ToInt32(val), f.PropertyType)); + f.SetValue(httpHeader, Convert.ChangeType(Convert.ToInt32(val), f.PropertyType)); else - f.SetValue(header, Convert.ChangeType(val, f.PropertyType)); + f.SetValue(httpHeader, Convert.ChangeType(val, f.PropertyType)); } } - return header; + return true; } } } diff --git a/Source/Framework/Web/Rest/Authentication/LogonData.cs b/Source/Framework/Web/Rest/Authentication/LogonData.cs deleted file mode 100644 index ffa4c1fba..000000000 --- a/Source/Framework/Web/Rest/Authentication/LogonData.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) CypherCore All rights reserved. -// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; - -namespace Framework.Web -{ - [DataContract] - public class LogonData - { - public string this[string inputId] => Inputs.SingleOrDefault(i => i.Id == inputId)?.Value; - - [DataMember(Name = "version")] - public string Version { get; set; } - - [DataMember(Name = "program_id")] - public string Program { get; set; } - - [DataMember(Name = "platform_id")] - public string Platform { get; set; } - - [DataMember(Name = "inputs")] - public List Inputs { get; set; } = new List(); - } -} diff --git a/Source/Framework/Web/Rest/Forms/FormInput.cs b/Source/Framework/Web/Rest/Login/FormInput.cs similarity index 63% rename from Source/Framework/Web/Rest/Forms/FormInput.cs rename to Source/Framework/Web/Rest/Login/FormInput.cs index 77a9f43d7..c33d1f721 100644 --- a/Source/Framework/Web/Rest/Forms/FormInput.cs +++ b/Source/Framework/Web/Rest/Login/FormInput.cs @@ -1,23 +1,22 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Login { - [DataContract] public class FormInput { - [DataMember(Name = "input_id")] + [JsonPropertyName("input_id")] public string Id { get; set; } - [DataMember(Name = "type")] + [JsonPropertyName("type")] public string Type { get; set; } - [DataMember(Name = "label")] + [JsonPropertyName("label")] public string Label { get; set; } - [DataMember(Name = "max_length")] + [JsonPropertyName("max_length")] public int MaxLength { get; set; } } } diff --git a/Source/Framework/Web/Rest/Forms/FormInputValue.cs b/Source/Framework/Web/Rest/Login/FormInputValue.cs similarity index 68% rename from Source/Framework/Web/Rest/Forms/FormInputValue.cs rename to Source/Framework/Web/Rest/Login/FormInputValue.cs index f67f2af0c..7f5e0a4b5 100644 --- a/Source/Framework/Web/Rest/Forms/FormInputValue.cs +++ b/Source/Framework/Web/Rest/Login/FormInputValue.cs @@ -1,17 +1,16 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Login { - [DataContract] public class FormInputValue { - [DataMember(Name = "input_id")] + [JsonPropertyName("input_id")] public string Id { get; set; } - [DataMember(Name = "value")] + [JsonPropertyName("value")] public string Value { get; set; } } } diff --git a/Source/Framework/Web/Rest/Forms/FormInputs.cs b/Source/Framework/Web/Rest/Login/FormInputs.cs similarity index 56% rename from Source/Framework/Web/Rest/Forms/FormInputs.cs rename to Source/Framework/Web/Rest/Login/FormInputs.cs index aa2d5c799..fe48d84b8 100644 --- a/Source/Framework/Web/Rest/Forms/FormInputs.cs +++ b/Source/Framework/Web/Rest/Login/FormInputs.cs @@ -2,20 +2,22 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Login { - [DataContract] public class FormInputs { - [DataMember(Name = "type")] + [JsonPropertyName("type")] public string Type { get; set; } - [DataMember(Name = "prompt")] - public string Prompt { get; set; } - - [DataMember(Name = "inputs")] + [JsonPropertyName("inputs")] public List Inputs { get; set; } = new List(); + + [JsonPropertyName("srp_url")] + public string SrpUrl { get; set; } + + [JsonPropertyName("srp_js")] + public string SrpJs { get; set; } } } diff --git a/Source/Framework/Web/Rest/Login/GameAccountInfo.cs b/Source/Framework/Web/Rest/Login/GameAccountInfo.cs new file mode 100644 index 000000000..0c08fe9be --- /dev/null +++ b/Source/Framework/Web/Rest/Login/GameAccountInfo.cs @@ -0,0 +1,28 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace Framework.Web.Rest.Login +{ + public class GameAccountInfo + { + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } + + [JsonPropertyName("expansion")] + public int Expansion { get; set; } + + [JsonPropertyName("is_suspended")] + public bool IsSuspended { get; set; } + + [JsonPropertyName("is_banned")] + public bool IsBanned { get; set; } + + [JsonPropertyName("suspension_expires")] + public long SuspensionExpires { get; set; } + + [JsonPropertyName("suspension_reason")] + public string SuspensionReason { get; set; } + } +} diff --git a/Source/Framework/Web/Rest/Login/GameAccountList.cs b/Source/Framework/Web/Rest/Login/GameAccountList.cs new file mode 100644 index 000000000..baf978c63 --- /dev/null +++ b/Source/Framework/Web/Rest/Login/GameAccountList.cs @@ -0,0 +1,14 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Framework.Web.Rest.Login +{ + public class GameAccountList + { + [JsonPropertyName("game_accounts")] + public List GameAccounts { get; set; } + } +} diff --git a/Source/Framework/Web/Rest/Login/LoginForm.cs b/Source/Framework/Web/Rest/Login/LoginForm.cs new file mode 100644 index 000000000..4aee45d35 --- /dev/null +++ b/Source/Framework/Web/Rest/Login/LoginForm.cs @@ -0,0 +1,23 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Framework.Web.Rest.Login +{ + public class LoginForm + { + [JsonPropertyName("platform_id")] + public string PlatformId { get; set; } + + [JsonPropertyName("program_id")] + public string ProgramId { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("inputs")] + public List Inputs { get; set; } = new List(); + } +} diff --git a/Source/Framework/Web/Rest/Login/LoginRefreshResult.cs b/Source/Framework/Web/Rest/Login/LoginRefreshResult.cs new file mode 100644 index 000000000..56e0a5bda --- /dev/null +++ b/Source/Framework/Web/Rest/Login/LoginRefreshResult.cs @@ -0,0 +1,16 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace Framework.Web.Rest.Login +{ + public class LoginRefreshResult + { + [JsonPropertyName("login_ticket_expiry")] + public long LoginTicketExpiry { get; set; } + + [JsonPropertyName("is_expired")] + public bool IsExpired { get; set; } + } +} diff --git a/Source/Framework/Web/Rest/Authentication/LogonResult.cs b/Source/Framework/Web/Rest/Login/LoginResult.cs similarity index 52% rename from Source/Framework/Web/Rest/Authentication/LogonResult.cs rename to Source/Framework/Web/Rest/Login/LoginResult.cs index 2d1681be8..0c0f4387a 100644 --- a/Source/Framework/Web/Rest/Authentication/LogonResult.cs +++ b/Source/Framework/Web/Rest/Login/LoginResult.cs @@ -1,30 +1,29 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Login { - [DataContract] - public class LogonResult + public class LoginResult { - [DataMember(Name = "authentication_state")] + [JsonPropertyName("authentication_state")] public string AuthenticationState { get; set; } - [DataMember(Name = "login_ticket")] - public string LoginTicket { get; set; } - - [DataMember(Name = "error_code")] + [JsonPropertyName("error_code")] public string ErrorCode { get; set; } - [DataMember(Name = "error_message")] + [JsonPropertyName("error_message")] public string ErrorMessage { get; set; } - [DataMember(Name = "support_error_code")] - public string SupportErrorCode { get; set; } + [JsonPropertyName("url")] + public string Url { get; set; } - [DataMember(Name = "authenticator_form")] - public FormInputs AuthenticatorForm { get; set; } = new FormInputs(); + [JsonPropertyName("login_ticket")] + public string LoginTicket { get; set; } + + [JsonPropertyName("server_evidence_M2")] + public string ServerEvidenceM2 { get; set; } } public enum AuthenticationState diff --git a/Source/Framework/Web/Rest/Login/SrpLoginChallenge.cs b/Source/Framework/Web/Rest/Login/SrpLoginChallenge.cs new file mode 100644 index 000000000..5ff901e30 --- /dev/null +++ b/Source/Framework/Web/Rest/Login/SrpLoginChallenge.cs @@ -0,0 +1,37 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace Framework.Web.Rest.Login +{ + public class SrpLoginChallenge + { + [JsonPropertyName("version")] + public int Version { get; set; } + + [JsonPropertyName("iterations")] + public int Iterations { get; set; } + + [JsonPropertyName("modulus")] + public string Modulus { get; set; } + + [JsonPropertyName("generator")] + public string Generator { get; set; } + + [JsonPropertyName("hash_function")] + public string HashFunction { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("salt")] + public string Salt { get; set; } + + [JsonPropertyName("public_B")] + public string PublicB { get; set; } + + [JsonPropertyName("eligible_credential_upgrade")] + public bool EligibleCredentialUpgrade { get; set; } + } +} diff --git a/Source/Framework/Web/Rest/Misc/Address.cs b/Source/Framework/Web/Rest/Realmlist/Address.cs similarity index 68% rename from Source/Framework/Web/Rest/Misc/Address.cs rename to Source/Framework/Web/Rest/Realmlist/Address.cs index 0f071f463..a625407f0 100644 --- a/Source/Framework/Web/Rest/Misc/Address.cs +++ b/Source/Framework/Web/Rest/Realmlist/Address.cs @@ -1,17 +1,16 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Realmlist { - [DataContract] public class Address { - [DataMember(Name = "ip")] + [JsonPropertyName("ip")] public string Ip { get; set; } - [DataMember(Name = "port")] + [JsonPropertyName("port")] public int Port { get; set; } } } diff --git a/Source/Framework/Web/Rest/Misc/AddressFamily.cs b/Source/Framework/Web/Rest/Realmlist/AddressFamily.cs similarity index 71% rename from Source/Framework/Web/Rest/Misc/AddressFamily.cs rename to Source/Framework/Web/Rest/Realmlist/AddressFamily.cs index d41acca4e..129579ad2 100644 --- a/Source/Framework/Web/Rest/Misc/AddressFamily.cs +++ b/Source/Framework/Web/Rest/Realmlist/AddressFamily.cs @@ -2,17 +2,16 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Realmlist { - [DataContract] public class AddressFamily { - [DataMember(Name = "family")] + [JsonPropertyName("family")] public int Id { get; set; } - [DataMember(Name = "addresses")] + [JsonPropertyName("addresses")] public IList
Addresses { get; set; } = new List
(); } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListTicketInformation.cs b/Source/Framework/Web/Rest/Realmlist/ClientInformation.cs similarity index 58% rename from Source/Framework/Web/Rest/Realmlist/RealmListTicketInformation.cs rename to Source/Framework/Web/Rest/Realmlist/ClientInformation.cs index 06b6dfa89..50b93263f 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListTicketInformation.cs +++ b/Source/Framework/Web/Rest/Realmlist/ClientInformation.cs @@ -2,53 +2,53 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Framework.Web.Rest.Realmlist; namespace Framework.Web { - [DataContract] - public class RealmListTicketInformation + public class ClientInformation { - [DataMember(Name = "platform")] + [JsonPropertyName("platform")] public int Platform { get; set; } - [DataMember(Name = "buildVariant")] + [JsonPropertyName("buildVariant")] public string BuildVariant { get; set; } - [DataMember(Name = "type")] + [JsonPropertyName("type")] public int Type { get; set; } - [DataMember(Name = "timeZone")] + [JsonPropertyName("timeZone")] public string Timezone { get; set; } - [DataMember(Name = "currentTime")] + [JsonPropertyName("currentTime")] public int CurrentTime { get; set; } - [DataMember(Name = "textLocale")] + [JsonPropertyName("textLocale")] public int TextLocale { get; set; } - [DataMember(Name = "audioLocale")] + [JsonPropertyName("audioLocale")] public int AudioLocale { get; set; } - [DataMember(Name = "versionDataBuild")] + [JsonPropertyName("versionDataBuild")] public int VersionDataBuild { get; set; } - [DataMember(Name = "version")] + [JsonPropertyName("version")] public ClientVersion ClientVersion { get; set; } = new ClientVersion(); - [DataMember(Name = "secret")] + [JsonPropertyName("secret")] public List Secret { get; set; } - [DataMember(Name = "clientArch")] + [JsonPropertyName("clientArch")] public int ClientArch { get; set; } - [DataMember(Name = "systemVersion")] + [JsonPropertyName("systemVersion")] public string SystemVersion { get; set; } - [DataMember(Name = "platformType")] + [JsonPropertyName("platformType")] public int PlatformType { get; set; } - [DataMember(Name = "systemArch")] + [JsonPropertyName("systemArch")] public int SystemArch { get; set; } } } diff --git a/Source/Framework/Web/Rest/Misc/ClientVersion.cs b/Source/Framework/Web/Rest/Realmlist/ClientVersion.cs similarity index 61% rename from Source/Framework/Web/Rest/Misc/ClientVersion.cs rename to Source/Framework/Web/Rest/Realmlist/ClientVersion.cs index 416042d4a..9fe0b2587 100644 --- a/Source/Framework/Web/Rest/Misc/ClientVersion.cs +++ b/Source/Framework/Web/Rest/Realmlist/ClientVersion.cs @@ -1,23 +1,22 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Realmlist { - [DataContract] public class ClientVersion { - [DataMember(Name = "versionMajor")] + [JsonPropertyName("versionMajor")] public int Major { get; set; } - [DataMember(Name = "versionBuild")] + [JsonPropertyName("versionBuild")] public int Build { get; set; } - [DataMember(Name = "versionMinor")] + [JsonPropertyName("versionMinor")] public int Minor { get; set; } - [DataMember(Name = "versionRevision")] + [JsonPropertyName("versionRevision")] public int Revision { get; set; } } } diff --git a/Source/Framework/Web/Rest/Misc/RealmCharacterCountEntry.cs b/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountEntry.cs similarity index 68% rename from Source/Framework/Web/Rest/Misc/RealmCharacterCountEntry.cs rename to Source/Framework/Web/Rest/Realmlist/RealmCharacterCountEntry.cs index 228c1f456..8d135a0ed 100644 --- a/Source/Framework/Web/Rest/Misc/RealmCharacterCountEntry.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountEntry.cs @@ -1,17 +1,16 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; -namespace Framework.Web +namespace Framework.Web.Rest.Realmlist { - [DataContract] public class RealmCharacterCountEntry { - [DataMember(Name = "wowRealmAddress")] + [JsonPropertyName("wowRealmAddress")] public int WowRealmAddress { get; set; } - [DataMember(Name = "count")] + [JsonPropertyName("count")] public int Count { get; set; } } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountList.cs b/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountList.cs index 4503af6ca..16bdf2e14 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountList.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmCharacterCountList.cs @@ -2,14 +2,14 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Framework.Web.Rest.Realmlist; namespace Framework.Web { - [DataContract] public class RealmCharacterCountList { - [DataMember(Name = "counts")] + [JsonPropertyName("counts")] public IList Counts { get; set; } = new List(); } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmEntry.cs b/Source/Framework/Web/Rest/Realmlist/RealmEntry.cs index d39d1ff7c..ce5473eb7 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmEntry.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmEntry.cs @@ -1,42 +1,41 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Framework.Web.Rest.Realmlist; namespace Framework.Web { - [DataContract] public class RealmEntry { - - [DataMember(Name = "wowRealmAddress")] + [JsonPropertyName("wowRealmAddress")] public int WowRealmAddress { get; set; } - [DataMember(Name = "cfgTimezonesID")] + [JsonPropertyName("cfgTimezonesID")] public int CfgTimezonesID { get; set; } - [DataMember(Name = "populationState")] + [JsonPropertyName("populationState")] public int PopulationState { get; set; } - [DataMember(Name = "cfgCategoriesID")] + [JsonPropertyName("cfgCategoriesID")] public int CfgCategoriesID { get; set; } - [DataMember(Name = "version")] + [JsonPropertyName("version")] public ClientVersion Version { get; set; } = new ClientVersion(); - [DataMember(Name = "cfgRealmsID")] + [JsonPropertyName("cfgRealmsID")] public int CfgRealmsID { get; set; } - [DataMember(Name = "flags")] + [JsonPropertyName("flags")] public int Flags { get; set; } - [DataMember(Name = "name")] + [JsonPropertyName("name")] public string Name { get; set; } - [DataMember(Name = "cfgConfigsID")] + [JsonPropertyName("cfgConfigsID")] public int CfgConfigsID { get; set; } - [DataMember(Name = "cfgLanguagesID")] + [JsonPropertyName("cfgLanguagesID")] public int CfgLanguagesID { get; set; } } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListServerIPAddresses.cs b/Source/Framework/Web/Rest/Realmlist/RealmListServerIPAddresses.cs index e867b19a0..d13d01dca 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListServerIPAddresses.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmListServerIPAddresses.cs @@ -2,14 +2,14 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Framework.Web.Rest.Realmlist; namespace Framework.Web { - [DataContract] public class RealmListServerIPAddresses { - [DataMember(Name = "families")] + [JsonPropertyName("families")] public IList Families { get; set; } = new List(); } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListTicketClientInformation.cs b/Source/Framework/Web/Rest/Realmlist/RealmListTicketClientInformation.cs index d1f73f034..bbc99b589 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListTicketClientInformation.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmListTicketClientInformation.cs @@ -1,14 +1,13 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Framework.Web { - [DataContract] public class RealmListTicketClientInformation { - [DataMember(Name = "info")] - public RealmListTicketInformation Info { get; set; } = new RealmListTicketInformation(); + [JsonPropertyName("info")] + public ClientInformation Info { get; set; } = new ClientInformation(); } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListTicketIdentity.cs b/Source/Framework/Web/Rest/Realmlist/RealmListTicketIdentity.cs index 7c9b2c70f..851392623 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListTicketIdentity.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmListTicketIdentity.cs @@ -1,17 +1,16 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Framework.Web { - [DataContract] public class RealmListTicketIdentity { - [DataMember(Name = "gameAccountID")] + [JsonPropertyName("gameAccountID")] public int GameAccountId { get; set; } - [DataMember(Name = "gameAccountRegion")] + [JsonPropertyName("gameAccountRegion")] public int GameAccountRegion { get; set; } } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListUpdate.cs b/Source/Framework/Web/Rest/Realmlist/RealmListUpdate.cs index e45b131f9..b4f12c49c 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListUpdate.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmListUpdate.cs @@ -1,17 +1,16 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. -using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Framework.Web { - [DataContract] public class RealmListUpdate { - [DataMember(Name = "update")] + [JsonPropertyName("update")] public RealmEntry Update { get; set; } = new RealmEntry(); - [DataMember(Name = "deleting")] + [JsonPropertyName("deleting")] public bool Deleting { get; set; } } } diff --git a/Source/Framework/Web/Rest/Realmlist/RealmListUpdates.cs b/Source/Framework/Web/Rest/Realmlist/RealmListUpdates.cs index 64a880584..e74960ce6 100644 --- a/Source/Framework/Web/Rest/Realmlist/RealmListUpdates.cs +++ b/Source/Framework/Web/Rest/Realmlist/RealmListUpdates.cs @@ -2,14 +2,13 @@ // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Framework.Web { - [DataContract] public class RealmListUpdates { - [DataMember(Name = "updates")] + [JsonPropertyName("updates")] public IList Updates { get; set; } = new List(); } } diff --git a/Source/Game/Accounts/AccountManager.cs b/Source/Game/Accounts/AccountManager.cs index d627764fb..47016ef77 100644 --- a/Source/Game/Accounts/AccountManager.cs +++ b/Source/Game/Accounts/AccountManager.cs @@ -34,7 +34,7 @@ namespace Game if (GetId(username) != 0) return AccountOpResult.NameAlreadyExist; - (byte[] salt, byte[] verifier) = SRP6.MakeRegistrationData(username, password); + (byte[] salt, byte[] verifier) = SRP6.MakeAccountRegistrationData(username, password); PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.INS_ACCOUNT); stmt.AddValue(0, username); @@ -152,7 +152,7 @@ namespace Game stmt.AddValue(1, accountId); DB.Login.Execute(stmt); - (byte[] salt, byte[] verifier) = SRP6.MakeRegistrationData(newUsername, newPassword); + (byte[] salt, byte[] verifier) = SRP6.MakeAccountRegistrationData(newUsername, newPassword); stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_LOGON); stmt.AddValue(0, salt); stmt.AddValue(1, verifier); @@ -178,7 +178,7 @@ namespace Game return AccountOpResult.PassTooLong; } - (byte[] salt, byte[] verifier) = SRP6.MakeRegistrationData(username, newPassword); + (byte[] salt, byte[] verifier) = SRP6.MakeAccountRegistrationData(username, newPassword); PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_LOGON); stmt.AddValue(0, salt); @@ -304,7 +304,7 @@ namespace Game { byte[] salt = result.Read(0); byte[] verifier = result.Read(1); - if (SRP6.CheckLogin(username, password, salt, verifier)) + if (new GruntSRP6(username, salt, verifier).CheckCredentials(username, password)) return true; } diff --git a/Source/Game/Accounts/BNetAccountManager.cs b/Source/Game/Accounts/BNetAccountManager.cs index 301e0dbb1..dde137996 100644 --- a/Source/Game/Accounts/BNetAccountManager.cs +++ b/Source/Game/Accounts/BNetAccountManager.cs @@ -1,6 +1,7 @@ // Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. +using Framework.Cryptography; using Framework.Database; using System; using System.Security.Cryptography; @@ -10,24 +11,32 @@ namespace Game { public sealed class BNetAccountManager : Singleton { + static int MAX_BNET_EMAIL_STR = 320; + static int MAX_BNET_PASS_STR = 128; + BNetAccountManager() { } public AccountOpResult CreateBattlenetAccount(string email, string password, bool withGameAccount, out string gameAccountName) { gameAccountName = ""; - if (email.IsEmpty() || email.Length > 320) + if (email.IsEmpty() || email.Length > MAX_BNET_EMAIL_STR) return AccountOpResult.NameTooLong; - if (password.IsEmpty() || password.Length > 16) + if (password.IsEmpty() || password.Length > MAX_BNET_PASS_STR) return AccountOpResult.PassTooLong; if (GetId(email) != 0) return AccountOpResult.NameAlreadyExist; + string srpUsername = GetSrpUsername(email); + var (salt, verifier) = SRP6.MakeBNetRegistrationData(srpUsername, password); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.INS_BNET_ACCOUNT); stmt.AddValue(0, email); - stmt.AddValue(1, CalculateShaPassHash(email.ToUpper(), password.ToUpper())); + stmt.AddValue(1, (sbyte)SrpVersion.v2); + stmt.AddValue(2, salt); + stmt.AddValue(3, verifier); DB.Login.DirectExecute(stmt); uint newAccountId = GetId(email); @@ -36,7 +45,8 @@ namespace Game if (withGameAccount) { gameAccountName = newAccountId + "#1"; - Global.AccountMgr.CreateAccount(gameAccountName, password, email, newAccountId, 1); + string gameAccountPassword = password.Substring(0, 16); + Global.AccountMgr.CreateAccount(gameAccountName, gameAccountPassword.ToUpper(), email, newAccountId, 1); } return AccountOpResult.Ok; @@ -48,12 +58,17 @@ namespace Game if (!GetName(accountId, out username)) return AccountOpResult.NameNotExist; - if (newPassword.Length > 16) + if (newPassword.Length > MAX_BNET_PASS_STR) return AccountOpResult.PassTooLong; - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_PASSWORD); - stmt.AddValue(0, CalculateShaPassHash(username, newPassword)); - stmt.AddValue(1, accountId); + string srpUsername = GetSrpUsername(username); + var (salt, verifier) = SRP6.MakeBNetRegistrationData(srpUsername, newPassword); + + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.UPD_BNET_LOGON); + stmt.AddValue(0, (sbyte)SrpVersion.v2); + stmt.AddValue(1, salt); + stmt.AddValue(2, verifier); + stmt.AddValue(3, accountId); DB.Login.Execute(stmt); return AccountOpResult.Ok; @@ -67,9 +82,23 @@ namespace Game PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_CHECK_PASSWORD); stmt.AddValue(0, accountId); - stmt.AddValue(1, CalculateShaPassHash(username, password)); + SQLResult result = DB.Login.Query(stmt); + if (!result.IsEmpty()) + { + var salt = result.Read(1); + var verifier = result.Read(2); + switch ((SrpVersion)result.Read(0)) + { + case SrpVersion.v1: + return new BnetSRP6v1Hash256(username, salt, verifier).CheckCredentials(username, password); + case SrpVersion.v2: + return new BnetSRP6v2Hash256(username, salt, verifier).CheckCredentials(username, password); + default: + break; + } + } - return !DB.Login.Query(stmt).IsEmpty(); + return false; } public AccountOpResult LinkWithGameAccount(string email, string gameAccountName) @@ -165,11 +194,15 @@ namespace Game return 0; } - public string CalculateShaPassHash(string name, string password) + public string GetSrpUsername(string name) { - SHA256 sha256 = SHA256.Create(); - var i = sha256.ComputeHash(Encoding.UTF8.GetBytes(name)); - return sha256.ComputeHash(Encoding.UTF8.GetBytes(i.ToHexString() + ":" + password)).ToHexString(true); + return SHA256.HashData(Encoding.UTF8.GetBytes(name)).ToHexString(); } } + + enum SrpVersion + { + v1 = 1, // password length limit 16 characters, case-insensitive, uses SHA256 to generate verifier + v2 = 2 // password length limit 128 characters, case-sensitive, uses PBKDF2 with SHA512 to generate verifier + } } diff --git a/Source/Game/Chat/Commands/BanCommands.cs b/Source/Game/Chat/Commands/BanCommands.cs index 505877164..57624c949 100644 --- a/Source/Game/Chat/Commands/BanCommands.cs +++ b/Source/Game/Chat/Commands/BanCommands.cs @@ -271,7 +271,7 @@ namespace Game.Chat.Commands [Command("account", RBACPermissions.CommandBanlistAccount, true)] static bool HandleBanListAccountCommand(CommandHandler handler, [OptionalArg] string filter) { - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.DelExpiredIpBans); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.DEL_EXPIRED_IP_BANS); DB.Login.Execute(stmt); SQLResult result; @@ -380,7 +380,7 @@ namespace Game.Chat.Commands [Command("ip", RBACPermissions.CommandBanlistIp, true)] static bool HandleBanListIPCommand(CommandHandler handler, [OptionalArg] string filter) { - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.DelExpiredIpBans); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.DEL_EXPIRED_IP_BANS); DB.Login.Execute(stmt); SQLResult result; diff --git a/Source/Game/Globals/Global.cs b/Source/Game/Globals/Global.cs index 7e24397fc..064db2e1f 100644 --- a/Source/Game/Globals/Global.cs +++ b/Source/Game/Globals/Global.cs @@ -16,9 +16,9 @@ using Game.DungeonFinding; using Game.Entities; using Game.Garrisons; using Game.Groups; -using Game.Guilds; using Game.Loots; using Game.Maps; +using Game.Networking; using Game.PvP; using Game.Scenarios; using Game.Scripting; @@ -33,6 +33,7 @@ public static class Global public static WorldManager WorldMgr { get { return WorldManager.Instance; } } public static RealmManager RealmMgr { get { return RealmManager.Instance; } } public static WorldServiceManager ServiceMgr { get { return WorldServiceManager.Instance; } } + public static WorldSocketManager WorldSocketMgr { get { return WorldSocketManager.Instance; } } //Guild public static PetitionManager PetitionMgr { get { return PetitionManager.Instance; } } diff --git a/Source/Game/Networking/RASocket.cs b/Source/Game/Networking/RASocket.cs index 03736cf9a..2c7712417 100644 --- a/Source/Game/Networking/RASocket.cs +++ b/Source/Game/Networking/RASocket.cs @@ -29,7 +29,7 @@ namespace Game.Networking _receiveBuffer = new byte[1024]; } - public void Accept() + public void Start() { // wait 1 second for active connections to send negotiation request for (int counter = 0; counter < 10 && _socket.Available == 0; counter++) @@ -46,7 +46,7 @@ namespace Game.Networking } Send("Authentication Required\r\n"); - Send("Email: "); + Send("Username: "); string userName = ReadString(); if (userName.IsEmpty()) { @@ -64,7 +64,7 @@ namespace Game.Networking return; } - if (!CheckAccessLevelAndPassword(userName, password)) + if (!CheckAccessLevel(userName) || Global.AccountMgr.CheckPassword(userName, password)) { Send("Authentication failed\r\n"); CloseSocket(); @@ -129,56 +129,29 @@ namespace Game.Networking } } - bool CheckAccessLevelAndPassword(string email, string password) + bool CheckAccessLevel(string user) { - //"SELECT a.id, a.username FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id WHERE ba.email = ?" - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_GAME_ACCOUNT_LIST); - stmt.AddValue(0, email); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_ACCOUNT_ACCESS); + stmt.AddValue(0, user); SQLResult result = DB.Login.Query(stmt); if (result.IsEmpty()) { - Log.outInfo(LogFilter.CommandsRA, $"User {email} does not exist in database"); + Log.outInfo(LogFilter.CommandsRA, $"User {user} does not exist in database"); return false; } - uint accountId = result.Read(0); - string username = result.Read(1); - - stmt = LoginDatabase.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"); + Log.outInfo(LogFilter.CommandsRA, $"User {user} 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')"); + Log.outInfo(LogFilter.CommandsRA, $"User {user} has to be assigned on all realms (with RealmID = '-1')"); return false; } - stmt = LoginDatabase.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; + return true; } bool ProcessCommand(string command) diff --git a/Source/Game/Networking/WorldSocket.cs b/Source/Game/Networking/WorldSocket.cs index 80c4a5411..28ad2c919 100644 --- a/Source/Game/Networking/WorldSocket.cs +++ b/Source/Game/Networking/WorldSocket.cs @@ -69,11 +69,11 @@ namespace Game.Networking base.Dispose(); } - public override void Accept() + public override void Start() { string ip_address = GetRemoteIpAddress().ToString(); - PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelIpInfo); + PreparedStatement stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_IP_INFO); stmt.AddValue(0, ip_address); stmt.AddValue(1, BitConverter.ToUInt32(GetRemoteIpAddress().Address.GetAddressBytes(), 0)); @@ -489,7 +489,7 @@ namespace Game.Networking var address = GetRemoteIpAddress(); Sha256 digestKeyHash = new(); - digestKeyHash.Process(account.game.SessionKey, account.game.SessionKey.Length); + digestKeyHash.Process(account.game.KeyData, account.game.KeyData.Length); if (account.game.OS == "Wn64") digestKeyHash.Finish(buildInfo.Win64AuthSeed); else if (account.game.OS == "Mc64") @@ -515,7 +515,7 @@ namespace Game.Networking } Sha256 keyData = new(); - keyData.Finish(account.game.SessionKey); + keyData.Finish(account.game.KeyData); HmacSha256 sessionKeyHmac = new(keyData.Digest); sessionKeyHmac.Process(_serverChallenge, 16); @@ -652,7 +652,7 @@ namespace Game.Networking Global.ScriptMgr.OnAccountLogin(account.game.Id); _worldSession = new WorldSession(account.game.Id, authSession.RealmJoinTicket, account.battleNet.Id, this, account.game.Security, (Expansion)account.game.Expansion, - mutetime, account.game.OS, account.battleNet.Locale, account.game.Recruiter, account.game.IsRectuiter); + mutetime, account.game.OS, account.game.TimezoneOffset, account.battleNet.Locale, account.game.Recruiter, account.game.IsRectuiter); // Initialize Warden system only if it is enabled by config //if (wardenActive) @@ -839,15 +839,15 @@ namespace Game.Networking { public AccountInfo(SQLFields fields) { - // 0 1 2 3 4 5 6 7 8 9 10 11 - // SELECT a.id, a.sessionkey, ba.last_ip, ba.locked, ba.lock_country, a.expansion, a.mutetime, ba.locale, a.recruiter, a.os, ba.id, aa.gmLevel, - // 12 13 14 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + // SELECT a.id, a.session_key, ba.last_ip, ba.locked, ba.lock_country, a.expansion, a.mutetime, ba.locale, a.recruiter, a.os, a.timezone_offset, ba.id, aa.SecurityLevel, + // 13 14 15 // bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, r.id - // FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id LEFT JOIN account_access aa ON a.id = aa.id AND aa.RealmID IN (-1, ?) + // FROM account a LEFT JOIN battlenet_accounts ba ON a.battlenet_account = ba.id LEFT JOIN account_access aa ON a.id = aa.AccountID AND aa.RealmID IN (-1, ?) // LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id LEFT JOIN account_banned ab ON a.id = ab.id LEFT JOIN account r ON a.id = r.recruiter - // WHERE a.username = ? ORDER BY aa.RealmID DESC LIMIT 1 + // WHERE a.username = ? AND LENGTH(a.session_key) = 40 ORDER BY aa.RealmID DESC LIMIT 1 game.Id = fields.Read(0); - game.SessionKey = fields.Read(1); + game.KeyData = fields.Read(1); battleNet.LastIP = fields.Read(2); battleNet.IsLockedToIP = fields.Read(3); battleNet.LockCountry = fields.Read(4); @@ -856,11 +856,12 @@ namespace Game.Networking battleNet.Locale = (Locale)fields.Read(7); game.Recruiter = fields.Read(8); game.OS = fields.Read(9); - battleNet.Id = fields.Read(10); - game.Security = (AccountTypes)fields.Read(11); - battleNet.IsBanned = fields.Read(12) != 0; - game.IsBanned = fields.Read(13) != 0; - game.IsRectuiter = fields.Read(14) != 0; + game.TimezoneOffset = TimeSpan.FromMinutes(fields.Read(10)); + battleNet.Id = fields.Read(11); + game.Security = (AccountTypes)fields.Read(12); + battleNet.IsBanned = fields.Read(13) != 0; + game.IsBanned = fields.Read(14) != 0; + game.IsRectuiter = fields.Read(15) != 0; if (battleNet.Locale >= Locale.Total) battleNet.Locale = Locale.enUS; @@ -884,11 +885,12 @@ namespace Game.Networking public struct Game { public uint Id; - public byte[] SessionKey; + public byte[] KeyData; public byte Expansion; public long MuteTime; - public string OS; public uint Recruiter; + public string OS; + public TimeSpan TimezoneOffset; public bool IsRectuiter; public AccountTypes Security; public bool IsBanned; diff --git a/Source/Game/Networking/WorldSocketManager.cs b/Source/Game/Networking/WorldSocketManager.cs index 0c9ba48c9..3c90b121c 100644 --- a/Source/Game/Networking/WorldSocketManager.cs +++ b/Source/Game/Networking/WorldSocketManager.cs @@ -10,7 +10,9 @@ namespace Game.Networking { public class WorldSocketManager : SocketManager { - public override bool StartNetwork(string bindIp, int port, int threadCount) + public static WorldSocketManager Instance { get; } = new WorldSocketManager(); + + public override bool StartNetwork(string bindIp, int port, int threadCount = 1) { _tcpNoDelay = ConfigMgr.GetDefaultValue("Network.TcpNodelay", true); @@ -29,6 +31,7 @@ namespace Game.Networking return false; } + Acceptor.AsyncAcceptSocket(OnSocketAccept); _instanceAcceptor.AsyncAcceptSocket(OnSocketOpen); return true; @@ -62,6 +65,11 @@ namespace Game.Networking base.OnSocketOpen(sock); } + static void OnSocketAccept(Socket sock) + { + Global.WorldSocketMgr.OnSocketOpen(sock); + } + AsyncAcceptor _instanceAcceptor; int _socketSendBufferSize; bool _tcpNoDelay; diff --git a/Source/Game/Server/WorldSession.cs b/Source/Game/Server/WorldSession.cs index 8877f44cc..02a82e380 100644 --- a/Source/Game/Server/WorldSession.cs +++ b/Source/Game/Server/WorldSession.cs @@ -25,7 +25,7 @@ namespace Game { public partial class WorldSession : IDisposable { - public WorldSession(uint id, string name, uint battlenetAccountId, WorldSocket sock, AccountTypes sec, Expansion expansion, long mute_time, string os, Locale locale, uint recruiter, bool isARecruiter) + public WorldSession(uint id, string name, uint battlenetAccountId, WorldSocket sock, AccountTypes sec, Expansion expansion, long mute_time, string os, TimeSpan timezoneOffset, Locale locale, uint recruiter, bool isARecruiter) { m_muteTime = mute_time; AntiDOS = new DosProtection(this); @@ -39,6 +39,7 @@ namespace Game _os = os; m_sessionDbcLocale = Global.WorldMgr.GetAvailableDbcLocale(locale); m_sessionDbLocaleIndex = locale; + _timezoneOffset = timezoneOffset; recruiterId = recruiter; isRecruiter = isARecruiter; expireTime = 60000; // 1 min after socket loss, session is deleted @@ -897,6 +898,8 @@ namespace Game public Locale GetSessionDbcLocale() { return m_sessionDbcLocale; } public Locale GetSessionDbLocaleIndex() { return m_sessionDbLocaleIndex; } + public TimeSpan GetTimezoneOffset() { return _timezoneOffset; } + public uint GetLatency() { return m_latency; } public void SetLatency(uint latency) { m_latency = latency; } public void ResetTimeOutTime(bool onlyActive) @@ -956,6 +959,7 @@ namespace Game bool m_playerSave; Locale m_sessionDbcLocale; Locale m_sessionDbLocaleIndex; + TimeSpan _timezoneOffset; uint m_latency; AccountData[] _accountData = new AccountData[(int)AccountDataTypes.Max]; uint[] tutorials = new uint[SharedConst.MaxAccountTutorialValues]; @@ -1141,7 +1145,7 @@ namespace Game stmt.AddValue(0, battlenetAccountId); SetQuery(AccountInfoQueryLoad.Mounts, stmt); - stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SelBnetCharacterCountsByAccountId); + stmt = LoginDatabase.GetPreparedStatement(LoginStatements.SEL_BNET_CHARACTER_COUNTS_BY_ACCOUNT_ID); stmt.AddValue(0, accountId); SetQuery(AccountInfoQueryLoad.GlobalRealmCharacterCounts, stmt); diff --git a/Source/Game/Services/GameUtilitiesService.cs b/Source/Game/Services/GameUtilitiesService.cs index 9a91305dc..071976110 100644 --- a/Source/Game/Services/GameUtilitiesService.cs +++ b/Source/Game/Services/GameUtilitiesService.cs @@ -3,11 +3,15 @@ using Bgs.Protocol.GameUtilities.V1; using Framework.Constants; +using Framework.IO; using Framework.Web; -using Framework.Serialization; +using Framework.Web.Rest.Realmlist; using Game.Services; using Google.Protobuf; +using System; using System.Collections.Generic; +using System.Text; +using System.Text.Json; namespace Game { @@ -90,7 +94,11 @@ namespace Game countEntry.Count = characterCount.Value; realmCharacterCounts.Counts.Add(countEntry); } - compressed = Json.Deflate("JSONRealmCharacterCountList", realmCharacterCounts); + + var jsonData = Encoding.UTF8.GetBytes("JSONRealmCharacterCountList:" + JsonSerializer.Serialize(realmCharacterCounts) + "\0"); + var compressedData = ZLib.Compress(jsonData); + + compressed = BitConverter.GetBytes(jsonData.Length).Combine(compressedData); attribute = new Bgs.Protocol.Attribute(); attribute.Name = "Param_CharacterCountList"; @@ -105,9 +113,9 @@ namespace Game var realmAddress = Params.LookupByKey("Param_RealmAddress"); if (realmAddress != null) return Global.RealmMgr.JoinRealm((uint)realmAddress.UintValue, Global.WorldMgr.GetRealm().Build, System.Net.IPAddress.Parse(GetRemoteAddress()), GetRealmListSecret(), - GetSessionDbcLocale(), GetOS(), GetAccountName(), response); + GetSessionDbcLocale(), GetOS(), GetTimezoneOffset(), GetAccountName(), response); return BattlenetRpcErrorCode.Ok; } } -} +} \ No newline at end of file diff --git a/Source/WorldServer/Server.cs b/Source/WorldServer/Server.cs index 681504a41..d4e92a0b4 100644 --- a/Source/WorldServer/Server.cs +++ b/Source/WorldServer/Server.cs @@ -67,8 +67,7 @@ namespace WorldServer return; } - var WorldSocketMgr = new WorldSocketManager(); - if (!WorldSocketMgr.StartNetwork(worldListener, worldPort, networkThreads)) + if (!Global.WorldSocketMgr.StartNetwork(worldListener, worldPort, networkThreads)) { Log.outError(LogFilter.Network, "Failed to start Realm Network"); ExitNow(); @@ -104,7 +103,7 @@ namespace WorldServer // unload Battlegroundtemplates before different singletons destroyed Global.BattlegroundMgr.DeleteAllBattlegrounds(); - WorldSocketMgr.StopNetwork(); + Global.WorldSocketMgr.StopNetwork(); Global.MapMgr.UnloadAll(); // unload all grids (including locked in memory) Global.TerrainMgr.UnloadAll();