/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors 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 Newtonsoft.Json; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using Terraria; using Terraria.ID; using Terraria.Localization; using TShockAPI.DB; using TerrariaApi.Server; using TShockAPI.Hooks; using Terraria.GameContent.Events; using Microsoft.Xna.Framework; using OTAPI.Tile; using TShockAPI.Localization; using System.Text.RegularExpressions; using Terraria.DataStructures; namespace TShockAPI { public delegate void CommandDelegate(CommandArgs args); public class CommandArgs : EventArgs { public string Message { get; private set; } public TSPlayer Player { get; private set; } public bool Silent { get; private set; } /// /// Parameters passed to the arguement. Does not include the command name. /// IE '/kick "jerk face"' will only have 1 argument /// public List Parameters { get; private set; } public Player TPlayer { get { return Player.TPlayer; } } public CommandArgs(string message, TSPlayer ply, List args) { Message = message; Player = ply; Parameters = args; Silent = false; } public CommandArgs(string message, bool silent, TSPlayer ply, List args) { Message = message; Player = ply; Parameters = args; Silent = silent; } } public class Command { /// /// Gets or sets whether to allow non-players to use this command. /// public bool AllowServer { get; set; } /// /// Gets or sets whether to do logging of this command. /// public bool DoLog { get; set; } /// /// Gets or sets the help text of this command. /// public string HelpText { get; set; } /// /// Gets or sets an extended description of this command. /// public string[] HelpDesc { get; set; } /// /// Gets the name of the command. /// public string Name { get { return Names[0]; } } /// /// Gets the names of the command. /// public List Names { get; protected set; } /// /// Gets the permissions of the command. /// public List Permissions { get; protected set; } private CommandDelegate commandDelegate; public CommandDelegate CommandDelegate { get { return commandDelegate; } set { if (value == null) throw new ArgumentNullException(); commandDelegate = value; } } public Command(List permissions, CommandDelegate cmd, params string[] names) : this(cmd, names) { Permissions = permissions; } public Command(string permissions, CommandDelegate cmd, params string[] names) : this(cmd, names) { Permissions = new List { permissions }; } public Command(CommandDelegate cmd, params string[] names) { if (cmd == null) throw new ArgumentNullException("cmd"); if (names == null || names.Length < 1) throw new ArgumentException("names"); AllowServer = true; CommandDelegate = cmd; DoLog = true; HelpText = "No help available."; HelpDesc = null; Names = new List(names); Permissions = new List(); } public bool Run(string msg, bool silent, TSPlayer ply, List parms) { if (!CanRun(ply)) return false; try { CommandDelegate(new CommandArgs(msg, silent, ply, parms)); } catch (Exception e) { ply.SendErrorMessage("Command failed, check logs for more details."); TShock.Log.Error(e.ToString()); } return true; } public bool Run(string msg, TSPlayer ply, List parms) { return Run(msg, false, ply, parms); } public bool HasAlias(string name) { return Names.Contains(name); } public bool CanRun(TSPlayer ply) { if (Permissions == null || Permissions.Count < 1) return true; foreach (var Permission in Permissions) { if (ply.HasPermission(Permission)) return true; } return false; } } public static class Commands { public static List ChatCommands = new List(); public static ReadOnlyCollection TShockCommands = new ReadOnlyCollection(new List()); /// /// The command specifier, defaults to "/" /// public static string Specifier { get { return string.IsNullOrWhiteSpace(TShock.Config.CommandSpecifier) ? "/" : TShock.Config.CommandSpecifier; } } /// /// The silent command specifier, defaults to "." /// public static string SilentSpecifier { get { return string.IsNullOrWhiteSpace(TShock.Config.CommandSilentSpecifier) ? "." : TShock.Config.CommandSilentSpecifier; } } private delegate void AddChatCommand(string permission, CommandDelegate command, params string[] names); public static void InitCommands() { List tshockCommands = new List(100); Action add = (cmd) => { tshockCommands.Add(cmd); ChatCommands.Add(cmd); }; add(new Command(SetupToken, "setup") { AllowServer = false, HelpText = "Used to authenticate as superadmin when first setting up TShock." }); add(new Command(Permissions.user, ManageUsers, "user") { DoLog = false, HelpText = "Manages user accounts." }); #region Account Commands add(new Command(Permissions.canlogin, AttemptLogin, "login") { AllowServer = false, DoLog = false, HelpText = "Logs you into an account." }); add(new Command(Permissions.canlogout, Logout, "logout") { AllowServer = false, DoLog = false, HelpText = "Logs you out of your current account." }); add(new Command(Permissions.canchangepassword, PasswordUser, "password") { AllowServer = false, DoLog = false, HelpText = "Changes your account's password." }); add(new Command(Permissions.canregister, RegisterUser, "register") { AllowServer = false, DoLog = false, HelpText = "Registers you an account." }); add(new Command(Permissions.checkaccountinfo, ViewAccountInfo, "accountinfo", "ai") { HelpText = "Shows information about a user." }); #endregion #region Admin Commands add(new Command(Permissions.ban, Ban, "ban") { HelpText = "Manages player bans." }); add(new Command(Permissions.broadcast, Broadcast, "broadcast", "bc", "say") { HelpText = "Broadcasts a message to everyone on the server." }); add(new Command(Permissions.logs, DisplayLogs, "displaylogs") { HelpText = "Toggles whether you receive server logs." }); add(new Command(Permissions.managegroup, Group, "group") { HelpText = "Manages groups." }); add(new Command(Permissions.manageitem, ItemBan, "itemban") { HelpText = "Manages item bans." }); add(new Command(Permissions.manageprojectile, ProjectileBan, "projban") { HelpText = "Manages projectile bans." }); add(new Command(Permissions.managetile, TileBan, "tileban") { HelpText = "Manages tile bans." }); add(new Command(Permissions.manageregion, Region, "region") { HelpText = "Manages regions." }); add(new Command(Permissions.kick, Kick, "kick") { HelpText = "Removes a player from the server." }); add(new Command(Permissions.mute, Mute, "mute", "unmute") { HelpText = "Prevents a player from talking." }); add(new Command(Permissions.savessc, OverrideSSC, "overridessc", "ossc") { HelpText = "Overrides serverside characters for a player, temporarily." }); add(new Command(Permissions.savessc, SaveSSC, "savessc") { HelpText = "Saves all serverside characters." }); add(new Command(Permissions.uploaddata, UploadJoinData, "uploadssc") { HelpText = "Upload the account information when you joined the server as your Server Side Character data." }); add(new Command(Permissions.settempgroup, TempGroup, "tempgroup") { HelpText = "Temporarily sets another player's group." }); add(new Command(Permissions.su, SubstituteUser, "su") { HelpText = "Temporarily elevates you to Super Admin." }); add(new Command(Permissions.su, SubstituteUserDo, "sudo") { HelpText = "Executes a command as the super admin." }); add(new Command(Permissions.userinfo, GrabUserUserInfo, "userinfo", "ui") { HelpText = "Shows information about a player." }); #endregion #region Annoy Commands add(new Command(Permissions.annoy, Annoy, "annoy") { HelpText = "Annoys a player for an amount of time." }); add(new Command(Permissions.annoy, Confuse, "confuse") { HelpText = "Confuses a player for an amount of time." }); add(new Command(Permissions.annoy, Rocket, "rocket") { HelpText = "Rockets a player upwards. Requires SSC." }); add(new Command(Permissions.annoy, FireWork, "firework") { HelpText = "Spawns fireworks at a player." }); #endregion #region Configuration Commands add(new Command(Permissions.maintenance, CheckUpdates, "checkupdates") { HelpText = "Checks for TShock updates." }); add(new Command(Permissions.maintenance, Off, "off", "exit", "stop") { HelpText = "Shuts down the server while saving." }); add(new Command(Permissions.maintenance, OffNoSave, "off-nosave", "exit-nosave", "stop-nosave") { HelpText = "Shuts down the server without saving." }); add(new Command(Permissions.cfgreload, Reload, "reload") { HelpText = "Reloads the server configuration file." }); add(new Command(Permissions.cfgpassword, ServerPassword, "serverpassword") { HelpText = "Changes the server password." }); add(new Command(Permissions.maintenance, GetVersion, "version") { HelpText = "Shows the TShock version." }); add(new Command(Permissions.whitelist, Whitelist, "whitelist") { HelpText = "Manages the server whitelist." }); #endregion #region Item Commands add(new Command(Permissions.give, Give, "give", "g") { HelpText = "Gives another player an item." }); add(new Command(Permissions.item, Item, "item", "i") { AllowServer = false, HelpText = "Gives yourself an item." }); #endregion #region NPC Commands add(new Command(Permissions.butcher, Butcher, "butcher") { HelpText = "Kills hostile NPCs or NPCs of a certain type." }); add(new Command(Permissions.renamenpc, RenameNPC, "renamenpc") { HelpText = "Renames an NPC." }); add(new Command(Permissions.invade, Invade, "invade") { HelpText = "Starts an NPC invasion." }); add(new Command(Permissions.maxspawns, MaxSpawns, "maxspawns") { HelpText = "Sets the maximum number of NPCs." }); add(new Command(Permissions.spawnboss, SpawnBoss, "spawnboss", "sb") { AllowServer = false, HelpText = "Spawns a number of bosses around you." }); add(new Command(Permissions.spawnmob, SpawnMob, "spawnmob", "sm") { AllowServer = false, HelpText = "Spawns a number of mobs around you." }); add(new Command(Permissions.spawnrate, SpawnRate, "spawnrate") { HelpText = "Sets the spawn rate of NPCs." }); add(new Command(Permissions.clearangler, ClearAnglerQuests, "clearangler") { HelpText = "Resets the list of users who have completed an angler quest that day." }); #endregion #region TP Commands add(new Command(Permissions.home, Home, "home") { AllowServer = false, HelpText = "Sends you to your spawn point." }); add(new Command(Permissions.spawn, Spawn, "spawn") { AllowServer = false, HelpText = "Sends you to the world's spawn point." }); add(new Command(Permissions.tp, TP, "tp") { AllowServer = false, HelpText = "Teleports a player to another player." }); add(new Command(Permissions.tpothers, TPHere, "tphere") { AllowServer = false, HelpText = "Teleports a player to yourself." }); add(new Command(Permissions.tpnpc, TPNpc, "tpnpc") { AllowServer = false, HelpText = "Teleports you to an npc." }); add(new Command(Permissions.tppos, TPPos, "tppos") { AllowServer = false, HelpText = "Teleports you to tile coordinates." }); add(new Command(Permissions.getpos, GetPos, "pos") { AllowServer = false, HelpText = "Returns the user's or specified user's current position." }); add(new Command(Permissions.tpallow, TPAllow, "tpallow") { AllowServer = false, HelpText = "Toggles whether other people can teleport you." }); #endregion #region World Commands add(new Command(Permissions.toggleexpert, ChangeWorldMode, "worldmode", "gamemode") { HelpText = "Changes the world mode." }); add(new Command(Permissions.antibuild, ToggleAntiBuild, "antibuild") { HelpText = "Toggles build protection." }); add(new Command(Permissions.bloodmoon, Bloodmoon, "tbloodmoon") { HelpText = "Sets a blood moon." }); add(new Command(Permissions.grow, Grow, "grow") { AllowServer = false, HelpText = "Grows plants at your location." }); add(new Command(Permissions.dropmeteor, DropMeteor, "dropmeteor") { HelpText = "Drops a meteor somewhere in the world." }); add(new Command(Permissions.eclipse, Eclipse, "eclipse") { HelpText = "Sets an eclipse." }); add(new Command(Permissions.halloween, ForceHalloween, "forcehalloween") { HelpText = "Toggles halloween mode (goodie bags, pumpkins, etc)." }); add(new Command(Permissions.xmas, ForceXmas, "forcexmas") { HelpText = "Toggles christmas mode (present spawning, santa, etc)." }); add(new Command(Permissions.fullmoon, Fullmoon, "fullmoon") { HelpText = "Sets a full moon." }); add(new Command(Permissions.hardmode, Hardmode, "hardmode") { HelpText = "Toggles the world's hardmode status." }); add(new Command(Permissions.editspawn, ProtectSpawn, "protectspawn") { HelpText = "Toggles spawn protection." }); add(new Command(Permissions.sandstorm, Sandstorm, "sandstorm") { HelpText = "Toggles sandstorms." }); add(new Command(Permissions.rain, Rain, "rain") { HelpText = "Toggles the rain." }); add(new Command(Permissions.worldsave, Save, "save") { HelpText = "Saves the world file." }); add(new Command(Permissions.worldspawn, SetSpawn, "setspawn") { AllowServer = false, HelpText = "Sets the world's spawn point to your location." }); add(new Command(Permissions.dungeonposition, SetDungeon, "setdungeon") { AllowServer = false, HelpText = "Sets the dungeon's position to your location." }); add(new Command(Permissions.worldsettle, Settle, "settle") { HelpText = "Forces all liquids to update immediately." }); add(new Command(Permissions.time, Time, "time") { HelpText = "Sets the world time." }); add(new Command(Permissions.wind, Wind, "wind") { HelpText = "Changes the wind speed." }); add(new Command(Permissions.worldinfo, WorldInfo, "world") { HelpText = "Shows information about the current world." }); #endregion #region Other Commands add(new Command(Permissions.buff, Buff, "buff") { AllowServer = false, HelpText = "Gives yourself a buff for an amount of time." }); add(new Command(Permissions.clear, Clear, "clear") { HelpText = "Clears item drops or projectiles." }); add(new Command(Permissions.buffplayer, GBuff, "gbuff", "buffplayer") { HelpText = "Gives another player a buff for an amount of time." }); add(new Command(Permissions.godmode, ToggleGodMode, "godmode") { HelpText = "Toggles godmode on a player." }); add(new Command(Permissions.heal, Heal, "heal") { HelpText = "Heals a player in HP and MP." }); add(new Command(Permissions.kill, Kill, "kill") { HelpText = "Kills another player." }); add(new Command(Permissions.cantalkinthird, ThirdPerson, "me") { HelpText = "Sends an action message to everyone." }); add(new Command(Permissions.canpartychat, PartyChat, "party", "p") { AllowServer = false, HelpText = "Sends a message to everyone on your team." }); add(new Command(Permissions.whisper, Reply, "reply", "r") { HelpText = "Replies to a PM sent to you." }); add(new Command(Rests.RestPermissions.restmanage, ManageRest, "rest") { HelpText = "Manages the REST API." }); add(new Command(Permissions.slap, Slap, "slap") { HelpText = "Slaps a player, dealing damage." }); add(new Command(Permissions.serverinfo, ServerInfo, "serverinfo") { HelpText = "Shows the server information." }); add(new Command(Permissions.warp, Warp, "warp") { HelpText = "Teleports you to a warp point or manages warps." }); add(new Command(Permissions.whisper, Whisper, "whisper", "w", "tell") { HelpText = "Sends a PM to a player." }); add(new Command(Permissions.createdumps, CreateDumps, "dump-reference-data") { HelpText = "Creates a reference tables for Terraria data types and the TShock permission system in the server folder." }); #endregion add(new Command(Aliases, "aliases") { HelpText = "Shows a command's aliases." }); add(new Command(Help, "help") { HelpText = "Lists commands or gives help on them." }); add(new Command(Motd, "motd") { HelpText = "Shows the message of the day." }); add(new Command(ListConnectedPlayers, "playing", "online", "who") { HelpText = "Shows the currently connected players." }); add(new Command(Rules, "rules") { HelpText = "Shows the server's rules." }); TShockCommands = new ReadOnlyCollection(tshockCommands); } public static bool HandleCommand(TSPlayer player, string text) { string cmdText = text.Remove(0, 1); string cmdPrefix = text[0].ToString(); bool silent = false; if (cmdPrefix == SilentSpecifier) silent = true; int index = -1; for (int i = 0; i < cmdText.Length; i++) { if (IsWhiteSpace(cmdText[i])) { index = i; break; } } string cmdName; if (index == 0) // Space after the command specifier should not be supported { player.SendErrorMessage("Invalid command entered. Type {0}help for a list of valid commands.", Specifier); return true; } else if (index < 0) cmdName = cmdText.ToLower(); else cmdName = cmdText.Substring(0, index).ToLower(); List args; if (index < 0) args = new List(); else args = ParseParameters(cmdText.Substring(index)); IEnumerable cmds = ChatCommands.FindAll(c => c.HasAlias(cmdName)); if (Hooks.PlayerHooks.OnPlayerCommand(player, cmdName, cmdText, args, ref cmds, cmdPrefix)) return true; if (cmds.Count() == 0) { if (player.AwaitingResponse.ContainsKey(cmdName)) { Action call = player.AwaitingResponse[cmdName]; player.AwaitingResponse.Remove(cmdName); call(new CommandArgs(cmdText, player, args)); return true; } player.SendErrorMessage("Invalid command entered. Type {0}help for a list of valid commands.", Specifier); return true; } foreach (Command cmd in cmds) { if (!cmd.CanRun(player)) { TShock.Utils.SendLogs(string.Format("{0} tried to execute {1}{2}.", player.Name, Specifier, cmdText), Color.PaleVioletRed, player); player.SendErrorMessage("You do not have access to this command."); if (player.HasPermission(Permissions.su)) { player.SendInfoMessage("You can use '{0}sudo {0}{1}' to override this check.", Specifier, cmdText); } } else if (!cmd.AllowServer && !player.RealPlayer) { player.SendErrorMessage("You must use this command in-game."); } else { if (cmd.DoLog) TShock.Utils.SendLogs(string.Format("{0} executed: {1}{2}.", player.Name, silent ? SilentSpecifier : Specifier, cmdText), Color.PaleVioletRed, player); cmd.Run(cmdText, silent, player, args); } } return true; } /// /// Parses a string of parameters into a list. Handles quotes. /// /// /// 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 (c == '\\' && ++i < str.Length) { if (str[i] != '"' && str[i] != ' ' && str[i] != '\\') sb.Append('\\'); sb.Append(str[i]); } else if (c == '"') { instr = !instr; if (!instr) { ret.Add(sb.ToString()); sb.Clear(); } else if (sb.Length > 0) { ret.Add(sb.ToString()); sb.Clear(); } } else if (IsWhiteSpace(c) && !instr) { if (sb.Length > 0) { ret.Add(sb.ToString()); sb.Clear(); } } else sb.Append(c); } if (sb.Length > 0) ret.Add(sb.ToString()); return ret; } private static bool IsWhiteSpace(char c) { return c == ' ' || c == '\t' || c == '\n'; } #region Account commands private static void AttemptLogin(CommandArgs args) { if (args.Player.LoginAttempts > TShock.Config.MaximumLoginAttempts && (TShock.Config.MaximumLoginAttempts != -1)) { TShock.Log.Warn(String.Format("{0} ({1}) had {2} or more invalid login attempts and was kicked automatically.", args.Player.IP, args.Player.Name, TShock.Config.MaximumLoginAttempts)); args.Player.Kick("Too many invalid login attempts."); return; } if (args.Player.IsLoggedIn) { args.Player.SendErrorMessage("You are already logged in, and cannot login again."); return; } UserAccount account = TShock.UserAccounts.GetUserAccountByName(args.Player.Name); string password = ""; bool usingUUID = false; if (args.Parameters.Count == 0 && !TShock.Config.DisableUUIDLogin) { if (PlayerHooks.OnPlayerPreLogin(args.Player, args.Player.Name, "")) return; usingUUID = true; } else if (args.Parameters.Count == 1) { if (PlayerHooks.OnPlayerPreLogin(args.Player, args.Player.Name, args.Parameters[0])) return; password = args.Parameters[0]; } else if (args.Parameters.Count == 2 && TShock.Config.AllowLoginAnyUsername) { if (String.IsNullOrEmpty(args.Parameters[0])) { args.Player.SendErrorMessage("Bad login attempt."); return; } if (PlayerHooks.OnPlayerPreLogin(args.Player, args.Parameters[0], args.Parameters[1])) return; account = TShock.UserAccounts.GetUserAccountByName(args.Parameters[0]); password = args.Parameters[1]; } else { args.Player.SendErrorMessage("Syntax: {0}login - Logs in using your UUID and character name", Specifier); args.Player.SendErrorMessage(" {0}login - Logs in using your password and character name", Specifier); args.Player.SendErrorMessage(" {0}login - Logs in using your username and password", Specifier); args.Player.SendErrorMessage("If you forgot your password, there is no way to recover it."); return; } try { if (account == null) { args.Player.SendErrorMessage("A user account by that name does not exist."); } else if (account.VerifyPassword(password) || (usingUUID && account.UUID == args.Player.UUID && !TShock.Config.DisableUUIDLogin && !String.IsNullOrWhiteSpace(args.Player.UUID))) { args.Player.PlayerData = TShock.CharacterDB.GetPlayerData(args.Player, account.ID); var group = TShock.Groups.GetGroupByName(account.Group); args.Player.Group = group; args.Player.tempGroup = null; args.Player.Account = account; args.Player.IsLoggedIn = true; args.Player.IsDisabledForSSC = false; if (Main.ServerSideCharacter) { if (args.Player.HasPermission(Permissions.bypassssc)) { args.Player.PlayerData.CopyCharacter(args.Player); TShock.CharacterDB.InsertPlayerData(args.Player); } args.Player.PlayerData.RestoreCharacter(args.Player); } args.Player.LoginFailsBySsi = false; if (args.Player.HasPermission(Permissions.ignorestackhackdetection)) args.Player.IsDisabledForStackDetection = false; if (args.Player.HasPermission(Permissions.usebanneditem)) args.Player.IsDisabledForBannedWearable = false; args.Player.SendSuccessMessage("Authenticated as " + account.Name + " successfully."); TShock.Log.ConsoleInfo(args.Player.Name + " authenticated successfully as user: " + account.Name + "."); if ((args.Player.LoginHarassed) && (TShock.Config.RememberLeavePos)) { if (TShock.RememberedPos.GetLeavePos(args.Player.Name, args.Player.IP) != Vector2.Zero) { Vector2 pos = TShock.RememberedPos.GetLeavePos(args.Player.Name, args.Player.IP); args.Player.Teleport((int)pos.X * 16, (int)pos.Y * 16); } args.Player.LoginHarassed = false; } TShock.UserAccounts.SetUserAccountUUID(account, args.Player.UUID); Hooks.PlayerHooks.OnPlayerPostLogin(args.Player); } else { if (usingUUID && !TShock.Config.DisableUUIDLogin) { args.Player.SendErrorMessage("UUID does not match this character!"); } else { args.Player.SendErrorMessage("Invalid password!"); } TShock.Log.Warn(args.Player.IP + " failed to authenticate as user: " + account.Name + "."); args.Player.LoginAttempts++; } } catch (Exception ex) { args.Player.SendErrorMessage("There was an error processing your request."); TShock.Log.Error(ex.ToString()); } } private static void Logout(CommandArgs args) { if (!args.Player.IsLoggedIn) { args.Player.SendErrorMessage("You are not logged in."); return; } args.Player.Logout(); args.Player.SendSuccessMessage("You have been successfully logged out of your account."); if (Main.ServerSideCharacter) { args.Player.SendWarningMessage("Server side characters are enabled. You need to be logged in to play."); } } private static void PasswordUser(CommandArgs args) { try { if (args.Player.IsLoggedIn && args.Parameters.Count == 2) { string password = args.Parameters[0]; if (args.Player.Account.VerifyPassword(password)) { try { args.Player.SendSuccessMessage("You changed your password!"); TShock.UserAccounts.SetUserAccountPassword(args.Player.Account, args.Parameters[1]); // SetUserPassword will hash it for you. TShock.Log.ConsoleInfo(args.Player.IP + " named " + args.Player.Name + " changed the password of account " + args.Player.Account.Name + "."); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); } } else { args.Player.SendErrorMessage("You failed to change your password!"); TShock.Log.ConsoleError(args.Player.IP + " named " + args.Player.Name + " failed to change password for account: " + args.Player.Account.Name + "."); } } else { args.Player.SendErrorMessage("Not logged in or invalid syntax! Proper syntax: {0}password ", Specifier); } } catch (UserAccountManagerException ex) { args.Player.SendErrorMessage("Sorry, an error occured: " + ex.Message + "."); TShock.Log.ConsoleError("PasswordUser returned an error: " + ex); } } private static void RegisterUser(CommandArgs args) { try { var account = new UserAccount(); string echoPassword = ""; if (args.Parameters.Count == 1) { account.Name = args.Player.Name; echoPassword = args.Parameters[0]; try { account.CreateBCryptHash(args.Parameters[0]); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); return; } } else if (args.Parameters.Count == 2 && TShock.Config.AllowRegisterAnyUsername) { account.Name = args.Parameters[0]; echoPassword = args.Parameters[1]; try { account.CreateBCryptHash(args.Parameters[1]); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); return; } } else { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}register ", Specifier); return; } account.Group = TShock.Config.DefaultRegistrationGroupName; // FIXME -- we should get this from the DB. --Why? account.UUID = args.Player.UUID; if (TShock.UserAccounts.GetUserAccountByName(account.Name) == null && account.Name != TSServerPlayer.AccountName) // Cheap way of checking for existance of a user { args.Player.SendSuccessMessage("Account \"{0}\" has been registered.", account.Name); args.Player.SendSuccessMessage("Your password is {0}.", echoPassword); TShock.UserAccounts.AddUserAccount(account); TShock.Log.ConsoleInfo("{0} registered an account: \"{1}\".", args.Player.Name, account.Name); } else { args.Player.SendErrorMessage("Sorry, " + account.Name + " was already taken by another person."); args.Player.SendErrorMessage("Please try a different username."); TShock.Log.ConsoleInfo(args.Player.Name + " failed to register an existing account: " + account.Name); } } catch (UserAccountManagerException ex) { args.Player.SendErrorMessage("Sorry, an error occured: " + ex.Message + "."); TShock.Log.ConsoleError("RegisterUser returned an error: " + ex); } } private static void ManageUsers(CommandArgs args) { // This guy needs to be here so that people don't get exceptions when they type /user if (args.Parameters.Count < 1) { args.Player.SendErrorMessage("Invalid user syntax. Try {0}user help.", Specifier); return; } string subcmd = args.Parameters[0]; // Add requires a username, password, and a group specified. if (subcmd == "add" && args.Parameters.Count == 4) { var account = new UserAccount(); account.Name = args.Parameters[1]; try { account.CreateBCryptHash(args.Parameters[2]); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); return; } account.Group = args.Parameters[3]; try { TShock.UserAccounts.AddUserAccount(account); args.Player.SendSuccessMessage("Account " + account.Name + " has been added to group " + account.Group + "!"); TShock.Log.ConsoleInfo(args.Player.Name + " added Account " + account.Name + " to group " + account.Group); } catch (GroupNotExistsException) { args.Player.SendErrorMessage("Group " + account.Group + " does not exist!"); } catch (UserAccountExistsException) { args.Player.SendErrorMessage("User " + account.Name + " already exists!"); } catch (UserAccountManagerException e) { args.Player.SendErrorMessage("User " + account.Name + " could not be added, check console for details."); TShock.Log.ConsoleError(e.ToString()); } } // User deletion requires a username else if (subcmd == "del" && args.Parameters.Count == 2) { var account = new UserAccount(); account.Name = args.Parameters[1]; try { TShock.UserAccounts.RemoveUserAccount(account); args.Player.SendSuccessMessage("Account removed successfully."); TShock.Log.ConsoleInfo(args.Player.Name + " successfully deleted account: " + args.Parameters[1] + "."); } catch (UserAccountNotExistException) { args.Player.SendErrorMessage("The user " + account.Name + " does not exist! Deleted nobody!"); } catch (UserAccountManagerException ex) { args.Player.SendErrorMessage(ex.Message); TShock.Log.ConsoleError(ex.ToString()); } } // Password changing requires a username, and a new password to set else if (subcmd == "password" && args.Parameters.Count == 3) { var account = new UserAccount(); account.Name = args.Parameters[1]; try { TShock.UserAccounts.SetUserAccountPassword(account, args.Parameters[2]); TShock.Log.ConsoleInfo(args.Player.Name + " changed the password of account " + account.Name); args.Player.SendSuccessMessage("Password change succeeded for " + account.Name + "."); } catch (UserAccountNotExistException) { args.Player.SendErrorMessage("User " + account.Name + " does not exist!"); } catch (UserAccountManagerException e) { args.Player.SendErrorMessage("Password change for " + account.Name + " failed! Check console!"); TShock.Log.ConsoleError(e.ToString()); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); } } // Group changing requires a username or IP address, and a new group to set else if (subcmd == "group" && args.Parameters.Count == 3) { var account = new UserAccount(); account.Name = args.Parameters[1]; try { TShock.UserAccounts.SetUserGroup(account, args.Parameters[2]); TShock.Log.ConsoleInfo(args.Player.Name + " changed account " + account.Name + " to group " + args.Parameters[2] + "."); args.Player.SendSuccessMessage("Account " + account.Name + " has been changed to group " + args.Parameters[2] + "!"); } catch (GroupNotExistsException) { args.Player.SendErrorMessage("That group does not exist!"); } catch (UserAccountNotExistException) { args.Player.SendErrorMessage("User " + account.Name + " does not exist!"); } catch (UserAccountManagerException e) { args.Player.SendErrorMessage("User " + account.Name + " could not be added. Check console for details."); TShock.Log.ConsoleError(e.ToString()); } } else if (subcmd == "help") { args.Player.SendInfoMessage("Use command help:"); args.Player.SendInfoMessage("{0}user add username password group -- Adds a specified user", Specifier); args.Player.SendInfoMessage("{0}user del username -- Removes a specified user", Specifier); args.Player.SendInfoMessage("{0}user password username newpassword -- Changes a user's password", Specifier); args.Player.SendInfoMessage("{0}user group username newgroup -- Changes a user's group", Specifier); } else { args.Player.SendErrorMessage("Invalid user syntax. Try {0}user help.", Specifier); } } #endregion #region Stupid commands private static void ServerInfo(CommandArgs args) { args.Player.SendInfoMessage("Memory usage: " + Process.GetCurrentProcess().WorkingSet64); args.Player.SendInfoMessage("Allocated memory: " + Process.GetCurrentProcess().VirtualMemorySize64); args.Player.SendInfoMessage("Total processor time: " + Process.GetCurrentProcess().TotalProcessorTime); args.Player.SendInfoMessage("WinVer: " + Environment.OSVersion); args.Player.SendInfoMessage("Proc count: " + Environment.ProcessorCount); args.Player.SendInfoMessage("Machine name: " + Environment.MachineName); } private static void WorldInfo(CommandArgs args) { args.Player.SendInfoMessage("World name: " + (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)); args.Player.SendInfoMessage("World size: {0}x{1}", Main.maxTilesX, Main.maxTilesY); args.Player.SendInfoMessage("World ID: " + Main.worldID); } #endregion #region Player Management Commands private static void GrabUserUserInfo(CommandArgs args) { if (args.Parameters.Count < 1) { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}userinfo ", Specifier); return; } var players = TSPlayer.FindByNameOrID(args.Parameters[0]); if (players.Count < 1) args.Player.SendErrorMessage("Invalid player."); else if (players.Count > 1) args.Player.SendMultipleMatchError(players.Select(p => p.Name)); else { var message = new StringBuilder(); message.Append("IP Address: ").Append(players[0].IP); if (players[0].Account != null && players[0].IsLoggedIn) message.Append(" | Logged in as: ").Append(players[0].Account.Name).Append(" | Group: ").Append(players[0].Group.Name); args.Player.SendSuccessMessage(message.ToString()); } } private static void ViewAccountInfo(CommandArgs args) { if (args.Parameters.Count < 1) { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}accountinfo ", Specifier); return; } string username = String.Join(" ", args.Parameters); if (!string.IsNullOrWhiteSpace(username)) { var account = TShock.UserAccounts.GetUserAccountByName(username); if (account != null) { DateTime LastSeen; string Timezone = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now).Hours.ToString("+#;-#"); if (DateTime.TryParse(account.LastAccessed, out LastSeen)) { LastSeen = DateTime.Parse(account.LastAccessed).ToLocalTime(); args.Player.SendSuccessMessage("{0}'s last login occured {1} {2} UTC{3}.", account.Name, LastSeen.ToShortDateString(), LastSeen.ToShortTimeString(), Timezone); } if (args.Player.Group.HasPermission(Permissions.advaccountinfo)) { List KnownIps = JsonConvert.DeserializeObject>(account.KnownIps?.ToString() ?? string.Empty); string ip = KnownIps?[KnownIps.Count - 1] ?? "N/A"; DateTime Registered = DateTime.Parse(account.Registered).ToLocalTime(); args.Player.SendSuccessMessage("{0}'s group is {1}.", account.Name, account.Group); args.Player.SendSuccessMessage("{0}'s last known IP is {1}.", account.Name, ip); args.Player.SendSuccessMessage("{0}'s register date is {1} {2} UTC{3}.", account.Name, Registered.ToShortDateString(), Registered.ToShortTimeString(), Timezone); } } else args.Player.SendErrorMessage("User {0} does not exist.", username); } else args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}accountinfo ", Specifier); } private static void Kick(CommandArgs args) { if (args.Parameters.Count < 1) { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}kick [reason]", Specifier); return; } if (args.Parameters[0].Length == 0) { args.Player.SendErrorMessage("Missing player name."); return; } string plStr = args.Parameters[0]; var players = TSPlayer.FindByNameOrID(plStr); if (players.Count == 0) { args.Player.SendErrorMessage("Invalid player!"); } else if (players.Count > 1) { args.Player.SendMultipleMatchError(players.Select(p => p.Name)); } else { string reason = args.Parameters.Count > 1 ? String.Join(" ", args.Parameters.GetRange(1, args.Parameters.Count - 1)) : "Misbehaviour."; if (!players[0].Kick(reason, !args.Player.RealPlayer, false, args.Player.Name)) { args.Player.SendErrorMessage("You can't kick another admin!"); } } } private static void Ban(CommandArgs args) { string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower(); switch (subcmd) { case "add": #region Add Ban { if (args.Parameters.Count < 2) { args.Player.SendErrorMessage("Invalid command. Format: {0}ban add [time] [reason]", Specifier); args.Player.SendErrorMessage("Example: {0}ban add Shank 10d Hacking and cheating", Specifier); args.Player.SendErrorMessage("Example: {0}ban add Ash", Specifier); args.Player.SendErrorMessage("Use the time 0 (zero) for a permanent ban."); return; } // Used only to notify if a ban was successful and who the ban was about bool success = false; string targetGeneralizedName = ""; // Effective ban target assignment List players = TSPlayer.FindByNameOrID(args.Parameters[1]); // Bad case: Players contains more than 1 person so we can't ban them if (players.Count > 1) { //Fail fast args.Player.SendMultipleMatchError(players.Select(p => p.Name)); return; } UserAccount offlineUserAccount = TShock.UserAccounts.GetUserAccountByName(args.Parameters[1]); // Storage variable to determine if the command executor is the server console // If it is, we assume they have full control and let them override permission checks bool callerIsServerConsole = args.Player is TSServerPlayer; // The ban reason the ban is going to have string banReason = "Unknown."; // The default ban length // 0 is permanent ban, otherwise temp ban int banLengthInSeconds = 0; // Figure out if param 2 is a time or 0 or garbage if (args.Parameters.Count >= 3) { bool parsedOkay = false; if (args.Parameters[2] != "0") { parsedOkay = TShock.Utils.TryParseTime(args.Parameters[2], out banLengthInSeconds); } else { parsedOkay = true; } if (!parsedOkay) { args.Player.SendErrorMessage("Invalid time format. Example: 10d 5h 3m 2s."); args.Player.SendErrorMessage("Use 0 (zero) for a permanent ban."); return; } } // If a reason exists, use the given reason. if (args.Parameters.Count > 3) { banReason = String.Join(" ", args.Parameters.Skip(3)); } // Good case: Online ban for matching character. if (players.Count == 1) { TSPlayer target = players[0]; if (target.HasPermission(Permissions.immunetoban) && !callerIsServerConsole) { args.Player.SendErrorMessage("Permission denied. Target {0} is immune to ban.", target.Name); return; } targetGeneralizedName = target.Name; success = TShock.Bans.AddBan(target.IP, target.Name, target.UUID, target.Account?.Name ?? "", banReason, false, args.Player.Account.Name, banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s")); // Since this is an online ban, we need to dc the player and tell them now. if (success) { if (banLengthInSeconds == 0) { target.Disconnect(String.Format("Permanently banned for {0}", banReason)); } else { target.Disconnect(String.Format("Banned for {0} seconds for {1}", banLengthInSeconds, banReason)); } } } // Case: Players & user are invalid, could be IP? // Note: Order matters. If this method is above the online player check, // This enables you to ban an IP even if the player exists in the database as a player. // You'll get two bans for the price of one, in theory, because both IP and user named IP will be banned. // ??? edge cases are weird, but this is going to happen // The only way around this is to either segregate off the IP code or do something else. if (players.Count == 0) { // If the target is a valid IP... string pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; Regex r = new Regex(pattern, RegexOptions.IgnoreCase); if (r.IsMatch(args.Parameters[1])) { targetGeneralizedName = "IP: " + args.Parameters[1]; success = TShock.Bans.AddBan(args.Parameters[1], "", "", "", banReason, false, args.Player.Account.Name, banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s")); if (success && offlineUserAccount != null) { args.Player.SendSuccessMessage("Target IP {0} was banned successfully.", targetGeneralizedName); args.Player.SendErrorMessage("Note: An account named with this IP address also exists."); args.Player.SendErrorMessage("Note: It will also be banned."); } } else { // Apparently there is no way to not IP ban someone // This means that where we would normally just ban a "character name" here // We can't because it requires some IP as a primary key. if (offlineUserAccount == null) { args.Player.SendErrorMessage("Unable to ban target {0}.", args.Parameters[1]); args.Player.SendErrorMessage("Target is not a valid IP address, a valid online player, or a known offline user."); return; } } } // Case: Offline ban if (players.Count == 0 && offlineUserAccount != null) { // Catch: we don't know an offline player's last login character name // This means that we're banning their *user name* on the assumption that // user name == character name // (which may not be true) // This needs to be fixed in a future implementation. targetGeneralizedName = offlineUserAccount.Name; if (TShock.Groups.GetGroupByName(offlineUserAccount.Group).HasPermission(Permissions.immunetoban) && !callerIsServerConsole) { args.Player.SendErrorMessage("Permission denied. Target {0} is immune to ban.", targetGeneralizedName); return; } if (offlineUserAccount.KnownIps == null) { args.Player.SendErrorMessage("Unable to ban target {0} because they have no valid IP to ban.", targetGeneralizedName); return; } string lastIP = JsonConvert.DeserializeObject>(offlineUserAccount.KnownIps).Last(); success = TShock.Bans.AddBan(lastIP, "", offlineUserAccount.UUID, offlineUserAccount.Name, banReason, false, args.Player.Account.Name, banLengthInSeconds == 0 ? "" : DateTime.UtcNow.AddSeconds(banLengthInSeconds).ToString("s")); } if (success) { args.Player.SendSuccessMessage("{0} was successfully banned.", targetGeneralizedName); args.Player.SendInfoMessage("Length: {0}", banLengthInSeconds == 0 ? "Permanent." : banLengthInSeconds + " seconds."); args.Player.SendInfoMessage("Reason: {0}", banReason); if (!args.Silent) { if (banLengthInSeconds == 0) { TSPlayer.All.SendErrorMessage("{0} was permanently banned by {1} for: {2}", targetGeneralizedName, args.Player.Account.Name, banReason); } else { TSPlayer.All.SendErrorMessage("{0} was temp banned for {1} seconds by {2} for: {3}", targetGeneralizedName, banLengthInSeconds, args.Player.Account.Name, banReason); } } } else { args.Player.SendErrorMessage("{0} was NOT banned due to a database error or other system problem.", targetGeneralizedName); args.Player.SendErrorMessage("If this player is online, they have NOT been kicked."); args.Player.SendErrorMessage("Check the system logs for details."); } return; } #endregion case "del": #region Delete ban { if (args.Parameters.Count != 2) { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban del ", Specifier); return; } string plStr = args.Parameters[1]; Ban ban = TShock.Bans.GetBanByName(plStr, false); if (ban != null) { if (TShock.Bans.RemoveBan(ban.Name, true)) args.Player.SendSuccessMessage("Unbanned {0} ({1}).", ban.Name, ban.IP); else args.Player.SendErrorMessage("Failed to unban {0} ({1}), check logs.", ban.Name, ban.IP); } else args.Player.SendErrorMessage("No bans for {0} exist.", plStr); } #endregion return; case "delip": #region Delete IP ban { if (args.Parameters.Count != 2) { args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban delip ", Specifier); return; } string ip = args.Parameters[1]; Ban ban = TShock.Bans.GetBanByIp(ip); if (ban != null) { if (TShock.Bans.RemoveBan(ban.IP, false)) args.Player.SendSuccessMessage("Unbanned IP {0} ({1}).", ban.IP, ban.Name); else args.Player.SendErrorMessage("Failed to unban IP {0} ({1}), check logs.", ban.IP, ban.Name); } else args.Player.SendErrorMessage("IP {0} is not banned.", ip); } #endregion return; case "help": #region Help { int pageNumber; if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber)) return; var lines = new List { "add