diff --git a/TrueCraft/MultiplayerServer.cs b/TrueCraft/MultiplayerServer.cs index ddcb3f5..4d75192 100644 --- a/TrueCraft/MultiplayerServer.cs +++ b/TrueCraft/MultiplayerServer.cs @@ -67,6 +67,7 @@ namespace TrueCraft private IList LogProviders; internal object ClientLock = new object(); private bool ShuttingDown = false; + private QueryProtocol QueryProtocol; public MultiplayerServer() { @@ -91,6 +92,7 @@ namespace TrueCraft CraftingRepository = craftingRepository; PendingBlockUpdates = new Queue(); EnableClientLogging = false; + QueryProtocol = new TrueCraft.QueryProtocol(25566); AccessConfiguration = Configuration.LoadConfiguration("access.yaml"); @@ -113,12 +115,14 @@ namespace TrueCraft Log(LogCategory.Notice, "Running TrueCraft server on {0}", EndPoint); NetworkWorker.Start(); EnvironmentWorker.Change(100, 1000 / 20); + QueryProtocol.Start(); } public void Stop() { ShuttingDown = true; Listener.Stop(); + QueryProtocol.Stop(); foreach (var w in Worlds) w.Save(); foreach (var c in Clients) diff --git a/TrueCraft/QueryProtocol.cs b/TrueCraft/QueryProtocol.cs new file mode 100644 index 0000000..825fc5e --- /dev/null +++ b/TrueCraft/QueryProtocol.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net; +using System.Net.Sockets; +using System.IO; +using System.Threading; + +namespace TrueCraft +{ + public class QueryProtocol + { + private UdpClient Udp; + private int Port; + private Timer Timer; + private Random Rnd; + private CancellationTokenSource CToken; + + private readonly Tuple ProtocolVersion = new Tuple(0xFE, 0xFD); + private readonly byte Type_Handshake = 0x09; + private readonly byte Type_Stat = 0x00; + + private Dictionary UserList; + private object UserLock = new object(); + + public QueryProtocol(int port) + { + Port = port; + Rnd = new Random(); + } + public void Start() + { + Udp = new UdpClient(Port); + Timer = new Timer(ResetUserList, null, 0, 30000); + CToken = new CancellationTokenSource(); + Udp.BeginReceive(HandleReceive, null); + } + + private void HandleReceive(IAsyncResult ar) + { + if (CToken.IsCancellationRequested) return; + + var clientEP = new IPEndPoint(IPAddress.Any, Port); + byte[] buffer = Udp.EndReceive(ar, ref clientEP); + + switch (buffer.Length) + { + case 7: HandleHandshake(buffer, clientEP); break; + case 11: HandleBasicStat(buffer, clientEP); break; + case 15: HandleFullStat(buffer, clientEP); break; + } + + if (CToken.IsCancellationRequested) return; + + Udp.BeginReceive(HandleReceive, null); + } + + private void HandleHandshake(byte[] buffer, IPEndPoint clientEP) + { + var stream = GetStream(buffer); + CheckHead(Type_Handshake, stream); + int sessionId = GetSessionId(stream); + + var user = new QueryUser { SessionId = sessionId, ChallengeToken = Rnd.Next() }; + lock (UserLock) + { + if (UserList.ContainsKey(clientEP)) + UserList.Remove(clientEP); + UserList.Add(clientEP, user); + } + + var response = GetStream(); + WriteHead(Type_Handshake, user, response); + WriteStringToStream(user.ChallengeToken.ToString(), response.BaseStream); + + SendResponse(response, clientEP); + } + + private void HandleBasicStat(byte[] buffer, IPEndPoint clientEP) + { + var stream = GetStream(buffer); + CheckHead(Type_Stat, stream); + int sessionId = GetSessionId(stream); + int token = GetToken(stream); + + var user = GetUser(clientEP); + if (user.ChallengeToken != token || user.SessionId != sessionId) throw new Exception("Invalid credentials"); + + var stats = GetStats(); + var response = GetStream(); + WriteHead(Type_Stat, user, response); + WriteStringToStream(stats["hostname"], response.BaseStream); + WriteStringToStream(stats["gametype"], response.BaseStream); + WriteStringToStream(stats["numplayers"], response.BaseStream); + WriteStringToStream(stats["maxplayers"], response.BaseStream); + byte[] hostport = BitConverter.GetBytes(UInt16.Parse(stats["hostport"])); + Array.Reverse(hostport);//The specification needs little endian short + response.Write(hostport); + WriteStringToStream(stats["hostip"], response.BaseStream); + + SendResponse(response, clientEP); + } + + private void HandleFullStat(byte[] buffer, IPEndPoint clientEP) + { + var stream = GetStream(buffer); + CheckHead(Type_Stat, stream); + int sessionId = GetSessionId(stream); + int token = GetToken(stream); + + var user = GetUser(clientEP); + if (user.ChallengeToken != token || user.SessionId != sessionId) throw new Exception("Invalid credentials"); + + var stats = GetStats(); + var response = GetStream(); + WriteHead(Type_Stat, user, response); + foreach (var pair in stats) + { + WriteStringToStream(pair.Key, response.BaseStream); + WriteStringToStream(pair.Value, response.BaseStream); + } + + SendResponse(response, clientEP); + } + + private void CheckVersion(BinaryReader stream) + { + byte ver1 = stream.ReadByte(); + byte ver2 = stream.ReadByte(); + if (ver1 != ProtocolVersion.Item1 || ver2 != ProtocolVersion.Item2) + throw new Exception("Incorrect Protocol Version"); + } + private void CheckType(byte Type, BinaryReader stream) + { + byte type = stream.ReadByte(); + if (type != Type) throw new Exception("Incorrect Type"); + } + private void CheckHead(byte Type, BinaryReader stream) + { + CheckVersion(stream); + CheckType(Type, stream); + } + private int GetSessionId(BinaryReader stream) + { + stream.BaseStream.Position = 3; + return stream.ReadInt32(); + } + private int GetToken(BinaryReader stream) + { + stream.BaseStream.Position = 7; + return stream.ReadInt32(); + } + private BinaryReader GetStream(byte[] buffer) + { return new BinaryReader(new MemoryStream(buffer)); } + private BinaryWriter GetStream() + { return new BinaryWriter(new MemoryStream()); } + private void WriteHead(byte type, QueryUser user, BinaryWriter stream) + { + stream.Write(type); + stream.Write(user.SessionId); + } + private void SendResponse(BinaryWriter res, IPEndPoint destination) + { + byte[] data = ((MemoryStream)res.BaseStream).ToArray(); + Udp.Send(data, data.Length, destination); + } + private QueryUser GetUser(IPEndPoint ep) + { + QueryUser user; + lock (UserLock) + if (!UserList.TryGetValue(ep, out user)) throw new Exception("Undefined user"); + return user; + } + private Dictionary GetStats() + { + var stats = new Dictionary(); + stats.Add("hostname", Program.ServerConfiguration.MOTD); + stats.Add("gametype", "SMP"); + stats.Add("game_id", "TRUECRAFT"); + stats.Add("version", "1.0"); + stats.Add("plugins", ""); + stats.Add("map", Program.Server.Worlds.First().Name); + stats.Add("numplayers", Program.Server.Clients.Count.ToString()); + stats.Add("maxplayers", "64"); + stats.Add("hostport", Program.ServerConfiguration.ServerPort.ToString()); + stats.Add("hostip", Program.ServerConfiguration.ServerAddress); + return stats; + } + + public void Stop() + { + CToken.Cancel(); + Udp.Close(); + } + + private void ResetUserList(object state) + { + lock (UserLock) + UserList = new Dictionary(); + } + + struct QueryUser + { + public int SessionId; + public int ChallengeToken; + } + + private byte[] String0ToBytes(string s) + { return Encoding.UTF8.GetBytes(s + "\0"); } + private void WriteToStream(byte[] bytes, Stream stream) + { stream.Write(bytes, 0, bytes.Length); } + private void WriteStringToStream(string s, Stream stream) + { WriteToStream(String0ToBytes(s), stream); } + } +} diff --git a/TrueCraft/TrueCraft.csproj b/TrueCraft/TrueCraft.csproj index 30149d8..ad320bb 100644 --- a/TrueCraft/TrueCraft.csproj +++ b/TrueCraft/TrueCraft.csproj @@ -42,6 +42,7 @@ +