From 5547ff86e8233605fedda1351325330aedbc20b1 Mon Sep 17 00:00:00 2001 From: manio143 Date: Sat, 20 Jun 2015 08:23:57 +0200 Subject: [PATCH 1/3] Support for Query Protocol --- TrueCraft/MultiplayerServer.cs | 4 + TrueCraft/QueryProtocol.cs | 216 +++++++++++++++++++++++++++++++++ TrueCraft/TrueCraft.csproj | 1 + 3 files changed, 221 insertions(+) create mode 100644 TrueCraft/QueryProtocol.cs 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 @@ + From 92c5b305dd9954841a4455247c20de08718b3b15 Mon Sep 17 00:00:00 2001 From: manio143 Date: Sat, 20 Jun 2015 09:07:19 +0200 Subject: [PATCH 2/3] Query - Added missing parts to FullStat --- TrueCraft/QueryProtocol.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/TrueCraft/QueryProtocol.cs b/TrueCraft/QueryProtocol.cs index 825fc5e..ec53178 100644 --- a/TrueCraft/QueryProtocol.cs +++ b/TrueCraft/QueryProtocol.cs @@ -115,11 +115,18 @@ namespace TrueCraft var stats = GetStats(); var response = GetStream(); WriteHead(Type_Stat, user, response); + WriteStringToStream("SPLITNUM", response.BaseStream); foreach (var pair in stats) { WriteStringToStream(pair.Key, response.BaseStream); WriteStringToStream(pair.Value, response.BaseStream); } + response.Write((byte)0x01); + WriteStringToStream("player_\0", response.BaseStream); + var players = GetPlayers(); + foreach (string player in players) + WriteStringToStream(player, response.BaseStream); + response.Write((byte)0x00); SendResponse(response, clientEP); } @@ -187,6 +194,14 @@ namespace TrueCraft stats.Add("hostip", Program.ServerConfiguration.ServerAddress); return stats; } + private List GetPlayers() + { + var names = new List(); + lock (Program.Server.ClientLock) + foreach (var client in Program.Server.Clients) + names.Add(client.Username); + return names; + } public void Stop() { From 64cfe80c9856063053ecff5d1b75452168704861 Mon Sep 17 00:00:00 2001 From: manio143 Date: Sat, 20 Jun 2015 18:05:53 +0200 Subject: [PATCH 3/3] Query - Fixed issues, added to server config --- TrueCraft/MultiplayerServer.cs | 8 +++-- TrueCraft/QueryProtocol.cs | 60 +++++++++++++++----------------- TrueCraft/ServerConfiguration.cs | 8 +++++ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/TrueCraft/MultiplayerServer.cs b/TrueCraft/MultiplayerServer.cs index 4d75192..5101862 100644 --- a/TrueCraft/MultiplayerServer.cs +++ b/TrueCraft/MultiplayerServer.cs @@ -92,7 +92,7 @@ namespace TrueCraft CraftingRepository = craftingRepository; PendingBlockUpdates = new Queue(); EnableClientLogging = false; - QueryProtocol = new TrueCraft.QueryProtocol(25566); + QueryProtocol = new TrueCraft.QueryProtocol(this); AccessConfiguration = Configuration.LoadConfiguration("access.yaml"); @@ -115,14 +115,16 @@ namespace TrueCraft Log(LogCategory.Notice, "Running TrueCraft server on {0}", EndPoint); NetworkWorker.Start(); EnvironmentWorker.Change(100, 1000 / 20); - QueryProtocol.Start(); + if(Program.ServerConfiguration.Query) + QueryProtocol.Start(); } public void Stop() { ShuttingDown = true; Listener.Stop(); - QueryProtocol.Stop(); + if(Program.ServerConfiguration.Query) + QueryProtocol.Stop(); foreach (var w in Worlds) w.Save(); foreach (var c in Clients) diff --git a/TrueCraft/QueryProtocol.cs b/TrueCraft/QueryProtocol.cs index ec53178..8e1495b 100644 --- a/TrueCraft/QueryProtocol.cs +++ b/TrueCraft/QueryProtocol.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; +using TrueCraft.API.Server; namespace TrueCraft { @@ -15,22 +16,24 @@ namespace TrueCraft private int Port; private Timer Timer; private Random Rnd; + private IMultiplayerServer Server; private CancellationTokenSource CToken; - private readonly Tuple ProtocolVersion = new Tuple(0xFE, 0xFD); + private readonly byte[] ProtocolVersion = new byte[] { 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) + public QueryProtocol(IMultiplayerServer server) { - Port = port; Rnd = new Random(); + Server = server; } public void Start() { + Port = Program.ServerConfiguration.QueryPort; Udp = new UdpClient(Port); Timer = new Timer(ResetUserList, null, 0, 30000); CToken = new CancellationTokenSource(); @@ -41,16 +44,25 @@ namespace TrueCraft { if (CToken.IsCancellationRequested) return; - var clientEP = new IPEndPoint(IPAddress.Any, Port); - byte[] buffer = Udp.EndReceive(ar, ref clientEP); - - switch (buffer.Length) + try { - case 7: HandleHandshake(buffer, clientEP); break; - case 11: HandleBasicStat(buffer, clientEP); break; - case 15: HandleFullStat(buffer, clientEP); break; - } + var clientEP = new IPEndPoint(IPAddress.Any, Port); + byte[] buffer = Udp.EndReceive(ar, ref clientEP); + if (CheckVersion(buffer)) + { + if (buffer[2] == Type_Handshake) + HandleHandshake(buffer, clientEP); + else if (buffer[2] == Type_Stat) + { + if (buffer.Length == 11) + HandleBasicStat(buffer, clientEP); + else if (buffer.Length == 15) + HandleFullStat(buffer, clientEP); + } + } + } + catch { } if (CToken.IsCancellationRequested) return; Udp.BeginReceive(HandleReceive, null); @@ -59,7 +71,6 @@ namespace TrueCraft 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() }; @@ -80,7 +91,6 @@ namespace TrueCraft private void HandleBasicStat(byte[] buffer, IPEndPoint clientEP) { var stream = GetStream(buffer); - CheckHead(Type_Stat, stream); int sessionId = GetSessionId(stream); int token = GetToken(stream); @@ -105,7 +115,6 @@ namespace TrueCraft private void HandleFullStat(byte[] buffer, IPEndPoint clientEP) { var stream = GetStream(buffer); - CheckHead(Type_Stat, stream); int sessionId = GetSessionId(stream); int token = GetToken(stream); @@ -131,22 +140,9 @@ namespace TrueCraft SendResponse(response, clientEP); } - private void CheckVersion(BinaryReader stream) + private bool CheckVersion(byte[] ver) { - 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); + return ver[0] == ProtocolVersion[0] && ver[1] == ProtocolVersion[1]; } private int GetSessionId(BinaryReader stream) { @@ -187,8 +183,8 @@ namespace TrueCraft 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("map", Server.Worlds.First().Name); + stats.Add("numplayers", Server.Clients.Count.ToString()); stats.Add("maxplayers", "64"); stats.Add("hostport", Program.ServerConfiguration.ServerPort.ToString()); stats.Add("hostip", Program.ServerConfiguration.ServerAddress); @@ -198,7 +194,7 @@ namespace TrueCraft { var names = new List(); lock (Program.Server.ClientLock) - foreach (var client in Program.Server.Clients) + foreach (var client in Server.Clients) names.Add(client.Username); return names; } diff --git a/TrueCraft/ServerConfiguration.cs b/TrueCraft/ServerConfiguration.cs index 8775602..20944c6 100644 --- a/TrueCraft/ServerConfiguration.cs +++ b/TrueCraft/ServerConfiguration.cs @@ -28,6 +28,8 @@ namespace TrueCraft ServerAddress = "0.0.0.0"; WorldSaveInterval = 30; Singleplayer = false; + Query = true; + QueryPort = 25566; } [YamlMember(Alias = "motd")] @@ -47,5 +49,11 @@ namespace TrueCraft [YamlIgnore] public bool Singleplayer { get; set; } + + [YamlMember(Alias = "query")] + public bool Query { get; set; } + + [YamlMember(Alias = "queryPort")] + public int QueryPort { get; set; } } } \ No newline at end of file