using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.IO; namespace SRB2Updater { class ServerQuerier { private UdpClient udpclient; private IPEndPoint ipepMS; private const int MS_HOLEPUNCH_SIZE = 0; private const int PT_ASKINFO_SIZE = 16; private const byte PT_ASKINFO = 12; private const byte PT_SERVERINFO = 13; private const int MAXSERVERNAME = 32; private const int MAX_WADPATH = 128; /// /// Constructs a ServerQuerier object. /// public ServerQuerier() { udpclient = new UdpClient(0, AddressFamily.InterNetwork); // Fix for WSAECONNRESET. Only affects Win2k and up. If I send a // packet to a host which replies with an ICMP Port Unreachable, // subsequent socket operations go doo-lally. So, we enable the // older behaviour of ignoring these ICMP messages, since we don't // care about them anyway. if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 5) { const uint IOC_IN = 0x80000000; const uint IOC_VENDOR = 0x18000000; const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; udpclient.Client.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { Convert.ToByte(false) }, null); } } public void StartListening(ServerInfoReceiveHandler sirh) { // Start listening. udpclient.BeginReceive(new AsyncCallback(ServerInfoReceiveHandler.Receive), sirh); } /// /// Sets the master server address. Necessary before querying via the MS. /// /// IP address of hostname of MS. /// Port of MS. public void SetMasterServer(string strAddress, ushort unPort) { IPAddress address = Dns.GetHostEntry(strAddress).AddressList[0]; ipepMS = new IPEndPoint(address, unPort); } public void Query(string strAddress, ushort unPort) { // Build the packet. byte[] byPacket = new byte[PT_ASKINFO_SIZE]; BinaryWriter bw = new BinaryWriter(new MemoryStream(byPacket)); bw.Seek(4, SeekOrigin.Begin); // Skip the checksum. bw.Write((byte)0); // ack bw.Write((byte)0); // ackreturn bw.Write((byte)PT_ASKINFO); // Packet type. bw.Write((byte)0); // Reserved. bw.Write((byte)0); // Version. This is actually unnecessary -- the client will reply anyway. -MattW_CFI bw.Write((byte)0); // Reserved. bw.Write((byte)0); // Reserved. bw.Write((byte)0); // Reserved. // Time for ping calculation. bw.Write(unchecked((uint)(DateTime.Now.Ticks / 10000))); // Calculate the checksum. bw.Seek(0, SeekOrigin.Begin); bw.Write(SRB2Checksum(byPacket)); // Send the packet. udpclient.Send(byPacket, byPacket.Length, strAddress, unPort); } /// /// Calculates the checksum of an SRB2 packet. /// /// Packet. /// Checksum. private static uint SRB2Checksum(byte[] byPacket) { uint c = 0x1234567; int i; for (i = 4; i < byPacket.Length; i++) unchecked { c += (uint)byPacket[i] * (uint)(i - 3); } return c; } private static string ReadFixedLengthStr(BinaryReader br, int iLen) { String str = Encoding.ASCII.GetString(br.ReadBytes(iLen)); int iPos = str.IndexOf("\0"); if (iPos >= 0) str = str.Remove(iPos); return str; } public abstract class ServerInfoReceiveHandler { UdpClient udpclient; IPEndPoint ipepRemote; /// /// Called after a server info packet is received. /// /// Server info. public abstract void ProcessServerInfo(SRB2ServerInfo srb2si); public abstract void HandleException(Exception e); public ServerInfoReceiveHandler(ServerQuerier sq) { ipepRemote = new IPEndPoint(IPAddress.Any, 0); udpclient = sq.udpclient; } public static void Receive(IAsyncResult ar) { ServerInfoReceiveHandler sirh = (ServerInfoReceiveHandler)ar.AsyncState; byte[] byPacket = sirh.udpclient.EndReceive(ar, ref sirh.ipepRemote); // Analyse the packet. BinaryReader br = new BinaryReader(new MemoryStream(byPacket)); // Get the checksum. uint uiChecksum = br.ReadUInt32(); // Skip ack and ackreturn and get packet type. br.ReadBytes(2); byte byPacketType = br.ReadByte(); // Only interested in valid PT_SERVERINFO packets. if (byPacketType == PT_SERVERINFO && uiChecksum == SRB2Checksum(byPacket)) { bool bMalformed = true; // Skip padding. br.ReadByte(); // Remember where we are. long iPacketStart = br.BaseStream.Position; // Try to interpret the packet in each recognised format. foreach (ServerInfoVer siv in Enum.GetValues(typeof(ServerInfoVer))) { SRB2ServerInfo srb2si; byte byNumWads = 0; srb2si.siv = siv; br.BaseStream.Position = iPacketStart; // Get address from socket. srb2si.strAddress = sirh.ipepRemote.Address.ToString(); srb2si.unPort = unchecked((ushort)sirh.ipepRemote.Port); // Get version. byte byVersion = br.ReadByte(); if (siv == ServerInfoVer.SIV_PREME) { br.ReadBytes(3); uint uiSubVersion = br.ReadUInt32(); // Format version. // MattW_CFI: I hope you don't mind this exception, Oogaland, but 0.01.6 looks odd >_> if (byVersion == 1 && uiSubVersion == 6) srb2si.strVersion = "X.01.6"; else srb2si.strVersion = byVersion.ToString(); //srb2si.strVersion = String.Format("{0}.{1:00}.{2}", byVersion / 100, byVersion % 100, uiSubVersion); } else { byte bySubVersion = br.ReadByte(); // Format version. //srb2si.strVersion = String.Format("{0}.{1:00}.{2}", byVersion / 100, byVersion % 100, bySubVersion); srb2si.strVersion = byVersion.ToString(); } srb2si.byPlayers = br.ReadByte(); srb2si.byMaxplayers = br.ReadByte(); srb2si.byGametype = br.ReadByte(); srb2si.bModified = (br.ReadByte() != 0); if (siv == ServerInfoVer.SIV_ME) byNumWads = br.ReadByte(); srb2si.sbyAdminplayer = br.ReadSByte(); if (siv == ServerInfoVer.SIV_PREME) br.ReadBytes(3); // Calculate ping. srb2si.uiTime = unchecked((uint)((long)(DateTime.Now.Ticks / 10000 - br.ReadUInt32()) % ((long)UInt32.MaxValue + 1))); if (siv == ServerInfoVer.SIV_PREME) br.ReadUInt32(); // Get and tidy map name. if (siv == ServerInfoVer.SIV_PREME) { srb2si.strMapName = ReadFixedLengthStr(br, 8); srb2si.strName = ReadFixedLengthStr(br, MAXSERVERNAME); } else { srb2si.strName = ReadFixedLengthStr(br, MAXSERVERNAME); srb2si.strMapName = ReadFixedLengthStr(br, 8); } if (siv == ServerInfoVer.SIV_PREME) byNumWads = br.ReadByte(); // Create new list of strings of initial size equal to number of wads. srb2si.listFiles = new List(byNumWads); // Get the files info. byte[] byFiles = br.ReadBytes(siv == ServerInfoVer.SIV_PREME ? 4096 : 936); BinaryReader brFiles = new BinaryReader(new MemoryStream(byFiles)); // Extract the filenames. try { for (int i = 0; i < byNumWads; i++) { bool bFullString = false; AddedWad aw = new AddedWad(); if (siv == ServerInfoVer.SIV_PREME) { aw.bImportant = brFiles.ReadByte() != 0; aw.downloadtype = (DownloadTypes)brFiles.ReadByte(); } else { byte byFileStatus = brFiles.ReadByte(); aw.bImportant = (byFileStatus & 0xF) != 0; aw.downloadtype = (DownloadTypes)(byFileStatus >> 4); } aw.uiSize = brFiles.ReadUInt32(); // Work out how long the string is. int iStringPos = (int)brFiles.BaseStream.Position; while (iStringPos < byFiles.Length && byFiles[iStringPos] != 0) iStringPos++; // Make sure it's not longer than the max name length. if (iStringPos - (int)brFiles.BaseStream.Position > MAX_WADPATH) { bFullString = true; iStringPos = MAX_WADPATH + (int)brFiles.BaseStream.Position; } // Get the info and add it, if possible. if (iStringPos > (int)brFiles.BaseStream.Position) { aw.strFilename = Encoding.ASCII.GetString(brFiles.ReadBytes(iStringPos - (int)brFiles.BaseStream.Position)); srb2si.listFiles.Add(aw); } // Skip nul. if (!bFullString) brFiles.ReadByte(); // Skip the md5sum. brFiles.ReadBytes(16); } // Okay, done! Do something useful with the server info. sirh.ProcessServerInfo(srb2si); // If we got this far without an exception, leave the foreach loop. bMalformed = false; break; } catch (EndOfStreamException) { // Packet doesn't match supposed type, so we swallow the exception // and try remaining types. } catch (Exception e) { sirh.HandleException(e); break; } } if (bMalformed) sirh.HandleException(new Exception("Received invalid PT_SERVERINFO packet from " + sirh.ipepRemote.Address + ":" + sirh.ipepRemote.Port + ".")); } // Resume listening. sirh.ipepRemote = new IPEndPoint(IPAddress.Any, 0); sirh.udpclient.BeginReceive(new AsyncCallback(Receive), sirh); } } public enum DownloadTypes { DT_TOOBIG = 0, DT_OK = 1, DT_DISABLED = 2 } public struct AddedWad { public string strFilename; public bool bImportant; public uint uiSize; public DownloadTypes downloadtype; } public enum ServerInfoVer { SIV_PREME, SIV_ME }; public struct SRB2ServerInfo { public string strAddress; public ushort unPort; public ServerInfoVer siv; public string strVersion; public byte byPlayers; public byte byMaxplayers; public byte byGametype; public bool bModified; public sbyte sbyAdminplayer; public uint uiTime; public string strMapName; public string strName; public List listFiles; } } }