/// <summary> /// Callback for a successful login /// </summary> /// <param name="sender">The GpcmClient that is logged in</param> private void GpcmClient_OnSuccessfulLogin(object sender) { // Wrap this in a try/catch try { GpcmClient oldC; GpcmClient client = sender as GpcmClient; // Remove connection from processing Processing.TryRemove(client.ConnectionId, out oldC); // Check to see if the client is already logged in, if so disconnect the old user if (Clients.TryRemove(client.PlayerId, out oldC)) { oldC.Disconnect(DisconnectReason.NewLoginDetected); ServerManager.Log("Login Clash: {0} - {1} - {2}", client.PlayerNick, client.PlayerId, client.RemoteEndPoint); } // Add current client to the dictionary if (!Clients.TryAdd(client.PlayerId, client)) { Program.ErrorLog.Write("ERROR: [GpcmServer._OnSuccessfulLogin] Unable to add client to HashSet."); } // Add player to database queue var status = new PlayerStatusUpdate(client, LoginStatus.Completed); PlayerStatusQueue.Enqueue(status); } catch (Exception E) { Program.ErrorLog.Write("ERROR: [GpcmServer._OnSuccessfulLogin] Exception was thrown, Generating exception log."); ExceptionHandler.GenerateExceptionLog(E); } }
/// <summary> /// Logs the client out of the game client, and closes the stream /// </summary> /// <param name="reason"> /// The disconnect reason code. /// </param> /// <remarks> /// If set the <paramref name="reason"/> is set to <see cref="DisconnectReason.ForcedServerShutdown"/>, /// the OnDisconect event will not be called, the database will not be updated to reset everyone's session code, /// and the EventArgs objects will NOT be returned to the IO pool. You should only set to /// <see cref="DisconnectReason.ForcedServerShutdown"/> for a planned server shutdown. /// </remarks> public void Disconnect(DisconnectReason reason) { // Update database session if (Status == LoginStatus.Completed && reason != DisconnectReason.ForcedServerShutdown) { try { using (GamespyDatabase Database = new GamespyDatabase()) Database.Execute("UPDATE player SET online=0 WHERE id=" + PlayerId); } catch { } } // If connection is still alive, disconnect user try { Stream.OnDisconnect -= Stream_OnDisconnect; Stream.DataReceived -= Stream_DataReceived; Stream.Close(reason == DisconnectReason.ForcedServerShutdown); } catch { } // Set status and log if (Status == LoginStatus.Completed) { if (reason == DisconnectReason.NormalLogout) { ServerManager.Log( "Client Logout: {0} - {1} - {2}", PlayerNick, PlayerId, RemoteEndPoint ); } else { ServerManager.Log( "Client Disconnected: {0} - {1} - {2}, Code={3}", PlayerNick, PlayerId, RemoteEndPoint, Enum.GetName(typeof(DisconnectReason), reason) ); } } // Preapare to be unloaded from memory Status = LoginStatus.Disconnected; Dispose(); // Call disconnect event OnDisconnect?.Invoke(this); }
/// <summary> /// Logs the client out of the game client, and closes the stream /// </summary> /// <param name="where"> /// The disconnect code. If set to 9, the OnDisconect event will not be called, the database /// will not be updated to reset everyone's session code, and the EventArgs objects will NOT /// be returned to the IO pool. You should only set to 9 for a planned server shutdown. /// </param> /// <remarks> /// Codes: /// 0 => Client sends the "logout" command /// 1 => Keep Alive Packet failed to send (may not work with new async socket code) /// 2 => Invalid login query, or username was incorrect /// 3 => Incorrect Password /// 4 => An error occured while trying to login the client (could be database related) /// 5 => Cant create account, username exists already /// 6 => Error Creating new account in database /// 7 => Invalid query for account creation, or an exception was thrown while trying to create account /// 8 => Stream Disconnected /// 9 => Forced server shutdown [No events called, database sessions are not updated, and EventArgs are disposed] /// 10 => Client challenge already sent /// </remarks> public void Disconnect(int where) { // Update database session if (Status == LoginStatus.Completed && where < 9) { try { using (GamespyDatabase Database = new GamespyDatabase()) Database.Execute("UPDATE web_users SET game_session=0 WHERE pid=" + PlayerId); } catch { } } // If connection is still alive, disconnect user try { Stream.OnDisconnect -= Stream_OnDisconnect; Stream.DataReceived -= Stream_DataReceived; Stream.Close(where == 9); } catch { } // Set status and log if (Status == LoginStatus.Completed) { ServerManager.Log("Client Logout: {0} - {1} - {2}, Code={3}", PlayerNick, PlayerId, RemoteEndPoint, where); } // Preapare to be unloaded from memory Status = LoginStatus.Disconnected; Dispose(); // Call disconnect event if (OnDisconnect != null) { OnDisconnect(this); } }
/// <summary> /// This method verifies the login information sent by /// the client, and returns encrypted data for the client /// to verify as well /// </summary> public void ProcessLogin(Dictionary <string, string> Recv) { // Make sure we have all the required data to process this login if (!Recv.ContainsKey("uniquenick") || !Recv.ContainsKey("challenge") || !Recv.ContainsKey("response")) { Stream.SendAsync(@"\error\\err\0\fatal\\errmsg\Invalid Query!\id\1\final\"); Disconnect(DisconnectReason.InvalidLoginQuery); return; } // Dispose connection after use try { using (GamespyDatabase Conn = new GamespyDatabase()) { // Try and fetch the user from the database Dictionary <string, object> User = Conn.GetUser(Recv["uniquenick"]); if (User == null) { Stream.SendAsync(@"\error\\err\265\fatal\\errmsg\The uniquenick provided is incorrect!\id\1\final\"); Disconnect(DisconnectReason.InvalidUsername); return; } // Check if user is banned bool banned = Int32.Parse(User["permban"].ToString()) > 0; if (banned) { Stream.SendAsync(@"\error\\err\265\fatal\\errmsg\You account has been permanently suspended.\id\1\final\"); Disconnect(DisconnectReason.PlayerIsBanned); return; } // Set player variables PlayerId = Int32.Parse(User["id"].ToString()); PlayerNick = Recv["uniquenick"]; PlayerEmail = User["email"].ToString(); PlayerCountryCode = User["country"].ToString(); PasswordHash = User["password"].ToString().ToLowerInvariant(); // Use the GenerateProof method to compare with the "response" value. This validates the given password if (Recv["response"] == GenerateProof(Recv["challenge"], ServerChallengeKey)) { // Create session key SessionKey = Crc.ComputeChecksum(PlayerNick); // Password is correct Stream.SendAsync( @"\lc\2\sesskey\{0}\proof\{1}\userid\{2}\profileid\{2}\uniquenick\{3}\lt\{4}__\id\1\final\", SessionKey, GenerateProof(ServerChallengeKey, Recv["challenge"]), // Do this again, Params are reversed! PlayerId, PlayerNick, GenerateRandomString(22) // Generate LT whatever that is (some sort of random string, 22 chars long) ); // Log Incoming Connections ServerManager.Log("Client Login: {0} - {1} - {2}", PlayerNick, PlayerId, RemoteEndPoint); // Update status last, and call success login Status = LoginStatus.Completed; CompletedLoginProcess = true; OnSuccessfulLogin?.Invoke(this); } else { // Log Incoming Connections ServerManager.Log("Failed Login Attempt: {0} - {1} - {2}", PlayerNick, PlayerId, RemoteEndPoint); // Password is incorrect with database value Stream.SendAsync(@"\error\\err\260\fatal\\errmsg\The password provided is incorrect.\id\1\final\"); Disconnect(DisconnectReason.InvalidPassword); } } } catch (Exception ex) { ExceptionHandler.GenerateExceptionLog(ex); Disconnect(DisconnectReason.GeneralError); return; } }
/// <summary> /// This method verifies the login information sent by /// the client, and returns encrypted data for the client /// to verify as well /// </summary> public void ProcessLogin(Dictionary <string, string> Recv) { // Make sure we have all the required data to process this login if (!Recv.ContainsKey("uniquenick") || !Recv.ContainsKey("challenge") || !Recv.ContainsKey("response")) { Stream.SendAsync(@"\error\\err\0\fatal\\errmsg\Invalid Query!\id\1\final\"); Disconnect(2); return; } // Dispose connection after use try { using (GamespyDatabase Conn = new GamespyDatabase()) { // Try and fetch the user from the database Dictionary <string, object> User = Conn.GetUser(Recv["uniquenick"]); if (User == null) { Stream.SendAsync(@"\error\\err\265\fatal\\errmsg\The uniquenick provided is incorrect!\id\1\final\"); Disconnect(2); return; } // Set player variables PlayerId = Int32.Parse(User["pid"].ToString()); PlayerNick = Recv["uniquenick"]; PlayerEmail = User["email"].ToString(); PlayerCountryCode = User["game_country"].ToString(); PasswordHash = User["password"].ToString().ToLowerInvariant(); // Use the GenerateProof method to compare with the "response" value. This validates the given password if (Recv["response"] == GenerateProof(Recv["challenge"], ServerChallengeKey)) { // Create session key SessionKey = Crc.ComputeChecksum(PlayerNick); // Password is correct Stream.SendAsync( @"\lc\2\sesskey\{0}\proof\{1}\userid\{2}\profileid\{2}\uniquenick\{3}\lt\{4}__\id\1\final\", SessionKey, GenerateProof(ServerChallengeKey, Recv["challenge"]), // Do this again, Params are reversed! PlayerId, PlayerNick, GenerateRandomString(22) // Generate LT whatever that is (some sort of random string, 22 chars long) ); // Log Incoming Connections ServerManager.Log("Client Login: {0} - {1} - {2}", PlayerNick, PlayerId, RemoteEndPoint); Conn.Execute( "UPDATE web_users SET last_game_ip=@P0, game_session=1, game_tstamp=@P1 WHERE pid=@P2", RemoteEndPoint.Address, DateTime.UtcNow.ToUnixTimestamp(), PlayerId ); // Update status last, and call success login Status = LoginStatus.Completed; if (OnSuccessfulLogin != null) { OnSuccessfulLogin(this); } } else { // Log Incoming Connections ServerManager.Log("Failed Login Attempt: {0} - {1} - {2}", PlayerNick, PlayerId, RemoteEndPoint); // Password is incorrect with database value Stream.SendAsync(@"\error\\err\260\fatal\\errmsg\The password provided is incorrect.\id\1\final\"); Disconnect(3); } } } catch (Exception ex) { ExceptionHandler.GenerateExceptionLog(ex); Disconnect(4); return; } }
/// <summary> /// Creates a new instance of <see cref="GpcmServer"/> /// </summary> /// <param name="bindTo"></param> public GpcmServer(IPEndPoint bindTo) : base(bindTo, MaxConnections) { // Register for events GpcmClient.OnSuccessfulLogin += GpcmClient_OnSuccessfulLogin; GpcmClient.OnDisconnect += GpcmClient_OnDisconnect; // Setup timer. Every 15 seconds should be sufficient if (PollTimer == null || !PollTimer.Enabled) { PollTimer = new System.Timers.Timer(15000); PollTimer.Elapsed += (s, e) => { // Send keep alive to all connected clients if (Clients.Count > 0) { Parallel.ForEach(Clients.Values, client => client.SendKeepAlive()); } // Disconnect hanging connections if (Processing.Count > 0) { Parallel.ForEach(Processing.Values, client => CheckTimeout(client)); } }; PollTimer.Start(); } // Setup timer. Every 5 seconds should be sufficient if (StatusTimer == null || !StatusTimer.Enabled) { StatusTimer = new System.Timers.Timer(5000); StatusTimer.Elapsed += (s, e) => { // Return if we are empty if (PlayerStatusQueue.IsEmpty) { return; } // Open database connection using (Database.GamespyDatabase db = new Database.GamespyDatabase()) using (var transaction = db.BeginTransaction()) { try { var timestamp = DateTime.UtcNow.ToUnixTimestamp(); PlayerStatusUpdate result; while (PlayerStatusQueue.TryDequeue(out result)) { // Skip if this player never finished logging in if (!result.Client.CompletedLoginProcess) { continue; } // Only update record under these two status' if (result.Status != LoginStatus.Completed && result.Status != LoginStatus.Disconnected) { continue; } // Update player record db.Execute( "UPDATE player SET online=@P0, lastip=@P1, lastonline=@P2 WHERE id=@P3", (result.Status == LoginStatus.Disconnected) ? 0 : 1, result.Client.RemoteEndPoint.Address, timestamp, result.Client.PlayerId ); } transaction.Commit(); } catch (Exception ex) { ServerManager.Log("[Gpcm..ctor] StatusTimer: " + ex.Message); transaction.Rollback(); } } }; StatusTimer.Start(); } // Set connection handling base.ConnectionEnforceMode = EnforceMode.DuringPrepare; base.FullErrorMessage = Config.GetValue("Settings", "LoginServerFullMessage").Replace("\"", ""); // Begin accepting connections base.StartAcceptAsync(); }