/* TShock, a server mod for Terraria Copyright (C) 2011-2015 Nyx Studios (fka. 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.Linq; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Terraria; using TShockAPI.DB; using BCrypt.Net; namespace TShockAPI { /// /// Utilities and other TShock core calls that don't fit anywhere else /// public class Utils { /// /// The lowest id for a prefix. /// private const int FirstItemPrefix = 1; /// /// The highest id for a prefix. /// private const int LastItemPrefix = 83; // Utils is a Singleton private static readonly Utils instance = new Utils(); private Utils() {} public static Utils Instance { get { return instance; } } public Random Random = new Random(); //private static List groups = new List(); /// /// Provides the real IP address from a RemoteEndPoint string that contains a port and an IP /// /// A string IPv4 address in IP:PORT form. /// A string IPv4 address. public string GetRealIP(string mess) { return mess.Split(':')[0]; } /// /// Returns a list of current players on the server /// /// bool includeIDs - whether or not the string of each player name should include ID data /// List of strings with names public List GetPlayers(bool includeIDs) { var players = new List(); foreach (TSPlayer ply in TShock.Players) { if (ply != null && ply.Active) { if (includeIDs) { players.Add(ply.Name + " (IX: " + ply.Index + ", ID: " + ply.UserID + ")"); } else { players.Add(ply.Name); } } } return players; } /// /// Finds a player and gets IP as string /// /// string playername public string GetPlayerIP(string playername) { foreach (TSPlayer player in TShock.Players) { if (player != null && player.Active) { if (playername.ToLower() == player.Name.ToLower()) { return player.IP; } } } return null; } /// /// It's a clamp function /// /// /// Value to clamp /// Maximum bounds of the clamp /// Minimum bounds of the clamp /// public T Clamp(T value, T max, T min) where T : IComparable { T result = value; if (value.CompareTo(max) > 0) result = max; if (value.CompareTo(min) < 0) result = min; return result; } /// /// Saves the map data /// public void SaveWorld() { SaveManager.Instance.SaveWorld(); } public void Broadcast(string msg, byte red, byte green, byte blue) { TSPlayer.All.SendMessage(msg, red, green, blue); TSPlayer.Server.SendMessage(msg, red, green, blue); TShock.Log.Info(string.Format("Broadcast: {0}", msg)); } public void Broadcast(string msg, Color color) { Broadcast(msg, color.R, color.G, color.B); } /// /// Broadcasts a message from a player, not TShock /// /// TSPlayer ply - the player that will send the packet /// string msg - the message /// r /// g /// b public void Broadcast(int ply, string msg, byte red, byte green, byte blue) { TSPlayer.All.SendMessageFromPlayer(msg, red, green, blue, ply); TSPlayer.Server.SendMessage(Main.player[ply].name + ": " + msg, red, green, blue); TShock.Log.Info(string.Format("Broadcast: {0}", Main.player[ply].name + ": " + msg)); } /// /// Sends message to all players with 'logs' permission. /// /// Message to send /// Color of the message /// The player to not send the message to. public void SendLogs(string log, Color color, TSPlayer excludedPlayer = null) { TShock.Log.Info(log); TSPlayer.Server.SendMessage(log, color); foreach (TSPlayer player in TShock.Players) { if (player != null && player != excludedPlayer && player.Active && player.Group.HasPermission(Permissions.logs) && player.DisplayLogs && TShock.Config.DisableSpewLogs == false) player.SendMessage(log, color); } } /// /// The number of active players on the server. /// /// int playerCount public int ActivePlayers() { return Main.player.Where(p => null != p && p.active).Count(); } /// /// Finds a TSPlayer based on name or ID /// /// Player name or ID /// public List FindPlayer(string plr) { var found = new List(); // Avoid errors caused by null search if (plr == null) return found; byte plrID; if (byte.TryParse(plr, out plrID) && plrID < Main.maxPlayers) { TSPlayer player = TShock.Players[plrID]; if (player != null && player.Active) { return new List { player }; } } string plrLower = plr.ToLower(); foreach (TSPlayer player in TShock.Players) { if (player != null) { // Must be an EXACT match if (player.Name == plr) return new List { player }; if (player.Name.ToLower().StartsWith(plrLower)) found.Add(player); } } return found; } /// /// Gets a random clear tile in range /// /// Bound X /// Bound Y /// Range on the X axis /// Range on the Y axis /// X location /// Y location public void GetRandomClearTileWithInRange(int startTileX, int startTileY, int tileXRange, int tileYRange, out int tileX, out int tileY) { int j = 0; do { if (j == 100) { tileX = startTileX; tileY = startTileY; break; } tileX = startTileX + Random.Next(tileXRange*-1, tileXRange); tileY = startTileY + Random.Next(tileYRange*-1, tileYRange); j++; } while (TilePlacementValid(tileX, tileY) && TileSolid(tileX, tileY)); } /// /// Determines if a tile is valid. /// /// Location X /// Location Y /// If the tile is valid public bool TilePlacementValid(int tileX, int tileY) { return tileX >= 0 && tileX < Main.maxTilesX && tileY >= 0 && tileY < Main.maxTilesY; } /// /// Checks if the tile is solid. /// /// Location X /// Location Y /// The tile's solidity. public bool TileSolid(int tileX, int tileY) { return TilePlacementValid(tileX, tileY) && Main.tile[tileX, tileY] != null && Main.tile[tileX, tileY].active() && Main.tileSolid[Main.tile[tileX, tileY].type] && !Main.tile[tileX, tileY].inActive() && !Main.tile[tileX, tileY].halfBrick() && Main.tile[tileX, tileY].slope() == 0; } /// /// Gets a list of items by ID or name /// /// Item ID or name /// List of Items public List GetItemByIdOrName(string idOrName) { int type = -1; if (int.TryParse(idOrName, out type)) { if (type >= Main.maxItemTypes) return new List(); return new List {GetItemById(type)}; } return GetItemByName(idOrName); } /// /// Gets an item by ID /// /// ID /// Item public Item GetItemById(int id) { Item item = new Item(); item.netDefaults(id); return item; } /// /// Gets items by name /// /// name /// List of Items public List GetItemByName(string name) { var found = new List(); Item item = new Item(); string nameLower = name.ToLower(); for (int i = -48; i < Main.maxItemTypes; i++) { item.netDefaults(i); if (item.name.ToLower() == nameLower) return new List {item}; if (item.name.ToLower().StartsWith(nameLower)) found.Add((Item)item.Clone()); } return found; } /// /// Gets an NPC by ID or Name /// /// /// List of NPCs public List GetNPCByIdOrName(string idOrName) { int type = -1; if (int.TryParse(idOrName, out type)) { if (type >= Main.maxNPCTypes) return new List(); return new List { GetNPCById(type) }; } return GetNPCByName(idOrName); } /// /// Gets an NPC by ID /// /// ID /// NPC public NPC GetNPCById(int id) { NPC npc = new NPC(); npc.netDefaults(id); return npc; } /// /// Gets a NPC by name /// /// Name /// List of matching NPCs public List GetNPCByName(string name) { var found = new List(); NPC npc = new NPC(); string nameLower = name.ToLower(); for (int i = -17; i < Main.maxNPCTypes; i++) { npc.netDefaults(i); if (npc.name.ToLower() == nameLower) return new List { npc }; if (npc.name.ToLower().StartsWith(nameLower)) found.Add((NPC)npc.Clone()); } return found; } /// /// Gets a buff name by id /// /// ID /// name public string GetBuffName(int id) { return (id > 0 && id < Main.maxBuffTypes) ? Main.buffName[id] : "null"; } /// /// Gets the description of a buff /// /// ID /// description public string GetBuffDescription(int id) { return (id > 0 && id < Main.maxBuffTypes) ? Main.buffTip[id] : "null"; } /// /// Gets a list of buffs by name /// /// name /// Matching list of buff ids public List GetBuffByName(string name) { string nameLower = name.ToLower(); for (int i = 1; i < Main.maxBuffTypes; i++) { if (Main.buffName[i].ToLower() == nameLower) return new List {i}; } var found = new List(); for (int i = 1; i < Main.maxBuffTypes; i++) { if (Main.buffName[i].ToLower().StartsWith(nameLower)) found.Add(i); } return found; } /// /// Gets a prefix based on its id /// /// ID /// Prefix name public string GetPrefixById(int id) { return id < FirstItemPrefix || id > LastItemPrefix ? "" : Lang.prefix[id] ?? ""; } /// /// Gets a list of prefixes by name /// /// Name /// List of prefix IDs public List GetPrefixByName(string name) { Item item = new Item(); item.SetDefaults(0); string lowerName = name.ToLower(); var found = new List(); for (int i = FirstItemPrefix; i <= LastItemPrefix; i++) { item.prefix = (byte)i; string prefixName = item.AffixName().Trim().ToLower(); if (prefixName == lowerName) return new List() { i }; else if (prefixName.StartsWith(lowerName)) // Partial match found.Add(i); } return found; } /// /// Gets a prefix by ID or name /// /// ID or name /// List of prefix IDs public List GetPrefixByIdOrName(string idOrName) { int type = -1; if (int.TryParse(idOrName, out type) && type >= FirstItemPrefix && type <= LastItemPrefix) { return new List {type}; } return GetPrefixByName(idOrName); } /// /// Kicks all player from the server without checking for immunetokick permission. /// /// string reason public void ForceKickAll(string reason) { foreach (TSPlayer player in TShock.Players) { if (player != null && player.Active) { ForceKick(player, reason, false, true); } } } /// /// Stops the server after kicking all players with a reason message, and optionally saving the world /// /// bool perform a world save before stop (default: true) /// string reason (default: "Server shutting down!") public void StopServer(bool save = true, string reason = "Server shutting down!") { ForceKickAll(reason); if (save) SaveManager.Instance.SaveWorld(); // Save takes a while so kick again ForceKickAll(reason); // Broadcast so console can see we are shutting down as well TShock.Utils.Broadcast(reason, Color.Red); // Disconnect after kick as that signifies server is exiting and could cause a race Netplay.disconnect = true; } /// /// Stops the server after kicking all players with a reason message, and optionally saving the world then attempts to /// restart it. /// /// bool perform a world save before stop (default: true) /// string reason (default: "Server shutting down!") public void RestartServer(bool save = true, string reason = "Server shutting down!") { if (Main.ServerSideCharacter) foreach (TSPlayer player in TShock.Players) if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan) TShock.CharacterDB.InsertPlayerData(player); StopServer(true, reason); System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase); Environment.Exit(0); } /// /// Reloads all configuration settings, groups, regions and raises the reload event. /// public void Reload(TSPlayer player) { FileTools.SetupConfig(); TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs()); TShock.Groups.LoadPermisions(); TShock.Regions.Reload(); TShock.Itembans.UpdateItemBans(); Hooks.GeneralHooks.OnReloadEvent(player); } /// /// Kicks a player from the server without checking for immunetokick permission. /// /// TSPlayer player /// string reason /// bool silent (default: false) /// bool saveSSI (default: false) public void ForceKick(TSPlayer player, string reason, bool silent = false, bool saveSSI = false) { Kick(player, reason, true, silent, null, saveSSI); } /// /// Kicks a player from the server.. /// /// TSPlayer player /// string reason /// bool force (default: false) /// bool silent (default: false) /// string adminUserName (default: null) /// bool saveSSI (default: false) public bool Kick(TSPlayer player, string reason, bool force = false, bool silent = false, string adminUserName = null, bool saveSSI = false) { if (!player.ConnectionAlive) return true; if (force || !player.Group.HasPermission(Permissions.immunetokick)) { string playerName = player.Name; player.SilentKickInProgress = silent; if (player.IsLoggedIn && saveSSI) player.SaveServerCharacter(); player.Disconnect(string.Format("Kicked: {0}", reason)); TShock.Log.ConsoleInfo(string.Format("Kicked {0} for : '{1}'", playerName, reason)); string verb = force ? "force " : ""; if (!silent) { if (string.IsNullOrWhiteSpace(adminUserName)) Broadcast(string.Format("{0} was {1}kicked for '{2}'", playerName, verb, reason.ToLower()), Color.Green); else Broadcast(string.Format("{0} {1}kicked {2} for '{3}'", adminUserName, verb, playerName, reason.ToLower()), Color.Green); } return true; } return false; } /// /// Bans and kicks a player from the server. /// /// TSPlayer player /// string reason /// bool force (default: false) /// string adminUserName (default: null) public bool Ban(TSPlayer player, string reason, bool force = false, string adminUserName = null) { if (!player.ConnectionAlive) return true; if (force || !player.Group.HasPermission(Permissions.immunetoban)) { string ip = player.IP; string uuid = player.UUID; string playerName = player.Name; TShock.Bans.AddBan(ip, playerName, uuid, reason, false, adminUserName); player.Disconnect(string.Format("Banned: {0}", reason)); string verb = force ? "force " : ""; if (string.IsNullOrWhiteSpace(adminUserName)) TSPlayer.All.SendInfoMessage("{0} was {1}banned for '{2}'.", playerName, verb, reason); else TSPlayer.All.SendInfoMessage("{0} {1}banned {2} for '{3}'.", adminUserName, verb, playerName, reason); return true; } return false; } public bool HasBanExpired(Ban ban, bool byName = false) { DateTime exp; bool expirationExists = DateTime.TryParse(ban.Expiration, out exp); if (!string.IsNullOrWhiteSpace(ban.Expiration) && (expirationExists) && (DateTime.UtcNow >= exp)) { if (byName) { TShock.Bans.RemoveBan(ban.Name, true, true, false); } else { TShock.Bans.RemoveBan(ban.IP, false, false, false); } return true; } return false; } /// /// Shows a file to the user. /// /// TSPlayer player /// string filename reletave to savedir public void ShowFileToUser(TSPlayer player, string file) { string foo = ""; using (var tr = new StreamReader(Path.Combine(TShock.SavePath, file))) { while ((foo = tr.ReadLine()) != null) { if (string.IsNullOrWhiteSpace(foo)) { continue; } foo = foo.Replace("%map%", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)); foo = foo.Replace("%players%", String.Join(",", GetPlayers(false))); Regex reg = new Regex("%\\s*(?\\d{1,3})\\s*,\\s*(?\\d{1,3})\\s*,\\s*(?\\d{1,3})\\s*%"); var matches = reg.Matches(foo); Color c = Color.White; foreach (Match match in matches) { byte r, g, b; if (byte.TryParse(match.Groups["r"].Value, out r) && byte.TryParse(match.Groups["g"].Value, out g) && byte.TryParse(match.Groups["b"].Value, out b)) { c = new Color(r, g, b); } foo = foo.Remove(match.Index, match.Length); } player.SendMessage(foo, c); } } } /// /// Returns a Group from the name of the group /// /// string groupName public Group GetGroup(string groupName) { //first attempt on cached groups for (int i = 0; i < TShock.Groups.groups.Count; i++) { if (TShock.Groups.groups[i].Name.Equals(groupName)) { return TShock.Groups.groups[i]; } } return Group.DefaultGroup; } /// /// Returns an IPv4 address from a DNS query /// /// string ip public string GetIPv4Address(string hostname) { try { //Get the ipv4 address from GetHostAddresses, if an ip is passed it will return that ip var ip = Dns.GetHostAddresses(hostname).FirstOrDefault(i => i.AddressFamily == AddressFamily.InterNetwork); //if the dns query was successful then return it, otherwise return an empty string return ip != null ? ip.ToString() : ""; } catch (SocketException) { } return ""; } /// /// Sends the player an error message stating that more than one match was found /// appending a csv list of the matches. /// /// Player to send the message to /// An enumerable list with the matches public void SendMultipleMatchError(TSPlayer ply, IEnumerable matches) { ply.SendErrorMessage("More than one match found: {0}", string.Join(",", matches)); ply.SendErrorMessage("Use \"my query\" for items with spaces"); } /// /// Default hashing algorithm. /// [Obsolete("This is no longer necessary, please use TShock.Config.HashAlgorithm instead if you really need it (but use User.VerifyPassword(password)) for verifying passwords.")] public string HashAlgo = "sha512"; /// /// A dictionary of hashing algortihms and an implementation object. /// [Obsolete("This is no longer necessary, after switching to User.VerifyPassword(password) instead.")] public readonly Dictionary> HashTypes = new Dictionary> { {"sha512", () => new SHA512Managed()}, {"sha256", () => new SHA256Managed()}, {"md5", () => new MD5Cng()}, {"sha512-xp", () => SHA512.Create()}, {"sha256-xp", () => SHA256.Create()}, {"md5-xp", () => MD5.Create()}, }; /// /// Returns a Sha256 string for a given string /// /// bytes to hash /// string sha256 [Obsolete("Please use User.VerifyPassword(password) instead. Warning: This will upgrade passwords to BCrypt. Already converted passwords will not hash correctly using this method.")] public string HashPassword(byte[] bytes) { if (bytes == null) throw new NullReferenceException("bytes"); Func func; if (!HashTypes.TryGetValue(HashAlgo.ToLower(), out func)) throw new NotSupportedException("Hashing algorithm {0} is not supported".SFormat(HashAlgo.ToLower())); using (var hash = func()) { var ret = hash.ComputeHash(bytes); return ret.Aggregate("", (s, b) => s + b.ToString("X2")); } } /// /// Returns a Sha256 string for a given string /// /// string to hash /// string sha256 [Obsolete("Please use User.VerifyPassword(password) instead. Warning: This will upgrade passwords to BCrypt. Already converted passwords will not hash correctly using this method.")] public string HashPassword(string password) { if (string.IsNullOrEmpty(password) || password == "non-existant password") return "non-existant password"; return HashPassword(Encoding.UTF8.GetBytes(password)); } /// /// Checks if the string contains any unprintable characters /// /// String to check /// True if the string only contains printable characters public bool ValidString(string str) { foreach (var c in str) { if (c < 0x20 || c > 0xA9) return false; } return true; } /// /// Checks if world has hit the max number of chests /// /// True if the entire chest array is used public bool MaxChests() { for (int i = 0; i < Main.chest.Length; i++) { if (Main.chest[i] == null) return false; } return true; } /// /// Attempts to parse a string as a timespan (_d_m_h_s). /// /// The time string. /// The seconds. /// Whether the string was parsed successfully. public bool TryParseTime(string str, out int seconds) { seconds = 0; var sb = new StringBuilder(3); for (int i = 0; i < str.Length; i++) { if (Char.IsDigit(str[i]) || (str[i] == '-' || str[i] == '+')) sb.Append(str[i]); else { int num; if (!int.TryParse(sb.ToString(), out num)) return false; sb.Clear(); switch (str[i]) { case 's': seconds += num; break; case 'm': seconds += num * 60; break; case 'h': seconds += num * 60 * 60; break; case 'd': seconds += num * 60 * 60 * 24; break; default: return false; } } } if (sb.Length != 0) return false; return true; } /// /// Searches for a projectile by identity and owner /// /// identity /// owner /// projectile ID public int SearchProjectile(short identity, int owner) { for (int i = 0; i < Main.maxProjectiles; i++) { if (Main.projectile[i].identity == identity && Main.projectile[i].owner == owner) return i; } return 1000; } /// /// Sanitizes input strings /// /// string /// sanitized string public string SanitizeString(string str) { var returnstr = str.ToCharArray(); for (int i = 0; i < str.Length; i++) { if (!ValidString(str[i].ToString())) returnstr[i] = ' '; } return new string(returnstr); } /// /// Enumerates boundary points of the given region's rectangle. /// /// The region's area to enumerate through. /// The enumerated boundary points. public IEnumerable EnumerateRegionBoundaries(Rectangle regionArea) { for (int x = 0; x < regionArea.Width + 1; x++) { yield return new Point(regionArea.Left + x, regionArea.Top); yield return new Point(regionArea.Left + x, regionArea.Bottom); } for (int y = 1; y < regionArea.Height; y++) { yield return new Point(regionArea.Left, regionArea.Top + y); yield return new Point(regionArea.Right, regionArea.Top + y); } } public int? EncodeColor(Color? color) { if (color == null) return null; return BitConverter.ToInt32(new[] { color.Value.R, color.Value.G, color.Value.B, color.Value.A }, 0); } public Color? DecodeColor(int? encodedColor) { if (encodedColor == null) return null; byte[] data = BitConverter.GetBytes(encodedColor.Value); return new Color(data[0], data[1], data[2], data[3]); } public byte? EncodeBitsByte(BitsByte? bitsByte) { if (bitsByte == null) return null; byte result = 0; for (int i = 0; i < 8; i++) if (bitsByte.Value[i]) result |= (byte)(1 << i); return result; } public BitsByte? DecodeBitsByte(int? encodedBitsByte) { if (encodedBitsByte == null) return null; BitsByte result = new BitsByte(); for (int i = 0; i < 8; i++) result[i] = (encodedBitsByte & 1 << i) != 0; return result; } public HttpWebResponse GetResponseNoException(HttpWebRequest req) { try { return (HttpWebResponse)req.GetResponse(); } catch (WebException we) { var resp = we.Response as HttpWebResponse; if (resp == null) throw; return resp; } } } }