/// <summary> /// This method parses and handles a version request from a game server /// or game client. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstVersionRequest(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; Byte Unknown0; // 0 Byte Unknown1; // 0 Byte Unknown2; // 1 if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadBYTE(out Unknown0)) return; if (!Parser.ReadBYTE(out Unknown1)) return; if (!Parser.ReadBYTE(out Unknown2)) return; SendMstVersion(Sender, BuildNumber); }
/// <summary> /// This method parses and handles a status request from a game server /// or game client. /// </summary> /// <param name="Parser">Supplies the message parser context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstStatusRequest(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; if (!Parser.ReadWORD(out DataPort)) return; // // Do not enter the server into the server list until // bidirectional communication has been established. However, // some broken NATs may respond briefly to the current source // port and then choose another (different!) source port later // for future pings. To handle this case, send a BNXI probe to // both internal and external addresses. If the responses are // the same, then assume that the server is actually located at // the internal address; otherwise, create server records for // both servers. // if (DataPort != (UInt16)Sender.Port) { IPEndPoint InternalAddress = new IPEndPoint(Sender.Address, (int)DataPort); SendServerInfoRequest(InternalAddress); } SendServerInfoRequest(Sender); SendMstStatusResponse(Sender, MstStatus.MST_STATUS_ONLINE); }
/// <summary> /// This method parses and handles a server startup notify message from /// a game server. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstStartupNotify(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { Byte Platform; UInt16 BuildNumber; Byte Unknown0; // 0 Byte Unknown1; // 0 Byte Unknown2; // 1 Byte Unknown3; // 0 Byte Unknown4; // 3 if (!Parser.ReadBYTE(out Platform)) return; if (!Parser.ReadWORD(out BuildNumber)) return; if (!Parser.ReadBYTE(out Unknown0)) return; if (!Parser.ReadBYTE(out Unknown1)) return; if (!Parser.ReadBYTE(out Unknown2)) return; if (!Parser.ReadBYTE(out Unknown3)) return; if (!Parser.ReadBYTE(out Unknown4)) return; // // Query for whether the sender is blacklisted and drop the message // if so. // BlacklistLookup Lookup = new BlacklistLookup(); Lookup.ServerAddress = Sender; if (ServerTracker.IsBlacklisted(Lookup)) return; NWGameServer Server = ServerTracker.LookupServerByAddress(Sender); // // Record the server startup. // Server.OnStartupNotify(Platform, BuildNumber); Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvMstStartupNotify(): Server {0} Platform={1} BuildNumber={2}.", Sender, (char)Platform, BuildNumber); }
/// <summary> /// This method parses and handles a MOTD request from a game server or /// game client. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstMOTDRequest(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; Byte Unknown0; // 0 Byte Unknown1; // 0 if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadBYTE(out Unknown0)) return; if (!Parser.ReadBYTE(out Unknown1)) return; SendMstMOTD(Sender, MOTD); }
/// <summary> /// This method parses and handles a heartbeat message from a game /// server. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstHeartbeat(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 PlayerCount; List<List<string>> PlayerCDKeyList = new List<List<string>>(); if (!Parser.ReadWORD(out PlayerCount)) return; while (PlayerCount-- != 0) { UInt16 CDKeyCount; List<string> CDKeyList = new List<string>(); if (!Parser.ReadWORD(out CDKeyCount)) return; while (CDKeyCount-- != 0) { string CDKey; if (!Parser.ReadSmallString(out CDKey, 16)) return; CDKeyList.Add(CDKey); } PlayerCDKeyList.Add(CDKeyList); } Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvMstHeartbeat(): Server {0} ActivePlayerCount={1}.", Sender, PlayerCDKeyList.Count); }
/// <summary> /// This method parses and handles a player disconnect notify message /// from a game server. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstDisconnectNotify(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; UInt16 EntryCount; UInt16 CDKeyCount; List<string> CDKeyList; NWGameServer Server; if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadWORD(out EntryCount)) return; if (EntryCount == 0) { // // Server shutdown. // Server = ServerTracker.LookupServerByAddress(Sender, false); if (Server == null || Server.Online == false) return; // // Record the server shutdown. // Server.OnShutdownNotify(); Logger.Log(LogLevel.Normal, "NWMasterServer.OnRecvMstShutdownNotify(): Server {0} shut down.", Sender); return; } else if (EntryCount != 1) { return; } if (!Parser.ReadWORD(out CDKeyCount)) return; CDKeyList = new List<string>(); while (CDKeyCount-- != 0) { string CDKey; if (!Parser.ReadSmallString(out CDKey, 16)) return; CDKeyList.Add(CDKey); } Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvMstDisconnectNotify()"); }
/// <summary> /// This method parses and handles a CD-Key authorization request from /// from a game server. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstCDKeyAuthorizationRequest(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; UInt16 EntryCount; UInt32 ClientIP; UInt16 ClientPort; byte[] ServerChallenge; UInt16 Length; List<CDKeyInfo> CDKeyHashes; CDKeyInfo CDKeyHash; string AccountName; int KeyIndex; int PortNumberHbo; if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadWORD(out EntryCount)) return; if (EntryCount != 1) return; if (!Parser.ReadDWORD(out ClientIP)) return; if (!Parser.ReadWORD(out ClientPort)) return; PortNumberHbo = IPAddress.NetworkToHostOrder(ClientPort); if ((PortNumberHbo == 0) || ((PortNumberHbo & 0xFFFF) != PortNumberHbo)) PortNumberHbo = 5120; IPEndPoint ClientEndpoint = new IPEndPoint((long)ClientIP, PortNumberHbo); if (!Parser.ReadWORD(out Length)) return; if ((ServerChallenge = Parser.ReadBytes(Length)) == null) return; if (!Parser.ReadWORD(out Length)) return; CDKeyHashes = new List<CDKeyInfo>(); CDKeyHash = new CDKeyInfo(); KeyIndex = 0; while (Length-- != 0) { UInt16 HashLength; if (!Parser.ReadSmallString(out CDKeyHash.PublicCDKey, 16)) return; if (!Parser.ReadWORD(out HashLength)) return; if ((CDKeyHash.CDKeyHash = Parser.ReadBytes(HashLength)) == null) return; // // N.B. The following is somewhat of a hack in that we are not // bothering to extract the (real) product type from the // CD-Key. As a result, it is possible that the wrong // answer could be provided for clients without all of // the expansions; this situation is considered unlikely // in the current state of affairs. // // A better fit could be had by examining the expansion // mask required by the server in the data table, but // this does not appear worthwhile at this stage. // if (KeyIndex == 0) CDKeyHash.Product = 0; else CDKeyHash.Product = (UInt16) (1 << (KeyIndex - 1)); CDKeyHash.AuthStatus = (UInt16)ConnectStatus.CONNECT_ERR_SUCCESS; CDKeyHashes.Add(CDKeyHash); } if (!Parser.ReadSmallString(out AccountName, 16)) return; SendMstCDKeyAuthorization(Sender, CDKeyHashes); }
/// <summary> /// This method parses and handles a community authorization request /// from a game server. /// </summary> /// <param name="Parser">Supplies the message parse context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvMstCommunityAuthorizationRequest(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; UInt16 Length; byte[] ServerChallenge; string AccountName; byte[] ClientVerifier; UInt16 Language; Byte Platform; Byte IsPlayer; if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadWORD(out Length)) return; if ((ServerChallenge = Parser.ReadBytes(Length)) == null) return; if (!Parser.ReadSmallString(out AccountName, 16)) return; if (!Parser.ReadWORD(out Length)) return; if ((ClientVerifier = Parser.ReadBytes(Length)) == null) return; if (!Parser.ReadWORD(out Language)) return; if (!Parser.ReadBYTE(out Platform)) return; if (!Parser.ReadBYTE(out IsPlayer)) return; SendMstCommunityAccountAuthorization(Sender, AccountName, ConnectStatus.CONNECT_ERR_SUCCESS); }
/// <summary> /// This method parses a server name response from a game server. /// </summary> /// <param name="Parser">Supplies the message parser context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvServerDescriptionResponse(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; string GameDetails; string ModuleDescription; string BuildNumber; UInt16 GameType; string ModuleUrl; string PWCUrl; UInt16 Build; if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadSmallString(out GameDetails, 32)) return; if (!Parser.ReadSmallString(out ModuleDescription, 32)) return; if (!Parser.ReadSmallString(out BuildNumber, 32)) return; if (!Parser.ReadWORD(out GameType)) return; if (Mode == GameMode.NWN2) { if (!Parser.ReadSmallString(out ModuleUrl, 32)) return; if (!Parser.ReadSmallString(out PWCUrl, 32)) return; } else { ModuleUrl = ""; PWCUrl = ""; } try { Build = Convert.ToUInt16(BuildNumber); } catch { Build = 0; } NWGameServer Server = ServerTracker.LookupServerByAddress(Sender, false); if (Server != null) Server.OnDescriptionInfoUpdate(ModuleDescription, ModuleUrl, GameType, PWCUrl, Build, GameDetails); Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvServerDescriptionResponse(): Server {0} description '{1}' URL '{2}' has game type {3}.", Sender, ModuleDescription, ModuleUrl, GameType); }
/// <summary> /// This method parses a server name response from a game server. /// </summary> /// <param name="Parser">Supplies the message parser context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvServerNameResponse(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { Byte UpdateType; UInt16 DataPort; Byte RequestCorrelationCookie; string ServerName; if (!Parser.ReadBYTE(out UpdateType)) return; if (UpdateType != 'U') return; if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadBYTE(out RequestCorrelationCookie)) return; if (RequestCorrelationCookie != 0) return; if (!Parser.ReadSmallString(out ServerName)) return; NWGameServer Server = ServerTracker.LookupServerByAddress(Sender, false); if (Server != null) Server.OnServerNameUpdate(ServerName); Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvServerNameResponse(): Server {0} name is {1}.", Sender, ServerName); }
/// <summary> /// This method parses a server info response from a game server. /// </summary> /// <param name="Parser">Supplies the message parser context.</param> /// <param name="Sender">Supplies the game server address.</param> /// <param name="Socket">Supplies the associated socket descriptor /// upon which the message was received.</param> private void OnRecvServerInfoResponse(ExoParseBuffer Parser, IPEndPoint Sender, SocketInfo Socket) { UInt16 DataPort; Byte Reserved; // 0xFC Byte HasPlayerPassword; Byte MinLevel; Byte MaxLevel; Byte ActivePlayers; Byte MaximumPlayers; Byte IsLocalVault; Byte PVPLevel; Byte IsPlayerPauseAllowed; Byte IsOnePartyOnly; Byte IsELC; Byte HasILR; Byte ExpansionsMask; string ModuleName; string BuildNumber; ServerInfo Info = new ServerInfo(); if (!Parser.ReadWORD(out DataPort)) return; if (!Parser.ReadBYTE(out Reserved)) return; if (Mode == GameMode.NWN2) { if (Reserved != 0xFC) return; } else { if (Reserved != 0xFD) return; } if (!Parser.ReadBYTE(out HasPlayerPassword)) return; if (!Parser.ReadBYTE(out MinLevel)) return; if (!Parser.ReadBYTE(out MaxLevel)) return; if (!Parser.ReadBYTE(out ActivePlayers)) return; if (!Parser.ReadBYTE(out MaximumPlayers)) return; if (!Parser.ReadBYTE(out IsLocalVault)) return; if (!Parser.ReadBYTE(out PVPLevel)) return; if (!Parser.ReadBYTE(out IsPlayerPauseAllowed)) return; if (!Parser.ReadBYTE(out IsOnePartyOnly)) return; if (!Parser.ReadBYTE(out IsELC)) return; if (!Parser.ReadBYTE(out HasILR)) return; if (!Parser.ReadBYTE(out ExpansionsMask)) return; if (!Parser.ReadSmallString(out ModuleName)) return; if (Mode == GameMode.NWN2) { if (!Parser.ReadSmallString(out BuildNumber)) return; } else { BuildNumber = "0"; } try { Info.BuildNumber = Convert.ToUInt16(BuildNumber); } catch { Info.BuildNumber = 0; } // // Query for whether the sender is blacklisted and drop the message // if so. // BlacklistLookup Lookup = new BlacklistLookup(); Lookup.ServerAddress = Sender; Lookup.ModuleName = ModuleName; if (ServerTracker.IsBlacklisted(Lookup)) return; Info.HasPlayerPassword = (HasPlayerPassword != 0); Info.MinLevel = MinLevel; Info.MaxLevel = MaxLevel; Info.ActivePlayers = ActivePlayers; Info.MaximumPlayers = MaximumPlayers; Info.IsLocalVault = (IsLocalVault != 0); Info.PVPLevel = PVPLevel; Info.IsPlayerPauseAllowed = (IsPlayerPauseAllowed != 0); Info.IsOnePartyOnly = (IsOnePartyOnly != 0); Info.IsELC = (IsELC != 0); Info.HasILR = (HasILR != 0); Info.ExpansionsMask = ExpansionsMask; Info.ModuleName = ModuleName; // // Look up the server and update the current server information. // Since the BNXR reply is used to differentiate between broken // NATs and endpoints with multiple servers on the same IP address, // carefully check for whether a duplicate server record exists on // the server internal port before creating a new server record. // NWGameServer Server; Server = ServerTracker.LookupServerByAddress(Sender); if (Mode != GameMode.NWN2) Info.BuildNumber = Server.BuildNumber; Server.OnServerInfoUpdate(Info); if (DataPort == (UInt16)Sender.Port) { // // Both internal and external ports match; the sender is not // likely behind a NAT. No action is necessary behind the // creation of the server record above. // } else { NWGameServer ServerInternal; IPEndPoint InternalAddress = new IPEndPoint(Sender.Address, (int)DataPort); ServerInternal = ServerTracker.LookupServerByAddress(InternalAddress, false); if (ServerInternal == null) { // // No record of a server existing at the internal address // is yet known. Proceed to create the server record at // the external address (as was already performed above). // } else { // // A record exists for both internal and external // addresses for the server. If the configuration values // between both servers are the same, then mark the // external address version as offline and prefer the // internal server address as authoritative (since it must // be globally reachable for a response to have been // received). // if (ServerInternal.CheckForNATDuplicate(Server)) { Logger.Log(LogLevel.Normal, "NWMasterServer.OnRecvServerInfoResponse(): Removing NAT duplicate server {0} in preference of server {1}.", Sender, InternalAddress); return; } } } Logger.Log(LogLevel.Verbose, "NWMasterServer.OnRecvServerInfoResponse(): Server {0} has {1}/{2} players ({3}).", Sender, Info.ActivePlayers, Info.MaximumPlayers, Info.ModuleName); }