/* TShock, a server mod for Terraria Copyright (C) 2011 The TShock Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using Terraria; using System.IO.Streams; namespace TShockAPI { class RconHandler { public static string Password = ""; private static DateTime LastRequest; private static DateTime LastHeartbeat; public static int ListenPort; public static bool ContinueServer = true; public static string Response = ""; private static bool Started; private static UdpClient listener; private static Thread startThread; private static Thread heartbeat; private static Thread listen; public static void ShutdownAllThreads() { if (Started) { startThread.Abort(); heartbeat.Abort(); listen.Abort(); Started = false; } } public static void StartThread() { if (!Started) { startThread = new Thread(Start); startThread.Start(); heartbeat = new Thread(SendHeartbeat); heartbeat.Start(); } Started = true; } public static void Start() { Log.Info("Starting RconHandler."); try { Console.WriteLine(string.Format("RconHandler is running at UDP port {0} and password is {1}", ListenPort, Password)); listen = new Thread(Listener); listen.Start(); while (true) { if (listen.ThreadState != ThreadState.Running) { listen.Abort(); while (listen.ThreadState != ThreadState.Stopped) continue; listen.Start(); } Thread.Sleep(3000); } } catch (Exception e) { Log.Error(e.ToString()); } } private static void Listener() { if (listener == null) try { listener = new UdpClient(ListenPort); } catch (SocketException e) { if (e.SocketErrorCode == SocketError.AddressAlreadyInUse) Log.ConsoleError("Could not bind to " + ListenPort + ". Are you sure you don't have another instance running?"); } catch (Exception e) { Log.Error(e.ToString()); } while (ContinueServer) { try { var listenEP = new IPEndPoint(IPAddress.Any, ListenPort); LastRequest = DateTime.Now; byte[] bytes = listener.Receive(ref listenEP); var packet = ParsePacket(bytes, listenEP); listener.Send(packet, packet.Length, listenEP); } catch (Exception e) { Log.Error(e.ToString()); } } } private static string SendPacket(byte[] bytes, string hostname, int port) { var response = Encoding.UTF8.GetString(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }) + "disconnect"; try { var EP = new IPEndPoint(IPAddress.Any, port); using (var client = new UdpClient()) { client.Connect(hostname, port); client.Client.ReceiveTimeout = 500; client.Send(bytes, bytes.Length); response = Encoding.UTF8.GetString(client.Receive(ref EP)); } } catch (Exception e) { Log.Error(e.ToString()); } return response; } private static byte[] ParsePacket(byte[] bytes, IPEndPoint EP) { string response = ""; var packetstring = Encoding.UTF8.GetString(PadPacket(bytes)); var redirect = false; var print = true; if ((DateTime.Now - LastRequest).Milliseconds >= 100) { if (packetstring.StartsWith("rcon") || packetstring.Substring(4).StartsWith("rcon") || packetstring.Substring(5).StartsWith("rcon")) { if (!string.IsNullOrEmpty(Password)) { var args = ParseParameters(packetstring); if (args.Count >= 3) { if (args[1] == Password) { args[1] = args[0] = ""; string command = string.Join(" ", args.ToArray()); command = command.TrimEnd(' ').TrimEnd('\0').TrimStart(' '); Log.ConsoleInfo("Rcon from " + EP + ":" + command); Response = ""; response = ExecuteCommand(command); response += "\n" + Response; Response = ""; response = response.TrimStart('\n'); } else { response = "Bad rcon password.\n"; Log.ConsoleInfo("Bad rcon password from " + EP); } } else response = ""; } else { response = "No rcon password set on the server.\n"; Log.Info("No password for rcon set"); } } else redirect = true; } if (packetstring.StartsWith("getinfo") || packetstring.Substring(4).StartsWith("getinfo") || packetstring.Substring(5).StartsWith("getinfo")) { var challenge = ""; if (packetstring.Split(' ').Length == 2) challenge = packetstring.Split(' ')[1]; response = "infoResponse\n"; var infostring = string.Format(@"\_TShock_ver\{6}\mapname\{1}\sv_maxclients\{2}\clients\{3}\sv_privateClients\{4}\hconly\{5}\gamename\TERRARIA\protocol\100\sv_hostname\{0}\g_needPass\{7}", TShock.Config.ServerName, Main.worldName, Main.maxNetPlayers, Tools.ActivePlayers(), Main.maxNetPlayers - TShock.Config.MaxSlots, TShock.Config.HardcoreOnly ? 1 : 0, TShock.VersionNum, Netplay.password != "" ? 1 : 0); if (challenge != "") infostring += @"\challenge\" + challenge; response += infostring; print = false; redirect = false; } else if (packetstring.StartsWith("getstatus") || packetstring.Substring(4).StartsWith("getstatus") || packetstring.Substring(5).StartsWith("getstatus")) { var challenge = ""; if (packetstring.Split(' ').Length == 2) challenge = packetstring.Split(' ')[1]; response = "statusResponse\n"; var statusstring = string.Format(@"\_TShock_ver\{6}\mapname\{1}\sv_maxclients\{2}\clients\{3}\sv_privateClients\{4}\hconly\{5}\gamename\TERRARIA\protocol\100\sv_hostname\{0}\g_needPass\{7}", TShock.Config.ServerName, Main.worldName, Main.maxNetPlayers, Tools.ActivePlayers(), Main.maxNetPlayers - TShock.Config.MaxSlots, TShock.Config.HardcoreOnly ? 1 : 0, TShock.VersionNum, Netplay.password != "" ? 1 : 0) + "\n"; if (challenge != "") statusstring += @"\challenge\" + challenge; foreach (TSPlayer player in TShock.Players) { if (player != null && player.Active) statusstring += (string.Format("0 0 {0}\n", player.Name)); } response += statusstring; print = false; redirect = false; } if (!redirect) return (ConstructPacket(response, print)); else return (ConstructPacket("disconnect", false)); } private static string ExecuteCommand(string text) { if (Main.rand == null) Main.rand = new Random(); if (WorldGen.genRand == null) WorldGen.genRand = new Random(); if (text.StartsWith("exit")) { Tools.ForceKickAll("Server shutting down!"); WorldGen.saveWorld(false); Netplay.disconnect = true; return "Server shutting down."; } else if (text.StartsWith("playing") || text.StartsWith("/playing")) { int count = 0; foreach (TSPlayer player in TShock.Players) { if (player != null && player.Active) { count++; TSPlayer.Server.SendMessage(string.Format("{0} ({1}) [{2}] <{3}>", player.Name, player.IP, player.Group.Name, player.UserAccountName)); } } TSPlayer.Server.SendMessage(string.Format("{0} players connected.", count)); } else if (text.StartsWith("status")) { Response += "map: " + Main.worldName + "\n"; Response += "num score ping name lastmsg address qport rate\n"; int count = 0; foreach (TSPlayer player in TShock.Players) { if (player != null && player.Active) { count++; Response += (string.Format("{0} 0 0 {1}({2}) {3} {4} 0 0", count, player.Name, player.Group.Name, Netplay.serverSock[player.Index].tcpClient.Client.RemoteEndPoint, "")) + "\n"; } } } else if (text.StartsWith("say ")) { Log.Info(string.Format("Server said: {0}", text.Remove(0, 4))); return string.Format("Server said: {0}", text.Remove(0, 4)); } else if (text == "autosave") { Main.autoSave = TShock.Config.AutoSave = !TShock.Config.AutoSave; Log.ConsoleInfo("AutoSave " + (TShock.Config.AutoSave ? "Enabled" : "Disabled")); return "AutoSave " + (TShock.Config.AutoSave ? "Enabled" : "Disabled"); } else if (text.StartsWith("/")) { if (!Commands.HandleCommand(TSPlayer.Server, text)) return "Invalid command."; } else if (!Commands.HandleCommand(TSPlayer.Server, "/" + text)) return "Invalid command."; return ""; } private static byte[] ConstructPacket(string response, bool print) { var oob = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; using (var stream = new MemoryStream()) { stream.WriteBytes(oob); if (print) stream.WriteBytes(Encoding.UTF8.GetBytes(string.Format("print\n{0}", response))); else stream.WriteBytes(Encoding.UTF8.GetBytes(response)); var trimmedpacket = new byte[(int)stream.Length]; var packet = stream.GetBuffer(); Array.Copy(packet, trimmedpacket, (int)stream.Length); return trimmedpacket; } } private static byte[] PadPacket(byte[] packet) { var returnpacket = new byte[(4 + packet.Length)]; int h = 0; if (packet[0] != 0xFF) { for (int i = 0; i < 4; i++) returnpacket[i] = 0xFF; for (int i = 4; i < returnpacket.Length; i++) returnpacket[i] = packet[h++]; } else returnpacket = packet; return returnpacket; } private static void SendHeartbeat() { LastHeartbeat = DateTime.UtcNow.Subtract(new TimeSpan(0, 0, 30)); while (true) { if ((DateTime.UtcNow - LastHeartbeat).Seconds >= 30) { var packet = ConstructPacket("heartbeat TerrariaShock", false); if (listener == null) try { listener = new UdpClient(ListenPort); } catch (SocketException e) { if (e.SocketErrorCode == SocketError.AddressAlreadyInUse) Log.ConsoleError("Could not bind to " + ListenPort + ". Are you sure you don't have another instance running?"); } catch (Exception e) { Log.Error(e.ToString()); } listener.Send(packet, packet.Length, TShock.Config.MasterServer, 27950); LastHeartbeat = DateTime.UtcNow; } Thread.Sleep(10000); } } #region ParseParams private static List ParseParameters(string str) { var ret = new List(); var sb = new StringBuilder(); bool instr = false; for (int i = 0; i < str.Length; i++) { char c = str[i]; if (instr) { if (c == '\\') { if (i + 1 >= str.Length) break; c = GetEscape(str[++i]); } else if (c == '"') { ret.Add(sb.ToString()); sb.Clear(); instr = false; continue; } sb.Append(c); } else { if (IsWhiteSpace(c)) { if (sb.Length > 0) { ret.Add(sb.ToString()); sb.Clear(); } } else if (c == '"') { if (sb.Length > 0) { ret.Add(sb.ToString()); sb.Clear(); } instr = true; } else { sb.Append(c); } } } if (sb.Length > 0) ret.Add(sb.ToString()); return ret; } private static char GetEscape(char c) { switch (c) { case '\\': return '\\'; case '"': return '"'; case 't': return '\t'; default: return c; } } private static bool IsWhiteSpace(char c) { return c == ' ' || c == '\t' || c == '\n'; } #endregion } }