/// <summary> /// Обрабатываем приватное сообщение чата. Либо личное, либо в чат канал /// </summary> void HandlePrivmsgCommand(TcpPortHandler handler, string[] values) { // PRIVMSG #GPG!1 :dfg var channelName = values[1]; // Определяем, отправлено ли сообщение в глобальный чат if (channelName == "#GPG!1") { _emulationAdapter.SendChatMessage(values[2]); } else { if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) { //var roomHash = channelName.Split('!')[2]; // Костыль для работы личных сообщений в игре. Удаляем префикс if (values[1].EndsWith("-thq")) { values[1] = values[1].Substring(0, values[1].Length - 4); } _emulationAdapter.SendLobbyBroadcast(string.Join(" ", values)); } } }
/// <summary> /// Обрабатывает новое сообщение, полученное по TCP чата /// </summary> unsafe void OnChatReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { // Дешифруем строку, если чат шифрованный if (_chatEncoded) { byte *bytesPtr = stackalloc byte[count]; for (int i = 0; i < count; i++) { bytesPtr[i] = buffer[i]; } ChatCrypt.GSEncodeDecode(_chatClientKey, bytesPtr, count); for (int i = 0; i < count; i++) { buffer[i] = bytesPtr[i]; } } var str = buffer.ToUtf8(count); LogTrace(">>>>> " + str); // var lines = str.Split(_chatSplitChars, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < lines.Length; i++) { HandleChatLine(handler, node, lines[i]); } }
/// <summary> /// Обратываем выход пользователя из комнаты чата /// </summary> void HandlePartCommand(TcpPortHandler handler, string[] values) { //CHATLINE PART #GSP!whamdowfr!Ml39ll1K9M : var channelName = values[1]; // Определяем, глобальный чат или комната автоматча if (channelName == "#GPG!1") { // Main chat - ignore } else { _enteredLobbyHash = null; _localServerHash = null; _emulationAdapter.LeaveFromCurrentLobby(); } if (!_emulationAdapter.HasLocalUserActiveInGameProfile) { return; } LogForUser($"Part sended {channelName}"); SendToClientChat($":{_user} PART {channelName} :Leaving\r\n"); }
/// <summary> /// Обрабатывает команду логина в чате. В этот момент пользователь уже авторизован, просто сообщаем ему его ID профиля. /// </summary> void HandleLoginCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { var nick = values[2]; var id = _emulationAdapter.GetUserInGameProfileId(nick); SendToClientChat(node, $":s 707 {nick} 12345678 {id}\r\n"); SendToClientChat(node, $":s 687ru: Your languages have been set\r\n"); }
/// <summary> /// Обрабатывает входящее сообщение сервера LOGIN (Client) /// </summary> void OnClientManagerReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { // Несколько сообщения может быть за раз - сплитим var messages = buffer.ToUtf8(count).Split(new string[] { @"\final\" }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < messages.Length; i++) { HandleClientManagerMessage(handler, node, messages[i]); } }
/// <summary> /// Обрабатывает сохранение пары ключ-значение глобально в указанной комнате чата. /// Все другие пользователи должны после этого получить сообщение Broadcast. /// </summary> void HandleSetckeyCommand(TcpPortHandler handler, TcpClientNode node, string line, string[] values) { var channelName = values[1]; if (channelName == "#GPG!1") { var keyValues = values[3]; var pairs = keyValues.Split(':', '\\'); if (pairs[1] == "username") { SendToClientChat(node, $":s 702 #GPG!1 #GPG!1 {values[2]} BCAST :\\{pairs[1]}\\{pairs[2]}\r\n"); return; } SendToClientChat(node, $":s 702 #GPG!1 #GPG!1 {values[2]} BCAST :\\{pairs[1]}\\{pairs[2]}\r\n"); var dictionary = new Dictionary <string, string>(); for (int i = 1; i < pairs.Length; i += 2) { dictionary[pairs[i]] = pairs[i + 1]; } _emulationAdapter.SetGlobalKeyValues(dictionary); /* for (int i = 1; i < pairs.Length; i += 2) * SendToClientChat($":s 702 #GPG!1 #GPG!1 {values[2]} BCAST :\\{pairs[i]}\\{pairs[i + 1]}\r\n");*/ } else { if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) { //var roomHash = channelName.Split('!')[2]; var keyValues = values[3]; var pairs = keyValues.Split(':', '\\'); // Skip first empty entry for (int i = 1; i < pairs.Length; i += 2) { _emulationAdapter.SetLobbyKeyValue(pairs[i], pairs[i + 1]); } _emulationAdapter.SendLobbyBroadcast(line); HandleRemoteSetckeyCommand(values); } } }
public GameSpyServer(IEmulationAdapter handler) { _emulationAdapter = handler; handler.GameSpyServer = this; _serverReport = new UdpPortHandler(27900, OnServerReportReceived, OnError); _serverRetrieve = new TcpPortHandler(28910, new RetrieveTcpSetting(), OnServerRetrieveReceived, OnServerRetrieveError); _clientManager = new TcpPortHandler(29900, new LoginTcpSetting(), OnClientManagerReceived, OnError, OnClientAccept, KillNode); _searchManager = new TcpPortHandler(29901, new LoginTcpSetting(), OnSearchManagerReceived, OnError, null, KillNode); _chat = new TcpPortHandler(6667, new ChatTcpSetting(), OnChatReceived, OnError, OnChatAccept, RestartServices); _stats = new TcpPortHandler(29920, new StatsTcpSetting(), OnStatsReceived, OnError, OnStatsAccept, RestartServices); _http = new TcpPortHandler(80, new HttpTcpSetting(), OnHttpReceived, OnError, null, KillNode); LogTrace("Services inited"); }
/// <summary> /// Обратывает команду установки ника. Наследие IRC. Просто отвечаем по стандарту. /// </summary> void HandleNickCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { var users = _emulationAdapter.ActivePlayersCount; SendToClientChat(node, $":s 001 {_name} :Welcome to the Matrix {_name}\r\n"); //SendToClientChat(node, $":s 002 {_name} :Your host is xs0, running version 1.0\r\n"); // SendToClientChat(node, $":s 003 {_name} :This server was created Fri Oct 19 1979 at 21:50:00 PDT\r\n"); //SendToClientChat(node, $":s 004 {_name} s 1.0 iq biklmnopqustvhe\r\n"); // SendToClientChat(node, $":s 375 {_name} :- (M) Message of the day - \r\n"); // SendToClientChat(node, $":s 372 {_name} :- Welcome to GameSpy\r\n"); //SendToClientChat(node, $":s 251 :There are {users} users and 0 services on 1 servers\r\n"); // SendToClientChat(node, $":s 252 0 :operator(s)online\r\n"); // SendToClientChat(node, $":s 253 1 :unknown connection(s)\r\n"); // SendToClientChat(node, $":s 254 1 :channels formed\r\n"); // SendToClientChat(node, $":s 255 :I have {users} clients and 1 servers\r\n"); SendToClientChat(node, $":{_user} NICK {_name}\r\n"); }
/// <summary> /// Обратывает установку заголовка комнаты. /// Для лобби это всегда имя хоста. /// </summary> void HandleTopicCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { // :Bambochuk2!Xu4FpqOa9X|[email protected] TOPIC #GSP!whamdowfr!76561198408785287 :Bambochuk2 _emulationAdapter.SetLobbyTopic(values[2]); SendToClientChat(node, $":{_user} TOPIC #GSP!{_gameGSkey}!{_enteredLobbyHash} :{values[2]}\r\n"); //TOPIC #GSP!whamdowfr!Ml39ll1K9M :elamaunt /*var channelName = values[1]; * * if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) * { * var roomHash = channelName.Split('!')[2]; * * if (roomHash == _localServerHash) * { * SteamLobbyManager.SetLobbyTopic(values[2]); * } * }*/ }
/// <summary> /// Сообщаем игре ее IP. Как правило, этот ответ ни на что не влияет. По крайней мере, я не обнаружил какой-либо зависимости. /// Особенность библиотеки GameSpy. /// </summary> void HandleUsripCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { SendToClientChat(node, $":s 302 :=+@{node.RemoteEndPoint?.Address}\r\n"); }
/// <summary> /// Обратываем команду редимов комнаты чата /// </summary> void HandleModeCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { var channelName = values[1]; // Определяем, главный ли чат if (channelName.StartsWith("#GPG", StringComparison.OrdinalIgnoreCase)) { // Просто захардкоженный ответ под Soulstorm, типа успех SendToClientChat(node, $":s 324 {_name} {channelName} +\r\n"); } else { // В авот с автоматчем поинтереснее if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) { // Уникальный хэш комнаты var roomHash = channelName.Split('!')[2]; // Извлекаем хоста по хэшу. Если не удастся, значит это локальный хост. if (_lastLoadedLobbies.TryGetValue(roomHash, out GameServerDetails details)) { // Отправляем ограничение на количество юзеров в комнате var maxPLayers = _emulationAdapter.GetCurrentLobbyMaxPlayers(); if (maxPLayers == 2 || maxPLayers == 4 || maxPLayers == 6 || maxPLayers == 8) { SendToClientChat(node, $":s 324 {_name} {channelName} +l {maxPLayers}\r\n"); } else { SendToClientChat(node, $":s 324 {_name} {channelName} +\r\n"); } } else { // На всякий случай проверяем на соответствие локальный хэш if (roomHash == _localServerHash) { // Обрабатываем установку ограничения на количество юзеров в комнате if (values.Length < 4) { // Это был запрос. Отвечаем var max = _emulationAdapter.GetCurrentLobbyMaxPlayers(); if (max > 0 && max < 9) { SendToClientChat(node, $":s 324 {_name} {channelName} +l {max}\r\n"); } else { SendToClientChat(node, $":s 324 {_name} {channelName} +\r\n"); } } else { // Это была установка. Задаем ограничение и отправляем успех var maxPlayers = values[3]; if (int.TryParse(maxPlayers, out int value)) { _emulationAdapter.SetLocalLobbyMaxPlayers(value); } SendToClientChat(node, $":{_user} MODE #GSP!whamdowfr!{_enteredLobbyHash} +l {maxPlayers}\r\n"); } // CHATLINE MODE #GSP!whamdowfr!Ml39ll1K9M +l 2 // CHATLINE MODE #GSP!whamdowfr!Ml39ll1K9M -i-p-s-m-n-t+l+e 2 } } } } }
/// <summary> /// Обрабатывает команду перехода в шифрованный чат /// </summary> unsafe void HandleCryptCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { _chatEncoded = true; _gameGSkeyBytes = null; // В зависимости от игры. Байты шифрования отличаются. Таблица есть в инете // https://gamerecon.net/support/topic/gamespy-supported-games-list/ // https://github.com/luisj135/nintendo_dwc_emulator/blob/master/gslist.cfg // Dawn of War if (values.Contains("dow")) { _gameGSkey = "dow"; _gameGSNameBytes = "dow".ToAsciiBytes(); _gameGSkeyBytes = "VLxgwe".ToAsciiBytes(); } // Dawn of War (более похоже на правду) if (values.Contains("whammer40000")) { _gameGSkey = "whammer40000"; _gameGSNameBytes = "whammer40000".ToAsciiBytes(); _gameGSkeyBytes = "uJ8d3N".ToAsciiBytes(); } // Winter Assault // ключа почему-то нет в таблице. Возьмем ключ 1 дова. // TODO: Надо потестить WA if (values.Contains("dowwad")) { _gameGSkey = "dowwad"; _gameGSNameBytes = "dowwad".ToAsciiBytes(); _gameGSkeyBytes = "uJ8d3N".ToAsciiBytes(); } // Dark Crusade if (values.Contains("whammer40kdc")) { _gameGSkey = "whammer40kdc"; _gameGSNameBytes = "whammer40kdc".ToAsciiBytes(); _gameGSkeyBytes = "Ue9v3H".ToAsciiBytes(); } // Soulstorm if (values.Contains("whamdowfr")) { _gameGSkey = "whamdowfr"; _gameGSNameBytes = "whamdowfr".ToAsciiBytes(); _gameGSkeyBytes = "pXL838".ToAsciiBytes(); } if (_gameGSkeyBytes == null) { Restart(); return; } // Ключ шифрования. Для простоты просто нули. Не тестил, что будет, если не нули. var chall = "0000000000000000".ToAsciiBytes(); var clientKey = new ChatCrypt.GDCryptKey(); var serverKey = new ChatCrypt.GDCryptKey(); // Инициализируем структуры-ключи для выполнения алгоритма спая fixed(byte *challPtr = chall) { fixed(byte *gamekeyPtr = _gameGSkeyBytes) { ChatCrypt.GSCryptKeyInit(clientKey, challPtr, gamekeyPtr, _gameGSkeyBytes.Length); ChatCrypt.GSCryptKeyInit(serverKey, challPtr, gamekeyPtr, _gameGSkeyBytes.Length); } } // Сохраняем структуры _chatClientKey = clientKey; _chatServerKey = serverKey; // Отправляем идентичные ключи для сервера и клиента. Шифрование в итоге будет полностью совпадать. // С этого момента чат шифрованный handler.SendAskii(node, ":s 705 * 0000000000000000 0000000000000000\r\n"); }
void HandleQuitCommand(TcpPortHandler handler, string[] values) { // Выход из чата не значит выход с сервера. Выход из чата обычно происходит при старте игры. // После игры игрок снова войдет в чат, но для других пользователей он всегда остается в чате //Restart(); }
void RestartServices(TcpPortHandler handler, TcpClientNode node) { // Получение 0 байт говорит о том, что клиент хочет прекратить взаимодействие. // Сбрасываем все соединения Restart(); }
/// <summary> /// Обратаываем UTM сообщение а чате. Уникальная команда GameSpy. Передаем ее всем участникам чата через Broadcast /// </summary> void HandleUtmCommand(TcpPortHandler handler, string line) { _emulationAdapter.SendLobbyBroadcast(line); }
void OnServerRetrieveReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { var str = buffer.ToASCII(count); LogTrace("RETRIEVE " + str); var endPoint = node.RemoteEndPoint; if (endPoint == null) { handler.KillClient(node); return; } string[] data = str.Split(new char[] { '\x00' }, StringSplitOptions.RemoveEmptyEntries); string validate = data[4]; string filter = null; bool isAutomatch = false; if (validate.Length > 8) { filter = validate.Substring(8); validate = validate.Substring(0, 8); } else { //Log(Category, "ROOMS REQUEST - "+ data[2]); isAutomatch = data[2].EndsWith("am"); if (!isAutomatch) { SendChatRooms(handler, node, validate); return; } } var lobbies = _emulationAdapter.GetOpenedLobbies(); try { // var currentRating = ServerContext.ChatServer.CurrentRating; /*for (int i = 0; i < lobbies.Length; i++) * { * var server = lobbies[i]; * * //server["score_"] = GetCurrentRating(server.MaxPlayers); * }*/ var fields = data[5].Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); var unencryptedBytes = ParseHelper.PackServerList(endPoint, lobbies, fields, isAutomatch); _lastLoadedLobbies.Clear(); for (int i = 0; i < lobbies.Length; i++) { var server = lobbies[i]; var address = server.HostIP ?? server.LocalIP; var port = ushort.Parse(server.HostPort ?? server.LocalPort); var channelHash = ChatCrypt.PiStagingRoomHash(address, address, port); Log($"HASHFOR {address}:{port} {channelHash}"); server.RoomHash = channelHash; _lastLoadedLobbies[channelHash] = server; } Log("SERVERS VALIDATE VALUE ~" + validate + "~"); var encryptedBytes = GSEncoding.Encode(_gameGSkeyBytes, validate.ToAsciiBytes(), unencryptedBytes, unencryptedBytes.LongLength); Log("SERVERS bytes " + encryptedBytes.Length); int autoGames = 0; int customGames = 0; // TODO вынести в отдельно for (int i = 0; i < lobbies.Length; i++) { var server = lobbies[i]; if (server.Ranked) { autoGames++; } else { customGames++; } } //CoreContext.ClientServer.SendAsServerMessage( // "Received game list: " + customGames + " - custom; " + autoGames + // " - auto; Mod: "+ CoreContext.ThunderHawkModManager.CurrentModName); handler.Send(node, encryptedBytes); } finally { handler.KillClient(node); } }
/// <summary> /// Обрабатывает получение нового TPC соединения для получения статистики игроков /// </summary> void OnStatsAccept(TcpPortHandler handler, TcpClientNode node, CancellationToken token) { // Отправляем challenge сервера шифрованный через Xor. Надо отправить, но особо ни на что не влияет // Все сообщения также надо будет прогонять через Xor. handler.Send(GSUtils.XorBytes(@"\lc\1\challenge\KNDVKXFQWP\id\1\final\", "GameSpy3D", 7)); }
/// <summary> /// Обрабатывает получение нового подключения по TCP порту IRC чата /// </summary> void OnChatAccept(TcpPortHandler handler, TcpClientNode node, CancellationToken token) { _inChat = false; _chatEncoded = false; }
/// <summary> /// Отправляем список публичных комнат в чате. /// Сейчас фейкает только один чат рум GPG1 с названием "Room 1" /// Игра сама подставит красивое имя из HTTP данных о списке комнат /// </summary> /// <param name="validate">Строка валидации запроса. Должна быть получена от игры</param> void SendChatRooms(TcpPortHandler handler, TcpClientNode node, string validate) { var bytes = new List <byte>(); //var remoteEndPoint = handler.RemoteEndPoint; //bytes.AddRange(remoteEndPoint.Address.GetAddressBytes()); bytes.AddRange(IPAddress.Loopback.GetAddressBytes()); byte[] value2 = BitConverter.GetBytes((ushort)6500); bytes.AddRange(BitConverter.IsLittleEndian ? value2.Reverse() : value2); bytes.Add(5); // fields count bytes.Add(0); // Забивает поля, которые нужны игре. В этом же порядке надо будет пихнуть значения дальше для каждой комнаты bytes.AddRange("hostname".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numwaiting".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("maxwaiting".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numservers".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numplayersname".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); // Изначально было 10 комнат в игре, но мы сделаем только одну и весь код написан для синхронизации чата в игре с чатом из лаунчера // for (int i = 1; i <= 10; i++) // { // Странный байт в начале инфы о комнате bytes.Add(81); // инфа об IP комнаты, но игре на нее пофиг var b2 = BitConverter.GetBytes((long)1); bytes.Add(b2[3]); bytes.Add(b2[2]); bytes.Add(b2[1]); bytes.Add(b2[0]); // инфа о порте комнаты, но игре на нее пофиг bytes.Add(0); bytes.Add(0); // Скрытое название комнаты. Только такой формат принимает с цифрой в конце bytes.Add(255); bytes.AddRange("Room 1".ToAsciiBytes()); bytes.Add(0); // Количество игроков в комнате bytes.Add(255); bytes.AddRange(_emulationAdapter.ActivePlayersCount.ToString().ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("1000".ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("1".ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("20".ToAsciiBytes()); bytes.Add(0); // } // Непонятный набор байт в конце, но без него не сработает bytes.AddRange(new byte[] { 0, 255, 255, 255, 255 }); var array = bytes.ToArray(); // Шифруем алгоритмом спая. Участвует строка валидации и уникальный ключ игры byte[] enc = GSEncoding.Encode(_gameGSkeyBytes, validate.ToAsciiBytes(), array, array.LongLength); handler.Send(node, enc); handler.KillClient(node); }
/// <summary> /// Обратывает строку IRC чата и вызывает соответствующий метод обработчик команды /// </summary> void HandleChatLine(TcpPortHandler handler, TcpClientNode node, string line) { var values = GetIrcChatLineValues(line); if (line.StartsWith("LOGIN", StringComparison.OrdinalIgnoreCase)) { HandleLoginCommand(handler, node, values); return; } if (line.StartsWith("USRIP", StringComparison.OrdinalIgnoreCase)) { HandleUsripCommand(handler, node, values); return; } if (line.StartsWith("CRYPT", StringComparison.OrdinalIgnoreCase)) { HandleCryptCommand(handler, node, values); return; } if (line.StartsWith("USER", StringComparison.OrdinalIgnoreCase)) { HandleUserCommand(handler, node, values); return; } if (line.StartsWith("NICK", StringComparison.OrdinalIgnoreCase)) { HandleNickCommand(handler, node, values); return; } if (line.StartsWith("CDKEY", StringComparison.OrdinalIgnoreCase)) { HandleCdkeyCommand(handler, node, values); return; } if (line.StartsWith("JOIN", StringComparison.OrdinalIgnoreCase)) { HandleJoinCommand(handler, node, line, values); return; } if (line.StartsWith("MODE", StringComparison.OrdinalIgnoreCase)) { HandleModeCommand(handler, node, values); return; } if (line.StartsWith("QUIT", StringComparison.OrdinalIgnoreCase)) { HandleQuitCommand(handler, values); return; } if (line.StartsWith("PRIVMSG", StringComparison.OrdinalIgnoreCase)) { HandlePrivmsgCommand(handler, values); return; } if (line.StartsWith("SETCKEY", StringComparison.OrdinalIgnoreCase)) { HandleSetckeyCommand(handler, node, line, values); return; } if (line.StartsWith("GETCKEY", StringComparison.OrdinalIgnoreCase)) { HandleGetckeyCommand(handler, node, values); return; } if (line.StartsWith("TOPIC", StringComparison.OrdinalIgnoreCase)) { HandleTopicCommand(handler, node, values); return; } if (line.StartsWith("PART", StringComparison.OrdinalIgnoreCase)) { HandlePartCommand(handler, values); return; } if (line.StartsWith("UTM", StringComparison.OrdinalIgnoreCase)) { HandleUtmCommand(handler, line); return; } if (line.StartsWith("PING", StringComparison.OrdinalIgnoreCase)) { HandlePingCommand(handler, node, values); return; } Debugger.Break(); }
/// <summary> /// Обрабатывает одно сообщение сервера /// </summary> private void HandleClientManagerMessage(TcpPortHandler handler, TcpClientNode node, string mes) { LogTrace("CLIENT " + mes); var pairs = ParseHelper.ParseMessage(mes, out string query); if (pairs == null || string.IsNullOrWhiteSpace(query)) { return; } // Исправление бага, когда игра по какой-то причине соединяет логин и почту в одну строку. Разбиваем, иначе не будет работать алгоритм хэширования при логине if (pairs.ContainsKey("name") && !pairs.ContainsKey("email")) { var parts = pairs["name"].Split('@'); if (parts.Length > 2) { pairs["name"] = parts[0]; pairs["email"] = parts[1] + "@" + parts[2]; } } switch (query) { case "login": HandleLogin(node, pairs); RestartUserSessionTimer(node); break; case "logout": _emulationAdapter.LeaveFromCurrentLobby(); _emulationAdapter.OnLogout(); break; case "registernick": handler.SendAskii(node, string.Format(@"\rn\{0}\id\{1}\final\", pairs["uniquenick"], pairs["id"])); break; case "ka": handler.SendAskii(node, $@"\ka\\final\"); break; case "status": HandleStatus(node, pairs); break; case "newuser": { var nick = pairs["nick"]; var email = pairs["email"]; var password = GSUtils.DecryptPassword(pairs["passwordenc"]); var passHash = password.ToMD5(); _emulationAdapter.TryCreateProfile(nick, email, passHash); } break; case "getprofile": // TODO break; default: Debugger.Break(); break; } }
/// <summary> /// Обработка HTTP запроса от игры на порт 80. /// Обрабатывает новостное сообщение, наличие патча, запрос страницы статистики, настройки автоматча и список имен комнат чата. /// </summary> void OnHttpReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { try { var str = buffer.ToUtf8(count); LogTrace("HTTP CLIENT HASH " + node.GetHashCode()); LogTrace("HTTP " + str); HttpRequest request; using (var ms = new MemoryStream(buffer, 0, count, false, true)) request = HttpHelper.GetRequest(ms); using (var ms = new MemoryStream()) { // Запрос страницы статистики по кнопке из игры if (request.Url.StartsWith("/SS_StatsPage", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.DowstatsRedirect()); goto END; } // Запрос текста новостей if (request.Url.EndsWith("news.txt", StringComparison.OrdinalIgnoreCase)) { LogForUser($"News requested"); // Фикс для рускоязычных if (request.Url.EndsWith("Russiandow_news.txt", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RusNews, Encoding.Unicode)); } else { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.EnNews, Encoding.Unicode)); } goto END; } // Отправка сообщения дня. Вроде нигде не отображается if (request.Url.StartsWith("/motd/motd", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RusNews, Encoding.Unicode)); goto END; } // Проверка на существование патча. Можно прокидывать свои патчи для игры if (request.Url.StartsWith("/motd/vercheck", StringComparison.OrdinalIgnoreCase)) { LogForUser($"Vercheck requested"); // Пример отправки ссылки на скачивания патча //HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(@"\newver\1\newvername\1.4\dlurl\http://127.0.0.1/NewPatchHere.exe")); // Отправка инфы о том, что патча сейчас нет HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(@"\newver\0", Encoding.UTF8)); goto END; } // Запрос списка комнат с именами для отображения в интерфейсе if (request.Url.EndsWith("LobbyRooms.lua", StringComparison.OrdinalIgnoreCase)) { LogForUser($"LobbyRooms requested"); HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RoomPairs, Encoding.ASCII)); goto END; } // Запрос дефолных настроек автоматча if (request.Url.EndsWith("AutomatchDefaultsSS.lua", StringComparison.OrdinalIgnoreCase) || request.Url.EndsWith("AutomatchDefaultsDXP2Fixed.lua", StringComparison.OrdinalIgnoreCase)) { LogForUser($"AutomatchDefaults requested"); //HttpHelper.WriteResponse(ms, HttpResponceBuilder.TextFileBytes(CoreContext.MasterServer.AutomatchDefaultsBytes)); HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.AutomatchDefaults, Encoding.ASCII)); goto END; } /*if (request.Url.EndsWith("homepage.php.htm", StringComparison.OrdinalIgnoreCase)) * { * if (StatsResponce == null || (DateTime.Now - _lastStatsUpdate).TotalMinutes > 5) * StatsResponce = BuildTop10StatsResponce(); * * HttpHelper.WriteResponse(ms, StatsResponce); * goto END; * }*/ // Если дошли сюда - отправляет NotFound HttpHelper.WriteResponse(ms, HttpResponceBuilder.NotFound()); END: LogTrace("HTTP WANT TO SEND " + node.GetHashCode() + " " + ms.Length); handler.Send(node, ms.ToArray()); handler.KillClient(node); } } catch (InvalidDataException ex) { //Log(ex); } }
/// <summary> /// На Ping в чате отвечаем Pong. Поддерживает соединение с чат сервером. /// </summary> void HandlePingCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { SendToClientChat(node, $":s PONG :s\r\n"); }
/// <summary> /// Обратаываем установку полного имени пользователя. /// </summary> void HandleUserCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { // Просто сохраняем себе для дальнейшего использования _user = $@"{_name}!{values[1]}@{node.RemoteEndPoint?.Address}"; _shortUser = values[1]; }
/// <summary> /// Обрабатывает полученное сообщение по TCP соединению для запросов статистики /// </summary> void OnStatsReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { var str = Encoding.UTF8.GetString(GSUtils.XorBytes(buffer, 0, count - 7, "GameSpy3D"), 0, count); LogTrace("STATS " + str); // Авторизация на сервере. Выдаем случайный ключ сессии (не обязательно) if (str.StartsWith(@"\auth\\gamename\", StringComparison.OrdinalIgnoreCase)) { var sesskey = Interlocked.Increment(ref _sessionCounter).ToString("0000000000"); handler.Send(node, GSUtils.XorBytes($@"\lc\2\sesskey\{sesskey}\proof\0\id\1\final\", "GameSpy3D", 7)); return; } // Игра присылает серверу ID авторизованного профиля. Но мы и так его знаем, поэтому просто фейкаем успех if (str.StartsWith(@"\authp\\pid\", StringComparison.OrdinalIgnoreCase)) { var pid = GetPidFromInput(str, 12); var profileId = long.Parse(pid); handler.Send(node, GSUtils.XorBytes($@"\pauthr\{pid}\lid\1\final\", "GameSpy3D", 7)); return; } // Запрос данных профиля. Надо отправить данные по списку запрошенных ключей. // Всегда запрашивается статистика if (str.StartsWith(@"\getpd\", StringComparison.OrdinalIgnoreCase)) { // \\getpd\\\\pid\\87654321\\ptype\\3\\dindex\\0\\keys\\\u0001points\u0001points2\u0001points3\u0001stars\u0001games\u0001wins\u0001disconn\u0001a_durat\u0001m_streak\u0001f_race\u0001SM_wins\u0001Chaos_wins\u0001Ork_wins\u0001Tau_wins\u0001SoB_wins\u0001DE_wins\u0001Eldar_wins\u0001IG_wins\u0001Necron_wins\u0001lsw\u0001rnkd_vics\u0001con_rnkd_vics\u0001team_vics\u0001mdls1\u0001mdls2\u0001rg\u0001pw\\lid\\1\\final\\ // \getpd\\pid\87654321\ptype\3\dindex\0\keys\pointspoints2points3starsgameswinsdisconna_duratm_streakf_raceSM_winsChaos_winsOrk_winsTau_winsSoB_winsDE_winsEldar_winsIG_winsNecron_winslswrnkd_vicscon_rnkd_vicsteam_vicsmdls1mdls2rgpw\lid\1\final\ var profileId = GetPidFromInput(str, 12); var keysIndex = str.IndexOf("keys") + 5; var keys = str.Substring(keysIndex); var keysList = keys.Split(new string[] { "\u0001", "\\lid\\1\\final\\", "final", "\\", "lid" }, StringSplitOptions.RemoveEmptyEntries); var keysResult = new StringBuilder(); var stats = _emulationAdapter.GetUserStatsInfo(profileId.ParseToLongOrDefault()); for (int i = 0; i < keysList.Length; i++) { var key = keysList[i]; keysResult.Append("\\" + key + "\\"); switch (key) { case "points": keysResult.Append(stats.Score1v1); break; case "points2": keysResult.Append(stats.Score2v2); break; case "points3": keysResult.Append(stats.Score3v3_4v4); break; case "stars": keysResult.Append(stats.StarsCount); break; case "games": keysResult.Append(stats.GamesCount); break; case "wins": keysResult.Append(stats.WinsCount); break; case "disconn": keysResult.Append(stats.Disconnects); break; case "a_durat": keysResult.Append(stats.AverageDuration); break; case "m_streak": keysResult.Append(stats.Best1v1Winstreak); break; case "f_race": keysResult.Append(stats.FavouriteRace); break; // Ключи, которые не используюся игрой, но запрашиваются. Может на что-то и влияет, но я ничего не обнаружил /* case "SM_wins": keysResult.Append("0"); break; * case "Chaos_wins": keysResult.Append("0"); break; * case "Ork_wins": keysResult.Append("0"); break; * case "Tau_wins": keysResult.Append("0"); break; * case "SoB_wins": keysResult.Append("0"); break; * case "DE_wins": keysResult.Append("0"); break; * case "Eldar_wins": keysResult.Append("0"); break; * case "IG_wins": keysResult.Append("0"); break; * case "Necron_wins": keysResult.Append("0"); break; * case "lsw": keysResult.Append("0"); break; * case "rnkd_vics": keysResult.Append("0"); break; * case "con_rnkd_vics": keysResult.Append("0"); break; * case "team_vics": keysResult.Append("0"); break; * case "mdls1": keysResult.Append("0"); break; * case "mdls2": keysResult.Append("0"); break; * case "rg": keysResult.Append("0"); break; * case "pw": keysResult.Append("0"); break;*/ default: keysResult.Append("0"); break; } } handler.Send(node, GSUtils.XorBytes($@"\getpdr\1\lid\1\pid\{profileId}\mod\{stats.ModifiedTimeTick}\length\{keys.Length}\data\{keysResult}\final\", "GameSpy3D", 7)); return; } // Игра присылает обновление данных профиля по списку ключей. // Игнорируем, потому что статистика обновляется другим способов. Просто фейкаем успех if (str.StartsWith(@"\setpd\", StringComparison.OrdinalIgnoreCase)) { var pid = GetPidFromInput(str, 12); var lidIndex = str.IndexOf("\\lid\\", StringComparison.OrdinalIgnoreCase); var lid = str.Substring(lidIndex + 5, 1); var timeInSeconds = (ulong)((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); // \setpd\\pid\3\ptype\1\dindex\0\kv\1\lid\1\length\413\data\\ckey\5604 - 7796 - 6425 - 0127 - DA96\system\Nr.Proc:8, Type: 586, GenuineIntel, unknown: f = 6,m = 12, Fam: 6, Mdl: 12, St: 3, Fe: 7, OS: 7, Ch: 15\speed\CPUSpeed: 3.5Mhz\os\OS NT 6.2\lang\Language: Русский(Россия), Country: Россия, User Language:Русский(Россия), User Country:Россия\vid\Card: Dx9: Hardware TnL, NVIDIA GeForce GTX 1080, \vidmod\Mode: 1920 x 1080 x 32\mem\2048Mb phys. memory handler.Send(node, GSUtils.XorBytes($@"\setpdr\1\lid\{lid}\pid\{pid}\mod\{timeInSeconds}\final\", "GameSpy3D", 7)); return; } // Игры присылает информацию о завершенной игре (камстока или автоматч) // Отсюда можно обновлять статистику игроков. Может быть прислано несколько раз от разных игроков в конце одного и того же матча, // поэтому необходима защита на сервере от двойного засчитывания статистики if (str.StartsWith(@"\updgame\", StringComparison.OrdinalIgnoreCase)) { var gamedataIndex = str.IndexOf("gamedata"); // Обрезаем конец сообщения var finalIndex = str.IndexOf("final"); var gameDataString = str.Substring(gamedataIndex + 9, finalIndex - gamedataIndex - 10); var valuesList = gameDataString.Split(new string[] { "\u0001", "\\lid\\1\\final\\", "\\" }, StringSplitOptions.None); // Преобразываем все пары ключ-значение в словарь для удобства var dictionary = new Dictionary <string, string>(); for (int i = 0; i < valuesList.Length - 1; i += 2) { if (i == valuesList.Length - 1) { continue; } dictionary[valuesList[i]] = valuesList[i + 1]; } if (!dictionary.TryGetValue("Mod", out string v)) { dictionary.Clear(); for (int i = 1; i < valuesList.Length - 1; i += 2) { if (i == valuesList.Length - 1) { continue; } dictionary[valuesList[i]] = valuesList[i + 1]; } } // Использованный мод и его версия var mod = dictionary["Mod"]; var modVersion = dictionary["ModVer"]; var playersCount = int.Parse(dictionary["Players"]); for (int i = 0; i < playersCount; i++) { // Dont process games with AI if (dictionary["PHuman_" + i] != "1") { LogTrace($"Stats socket: GAME WITH NONHUMAN PLAYER"); return; } } var gameInternalSession = dictionary["SessionID"]; var teamsCount = int.Parse(dictionary["Teams"]); var version = dictionary["Version"]; // Строим уникальные идентификатор сессии игры, чтобы в дальнейшем не зачислить дважды одну и ту же игру var uniqueGameSessionBuilder = new StringBuilder(gameInternalSession); for (int i = 0; i < playersCount; i++) { uniqueGameSessionBuilder.Append('<'); uniqueGameSessionBuilder.Append(dictionary["player_" + i]); uniqueGameSessionBuilder.Append('>'); } var uniqueSession = uniqueGameSessionBuilder.ToString(); // Строим объекты с данным игроков var players = new PlayerData[playersCount]; for (int i = 0; i < players.Length; i++) { var player = new PlayerData(); player.Name = dictionary["player_" + i]; player.Race = dictionary["PRace_" + i]; player.Team = int.Parse(dictionary["PTeam_" + i]); player.FinalState = (PlayerFinalState)Enum.Parse(typeof(PlayerFinalState), dictionary["PFnlState_" + i]); players[i] = player; } // Собираем окончательный объект с данными var gameFinishedMessage = new GameFinishedData { Map = dictionary["Scenario"], SessionId = uniqueSession, Duration = long.Parse(dictionary["Duration"]), ModName = dictionary["Mod"], ModVersion = dictionary["ModVer"], Players = players, IsRateGame = dictionary["Ladder"] == "1" }; _emulationAdapter.SendGameFinishedData(gameFinishedMessage); //DowstatsReplaySender.SendReplay(gameFinishedMessage); return; } // Создание новой игры до регистрации статистики. Никакой полезной информации нет, поэтому игнорируем. // Вся логика произойдет в момент отправки данных об игре. if (str.StartsWith(@"\newgame\", StringComparison.OrdinalIgnoreCase)) { return; } // На случай, если есть еще какие-то команды Debugger.Break(); }
void KillNode(TcpPortHandler handler, TcpClientNode node) { // Получение 0 байт говорит о том, что клиент хочет прекратить взаимодействие. handler.KillClient(node); }
/// <summary> /// Обрабатывает вход в чат. /// </summary> void HandleJoinCommand(TcpPortHandler handler, TcpClientNode node, string line, string[] values) { var channelName = values[1]; // Определяем, главный ли чат if (channelName.StartsWith("#GPG", StringComparison.OrdinalIgnoreCase)) { var users = _emulationAdapter.GetUsersInMainChat(); var builder = new StringBuilder(); builder.Append($":{_user} JOIN {channelName}\r\n"); // SendToClientChat(node, $":{_user} JOIN {channelName}\r\n"); builder.Append($":s 331 {channelName} :No topic is set\r\n"); // SendToClientChat(node, $":s 331 {channelName} :No topic is set\r\n"); _inChat = true; var playersList = new StringBuilder(); for (int i = 0; i < users.Length; i++) { var user = users[i]; playersList.Append(user + " "); } // Посылаем список юзеров в чате builder.Append($":s 353 {_name} = {channelName} :{playersList}\r\n"); //SendToClientChat(node, $":s 353 {_name} = {channelName} :{playersList}\r\n"); builder.Append($":s 366 {_name} {channelName} :End of NAMES list\r\n"); //SendToClientChat(node, $":s 366 {_name} {channelName} :End of NAMES list\r\n"); SendToClientChat(builder.ToString()); } else { if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) { // Вход в комнату для автоматча // Извлекаем уникальный хэш, чтобы определить сопоставить со списком выданных ранее хостов. var roomHash = channelName.Split('!')[2]; LogForUser($"Try to get lobby [{roomHash}]"); // Берем хост, если хоста нет. Значит это попытка войти в комнате локального хоста. if (_lastLoadedLobbies.TryGetValue(roomHash, out GameServerDetails details)) { // Входим в чужой хост LogForUser($"Try to enter lobby [{roomHash}]"); var hostId = details.HostId; // Попытка войти в хост. Сервер решает успех _emulationAdapter.TryEnterInLobby(hostId, _name, new EnterInLobbySuccessDelegate((hostName, members) => { _enteredLobbyHash = details.RoomHash; Log($"Entered to lobby [{roomHash}]"); var playersList = new StringBuilder(); // Вошедший должен быть в этом списке for (int i = 0; i < members.Length; i++) { var member = members[i]; Log($"Player {i} [{GetNickHash(member)}]"); playersList.Append(member + " "); } // Теперь все должны узнать, что юзер вошел. Делается через Broadcast _emulationAdapter.SendLobbyBroadcast($"JOIN {_user}"); // Себе отправляем результат сразу. SendToClientChat(node, $":{_user} JOIN {channelName}\r\n"); var topic = hostName; // Присылаем список пользователей в комнате SendToClientChat(node, $":s 331 {channelName} :{topic}\r\n"); SendToClientChat(node, $":s 353 {_name} = {channelName} :@{playersList}\r\n"); SendToClientChat(node, $":s 366 {_name} {channelName} :End of NAMES list\r\n"); }), new EnterInLobbyFailedDelegate(() => { // Не удалось войти в чате, причина не важна. После этого игра попробует создать хост самостоятельно SendToClientChat(node, $":{_user} {channelName} :Bad Channel Mask\r\n"); })); } else { // Мы в своем хосте. Выполним требования IRC LogForUser($"This lobby is local [{roomHash}]"); LogForUser($"Player 0 [{GetNickHash(_name)}]"); _localServerHash = roomHash; _enteredLobbyHash = roomHash; var builder = new StringBuilder(); builder.Append($":{_user} JOIN {channelName}\r\n"); builder.Append($":s 331 {channelName} :No topic is set\r\n"); // Мы только одни будем в комнате в этот момент. builder.Append($":s 353 {_name} = {channelName} :@{_name}\r\n"); builder.Append($":s 366 {_name} {channelName} :End of NAMES list\r\n"); SendToClientChat(node, builder.ToString()); } } } }
/// <summary> /// Обрабатывает проверку CD ключа игры. Мы просто всегда возвращаем успех. /// Если ключа у юзера нет в реестре, то до этой команды не дойдет. Помогает разблокировка рас. /// </summary> void HandleCdkeyCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { LogForUser($"Cdkey check"); SendToClientChat(node, $":s 706 {_name}: 1 :\"Authenticated\"\r\n"); }
/// <summary> /// Обрабатывает команду IRC на получение значения юзера в комнате чата по массиву ключей /// </summary> void HandleGetckeyCommand(TcpPortHandler handler, TcpClientNode node, string[] values) { var channelName = values[1]; //GETCKEY #GPG!1 * 000 0 :\\username\\b_flags var id = values[3]; var keysString = values[5]; // Извлекаем список ключей var keys = keysString.Split(':', '\\'); var builder = new StringBuilder(); if (channelName.StartsWith("#GSP", StringComparison.OrdinalIgnoreCase)) { //var roomHash = channelName.Split('!')[2]; // Если мы по какой-то причине не в лобби - эмулируем ответ, будто мы в лобби. Защита от багов if (!_emulationAdapter.IsInLobbyNow) { for (int k = 0; k < keys.Length; k++) { var key = keys[k]; if (string.IsNullOrWhiteSpace(key)) { continue; } string value; if (key == "username") { value = _shortUser; } else { value = _emulationAdapter.GetLobbyKeyValue(key); } builder.Append($@"\{value ?? string.Empty}"); } SendToClientChat(node, $":s 702 {_name} {channelName} {id} :{builder}\r\n"); } else { // Если мы в лобби - отправляем нормальные данные по указанным ключам var members = _emulationAdapter.GetLobbyMembers(); for (int i = 0; i < members.Length; i++) { builder.Clear(); var name = members[i]; for (int k = 0; k < keys.Length; k++) { var key = keys[k]; if (string.IsNullOrWhiteSpace(key)) { continue; } var value = _emulationAdapter.GetLobbyMemberData(name, key); builder.Append(@"\" + value); } SendToClientChat(node, $":s 702 {_name} {channelName} {name} {id} :{builder}\r\n"); } } SendToClientChat(node, $":s 703 {_name} {channelName} {id} :End of GETCKEY\r\n"); } else { // Для главного чата эмулируем значения, если их по какой-то причине нет, // или даем нормальные данные, полученные от других клиентов if (channelName.StartsWith("#GPG", StringComparison.OrdinalIgnoreCase)) { var users = _emulationAdapter.GetUsersInMainChat(); for (int i = 0; i < users.Length; i++) { var user = users[i]; builder.Clear(); for (int k = 0; k < keys.Length; k++) { var key = keys[k]; if (string.IsNullOrWhiteSpace(key)) { continue; } string value = string.Empty; if (key == "username") { var localName = _emulationAdapter.LocalUserName; if (string.Equals(localName, user, StringComparison.Ordinal)) { value = _shortUser; } else { value = $"X{GetEncodedIp(user)}X|{_emulationAdapter.GetUserInGameProfileId(user)}"; } } if (key == "b_stats") { value = _emulationAdapter.GetUserGlobalKeyValue(user, key); if (value == null) { var stats = _emulationAdapter.GetUserStatsInfo(user); value = $"{_emulationAdapter.GetUserInGameProfileId(user)}|{stats.Score1v1}|{stats.StarsCount}|"; } } if (key == "b_flags") { value = _emulationAdapter.GetUserGlobalKeyValue(user, key); if (value == null) { value = string.Empty; } } builder.Append(@"\" + value); } SendToClientChat(node, $":s 702 {_name} {channelName} {user} {id} :{builder}\r\n"); } SendToClientChat(node, $":s 703 {_name} {channelName} {id} :End of GETCKEY\r\n"); } } }
/// <summary> /// Обрабатывает входящие TCP соединения для сервера LOGIN (Client) /// </summary> void OnClientAccept(TcpPortHandler handler, TcpClientNode node, CancellationToken token) { //Обновляем челендж для нового соединения _serverChallenge = RandomHelper.GetString(10); handler.SendAskii(node, $@"\lc\1\challenge\{_serverChallenge}\id\1\final\"); }