/* 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; using System.Collections.Generic; using System.Linq; using HttpServer; using Rests; using Terraria; using TShockAPI.DB; namespace TShockAPI { public class RestManager { private Rest Rest; public RestManager(Rest rest) { Rest = rest; } public void RegisterRestfulCommands() { // Server Commands Rest.Register(new RestCommand("/v2/server/broadcast", ServerBroadcast)); Rest.Register(new RestCommand("/v2/server/off", ServerOff)); Rest.Register(new RestCommand("/v2/server/rawcmd", ServerCommand)); Rest.Register(new RestCommand("/v2/server/status", ServerStatusV2) { RequiresToken = false }); Rest.Register(new RestCommand("/tokentest", ServerTokenTest)); Rest.Register(new RestCommand("/status", ServerStatus) { RequiresToken = false }); // User Commands Rest.Register(new RestCommand("/v2/users/activelist", UserActiveListV2)); Rest.Register(new RestCommand("/v2/users/create", UserCreateV2)); Rest.Register(new RestCommand("/v2/users/list", UserListV2)); Rest.Register(new RestCommand("/v2/users/read", UserInfoV2)); Rest.Register(new RestCommand("/v2/users/destroy", UserDestroyV2)); Rest.Register(new RestCommand("/v2/users/update", UserUpdateV2)); // Ban Commands Rest.Register(new RestCommand("/bans/create", BanCreate)); Rest.Register(new RestCommand("/v2/bans/list", BanListV2)); Rest.Register(new RestCommand("/v2/bans/read", BanInfoV2)); Rest.Register(new RestCommand("/v2/bans/destroy", BanDestroyV2)); // World Commands Rest.Register(new RestCommand("/world/read", WorldRead)); Rest.Register(new RestCommand("/world/meteor", WorldMeteor)); Rest.Register(new RestCommand("/world/bloodmoon/{bool}", WorldBloodmoon)); Rest.Register(new RestCommand("/v2/world/save", WorldSave)); Rest.Register(new RestCommand("/v2/world/autosave/state/{bool}", WorldChangeSaveSettings)); Rest.Register(new RestCommand("/v2/world/butcher", WorldButcher)); // Player Commands Rest.Register(new RestCommand("/lists/players", PlayerList)); Rest.Register(new RestCommand("/v2/players/list", PlayerListV2)); Rest.Register(new RestCommand("/v2/players/read", PlayerReadV2)); Rest.Register(new RestCommand("/v2/players/kick", PlayerKickV2)); Rest.Register(new RestCommand("/v2/players/ban", PlayerBanV2)); Rest.Register(new RestCommand("/v2/players/kill", PlayerKill)); Rest.Register(new RestCommand("/v2/players/mute", PlayerMute)); Rest.Register(new RestCommand("/v2/players/unmute", PlayerUnMute)); // Group Commands Rest.Register(new RestCommand("/v2/groups/list", GroupList)); Rest.Register(new RestCommand("/v2/groups/read", GroupInfo)); Rest.Register(new RestCommand("/v2/groups/destroy", GroupDestroy)); Rest.Register(new RestCommand("/v2/groups/create", GroupCreate)); Rest.Register(new RestCommand("/v2/groups/update", GroupUpdate)); } #region RestServerMethods private object ServerCommand(RestVerbs verbs, IParameterCollection parameters) { if (string.IsNullOrWhiteSpace(parameters["cmd"])) return RestMissingParam("cmd"); TSRestPlayer tr = new TSRestPlayer(); Commands.HandleCommand(tr, parameters["cmd"]); return RestResponse(string.Join("\n", tr.GetCommandOutput())); } private object ServerOff(RestVerbs verbs, IParameterCollection parameters) { if (!GetBool(parameters["confirm"], false)) return RestInvalidParam("confirm"); if (!GetBool(parameters["nosave"], false)) WorldGen.saveWorld(); Netplay.disconnect = true; // Inform players the server is shutting down var msg = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; foreach (TSPlayer player in TShock.Players.Where(p => null != p)) { TShock.Utils.ForceKick(player, msg); } return RestResponse("The server is shutting down"); } private object ServerBroadcast(RestVerbs verbs, IParameterCollection parameters) { var msg = parameters["msg"]; if (string.IsNullOrWhiteSpace(msg)) return RestMissingParam("msg"); TShock.Utils.Broadcast(msg); return RestResponse("The message was broadcasted successfully"); } private object ServerStatus(RestVerbs verbs, IParameterCollection parameters) { if (TShock.Config.EnableTokenEndpointAuthentication) return RestError("Server settings require a token for this API call"); var activeplayers = Main.player.Where(p => null != p && p.active).ToList(); return new RestObject() { {"name", TShock.Config.ServerNickname}, {"port", Convert.ToString(TShock.Config.ServerPort)}, {"playercount", Convert.ToString(activeplayers.Count())}, {"players", string.Join(", ", activeplayers.Select(p => p.name))}, }; } private object ServerStatusV2(RestVerbs verbs, IParameterCollection parameters) { if (TShock.Config.EnableTokenEndpointAuthentication) return RestError("Server settings require a token for this API call"); var ret = new RestObject() { {"name", TShock.Config.ServerNickname}, {"port", TShock.Config.ServerPort}, {"playercount", Main.player.Where(p => null != p && p.active).Count()}, {"maxplayers", TShock.Config.MaxSlots}, {"world", Main.worldName} }; if (GetBool(parameters["players"], false)) { var players = new ArrayList(); foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) { var p = PlayerFilter(tsPlayer, parameters); if (null != p) players.Add(p); } ret.Add("players", players); } if (GetBool(parameters["rules"], false)) { var rules = new Dictionary(); rules.Add("AutoSave", TShock.Config.AutoSave); rules.Add("DisableBuild", TShock.Config.DisableBuild); rules.Add("DisableClownBombs", TShock.Config.DisableClownBombs); rules.Add("DisableDungeonGuardian", TShock.Config.DisableDungeonGuardian); rules.Add("DisableInvisPvP", TShock.Config.DisableInvisPvP); rules.Add("DisableSnowBalls", TShock.Config.DisableSnowBalls); rules.Add("DisableTombstones", TShock.Config.DisableTombstones); rules.Add("EnableWhitelist", TShock.Config.EnableWhitelist); rules.Add("HardcoreOnly", TShock.Config.HardcoreOnly); rules.Add("PvPMode", TShock.Config.PvPMode); rules.Add("SpawnProtection", TShock.Config.SpawnProtection); rules.Add("SpawnProtectionRadius", TShock.Config.SpawnProtectionRadius); ret.Add("rules", rules); } return ret; } private object ServerTokenTest(RestVerbs verbs, IParameterCollection parameters) { return RestResponse("Token is valid and was passed through correctly"); } #endregion #region RestUserMethods private object UserActiveListV2(RestVerbs verbs, IParameterCollection parameters) { return new RestObject() { { "activeusers", string.Join("\t", TShock.Players.Where(p => null != p && null != p.UserAccountName && p.Active).Select(p => p.UserAccountName)) } }; } private object UserListV2(RestVerbs verbs, IParameterCollection parameters) { return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary(){ {"name", p.Name}, {"id", p.ID}, {"group", p.Group}, {"ip", p.Address}, }) } }; } private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters) { var username = parameters["user"]; if (string.IsNullOrWhiteSpace(username)) return RestMissingParam("user"); var group = parameters["group"]; if (string.IsNullOrWhiteSpace(group)) return RestMissingParam("group"); var password = parameters["password"]; if (string.IsNullOrWhiteSpace(password)) return RestMissingParam("password"); // NOTE: ip can be blank User user = new User(parameters["ip"], username, password, group); try { TShock.Users.AddUser(user); } catch (Exception e) { return RestError(e.Message); } return RestResponse("User was successfully created"); } private object UserUpdateV2(RestVerbs verbs, IParameterCollection parameters) { var ret = UserFind(parameters); if (ret is RestObject) return ret; var password = parameters["password"]; var group = parameters["group"]; if (string.IsNullOrWhiteSpace(group) && string.IsNullOrWhiteSpace(password)) return RestMissingParam("group", "password"); User user = (User)ret; var response = new RestObject(); if (!string.IsNullOrWhiteSpace(password)) { try { TShock.Users.SetUserPassword(user, password); response.Add("password-response", "Password updated successfully"); } catch (Exception e) { return RestError("Failed to update user password (" + e.Message + ")"); } } if (!string.IsNullOrWhiteSpace(group)) { try { TShock.Users.SetUserGroup(user, group); response.Add("group-response", "Group updated successfully"); } catch (Exception e) { return RestError("Failed to update user group (" + e.Message + ")"); } } return response; } private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters) { var ret = UserFind(parameters); if (ret is RestObject) return ret; try { TShock.Users.RemoveUser((User)ret); } catch (Exception e) { return RestError(e.Message); } return RestResponse("User deleted successfully"); } private object UserInfoV2(RestVerbs verbs, IParameterCollection parameters) { var ret = UserFind(parameters); if (ret is RestObject) return ret; User user = (User)ret; return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() } }; } #endregion #region RestBanMethods private object BanCreate(RestVerbs verbs, IParameterCollection parameters) { var ip = parameters["ip"]; var name = parameters["name"]; if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name)) return RestMissingParam("ip", "name"); try { TShock.Bans.AddBan(ip, name, parameters["reason"], true); } catch (Exception e) { return RestError(e.Message); } return RestResponse("Ban created successfully"); } private object BanDestroyV2(RestVerbs verbs, IParameterCollection parameters) { var ret = BanFind(parameters); if (ret is RestObject) return ret; try { Ban ban = (Ban)ret; switch (parameters["type"]) { case "ip": if (!TShock.Bans.RemoveBan(ban.IP, false, false, true)) return RestResponse("Failed to delete ban (already deleted?)"); break; case "name": if (!TShock.Bans.RemoveBan(ban.Name, true, GetBool(parameters["caseinsensitive"], true))) return RestResponse("Failed to delete ban (already deleted?)"); break; default: return RestError("Invalid Type: '" + parameters["type"] + "'"); } } catch (Exception e) { return RestError(e.Message); } return RestResponse("Ban deleted successfully"); } private object BanInfoV2(RestVerbs verbs, IParameterCollection parameters) { var ret = BanFind(parameters); if (ret is RestObject) return ret; Ban ban = (Ban)ret; return new RestObject() { {"name", null == ban.Name ? "" : ban.Name}, {"ip", null == ban.IP ? "" : ban.IP}, {"reason", null == ban.Reason ? "" : ban.Reason}, }; } private object BanListV2(RestVerbs verbs, IParameterCollection parameters) { var banList = new ArrayList(); foreach (var ban in TShock.Bans.GetBans()) { banList.Add( new Dictionary { {"name", null == ban.Name ? "" : ban.Name}, {"ip", null == ban.IP ? "" : ban.IP}, {"reason", null == ban.Reason ? "" : ban.Reason}, } ); } return new RestObject() { { "bans", banList } }; } #endregion #region RestWorldMethods private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters) { bool autoSave; if (!bool.TryParse(verbs["bool"], out autoSave)) return RestInvalidParam("state"); TShock.Config.AutoSave = autoSave; return RestResponse("AutoSave has been set to " + autoSave); } private object WorldSave(RestVerbs verbs, IParameterCollection parameters) { TShock.Utils.SaveWorld(); return RestResponse("World saved"); } private object WorldButcher(RestVerbs verbs, IParameterCollection parameters) { bool killFriendly; if (!bool.TryParse(parameters["killfriendly"], out killFriendly)) return RestInvalidParam("killfriendly"); if (killFriendly) killFriendly = !killFriendly; int killcount = 0; for (int i = 0; i < Main.npc.Length; i++) { if (Main.npc[i].active && Main.npc[i].type != 0 && !Main.npc[i].townNPC && (!Main.npc[i].friendly || killFriendly)) { TSPlayer.Server.StrikeNPC(i, 99999, 90f, 1); killcount++; } } return RestResponse(killcount + " NPCs have been killed"); } private object WorldRead(RestVerbs verbs, IParameterCollection parameters) { return new RestObject() { {"name", Main.worldName}, {"size", Main.maxTilesX + "*" + Main.maxTilesY}, {"time", Main.time}, {"daytime", Main.dayTime}, {"bloodmoon", Main.bloodMoon}, {"invasionsize", Main.invasionSize} }; } private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters) { if (null == WorldGen.genRand) WorldGen.genRand = new Random(); WorldGen.dropMeteor(); return RestResponse("Meteor has been spawned"); } private object WorldBloodmoon(RestVerbs verbs, IParameterCollection parameters) { bool bloodmoon; if (!bool.TryParse(verbs["bool"], out bloodmoon)) return RestInvalidParam("bloodmoon"); Main.bloodMoon = bloodmoon; return RestResponse("Blood Moon has been set to " + bloodmoon); } #endregion #region RestPlayerMethods private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters) { return PlayerSetMute(parameters, false); } private object PlayerMute(RestVerbs verbs, IParameterCollection parameters) { return PlayerSetMute(parameters, true); } private object PlayerList(RestVerbs verbs, IParameterCollection parameters) { var activeplayers = Main.player.Where(p => null != p && p.active).ToList(); return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } }; } private object PlayerListV2(RestVerbs verbs, IParameterCollection parameters) { var playerList = new ArrayList(); foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) { var p = PlayerFilter(tsPlayer, parameters); if (null != p) playerList.Add(p); } return new RestObject() { { "players", playerList } }; } private object PlayerReadV2(RestVerbs verbs, IParameterCollection parameters) { var ret = PlayerFind(parameters); if (ret is RestObject) return ret; TSPlayer player = (TSPlayer)ret; var activeItems = player.TPlayer.inventory.Where(p => p.active).ToList(); return new RestObject() { {"nickname", player.Name}, {"username", null == player.UserAccountName ? "" : player.UserAccountName}, {"ip", player.IP}, {"group", player.Group.Name}, {"position", player.TileX + "," + player.TileY}, {"inventory", string.Join(", ", activeItems.Select(p => (p.name + ":" + p.stack)))}, {"buffs", string.Join(", ", player.TPlayer.buffType)} }; } private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters) { var ret = PlayerFind(parameters); if (ret is RestObject) return ret; TSPlayer player = (TSPlayer)ret; TShock.Utils.ForceKick(player, null == parameters["reason"] ? "Kicked via web" : parameters["reason"]); return RestResponse("Player " + player.Name + " was kicked"); } private object PlayerBanV2(RestVerbs verbs, IParameterCollection parameters) { var ret = PlayerFind(parameters); if (ret is RestObject) return ret; TSPlayer player = (TSPlayer)ret; var reason = null == parameters["reason"] ? "Banned via web" : parameters["reason"]; TShock.Bans.AddBan(player.IP, player.Name, reason); TShock.Utils.ForceKick(player, reason); return RestResponse("Player " + player.Name + " was banned"); } private object PlayerKill(RestVerbs verbs, IParameterCollection parameters) { var ret = PlayerFind(parameters); if (ret is RestObject) return ret; TSPlayer player = (TSPlayer)ret; player.DamagePlayer(999999); var from = string.IsNullOrWhiteSpace(parameters["from"]) ? "Server Admin" : parameters["from"]; player.SendMessage(string.Format("{0} just killed you!", from)); return RestResponse("Player " + player.Name + " was killed"); } #endregion #region RestGroupMethods private object GroupList(RestVerbs verbs, IParameterCollection parameters) { var groups = new ArrayList(); foreach (Group group in TShock.Groups) { groups.Add(new Dictionary {{"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}}); } return new RestObject() { { "groups", groups } }; } private object GroupInfo(RestVerbs verbs, IParameterCollection parameters) { var ret = GroupFind(parameters); if (ret is RestObject) return ret; Group group = (Group)ret; return new RestObject() { {"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}, {"permissions", group.permissions}, {"negatedpermissions", group.negatedpermissions} }; } private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters) { var ret = GroupFind(parameters); if (ret is RestObject) return ret; Group group = (Group)ret; try { TShock.Groups.DeleteGroup(group.Name, true); } catch (Exception e) { return RestError(e.Message); } return RestResponse("Group '" + group.Name + "' deleted successfully"); } private object GroupCreate(RestVerbs verbs, IParameterCollection parameters) { var name = parameters["group"]; if (string.IsNullOrWhiteSpace(name)) return RestMissingParam("group"); try { TShock.Groups.AddGroup(name, parameters["parent"], parameters["permissions"], parameters["chatcolor"], true); } catch (Exception e) { return RestError(e.Message); } return RestResponse("Group '" + name + "' created successfully"); } private object GroupUpdate(RestVerbs verbs, IParameterCollection parameters) { var ret = GroupFind(parameters); if (ret is RestObject) return ret; Group group = (Group)ret; var parent = (null == parameters["parent"]) ? group.ParentName : parameters["parent"]; var chatcolor = (null == parameters["chatcolor"]) ? group.ChatColor : parameters["chatcolor"]; var permissions = (null == parameters["permissions"]) ? group.Permissions : parameters["permissions"]; try { TShock.Groups.UpdateGroup(group.Name, parent, permissions, chatcolor); } catch (Exception e) { return RestError(e.Message); } return RestResponse("Group '" + group.Name + "' updated successfully"); } #endregion #region Utility Methods private RestObject RestError(string message, string status = "400") { return new RestObject(status) {Error = message}; } private RestObject RestResponse(string message, string status = "200") { return new RestObject(status) {Response = message}; } private RestObject RestMissingParam(string var) { return RestError("Missing or empty " + var + " parameter"); } private RestObject RestMissingParam(params string[] vars) { return RestMissingParam(string.Join(", ", vars)); } private RestObject RestInvalidParam(string var) { return RestError("Missing or invalid " + var + " parameter"); } private bool GetBool(string val, bool def) { bool ret; return bool.TryParse(val, out ret) ? ret : def; } private object PlayerFind(IParameterCollection parameters) { string name = parameters["player"]; if (string.IsNullOrWhiteSpace(name)) return RestMissingParam("player"); var found = TShock.Utils.FindPlayer(name); switch(found.Count) { case 1: return found[0]; case 0: return RestError("Player " + name + " was not found"); default: return RestError("Player " + name + " matches " + found.Count + " players"); } } private object UserFind(IParameterCollection parameters) { string name = parameters["user"]; if (string.IsNullOrWhiteSpace(name)) return RestMissingParam("user"); User user; string type = parameters["type"]; try { switch (type) { case null: case "name": type = "name"; user = TShock.Users.GetUserByName(name); break; case "id": user = TShock.Users.GetUserByID(Convert.ToInt32(name)); break; case "ip": user = TShock.Users.GetUserByIP(name); break; default: return RestError("Invalid Type: '" + type + "'"); } } catch (Exception e) { return RestError(e.Message); } if (null == user) return RestError(String.Format("User {0} '{1}' doesn't exist", type, name)); return user; } private object BanFind(IParameterCollection parameters) { string name = parameters["ban"]; if (string.IsNullOrWhiteSpace(name)) return RestMissingParam("ban"); string type = parameters["type"]; if (string.IsNullOrWhiteSpace(type)) return RestMissingParam("type"); Ban ban; switch (type) { case "ip": ban = TShock.Bans.GetBanByIp(name); break; case "name": ban = TShock.Bans.GetBanByName(name, GetBool(parameters["caseinsensitive"], true)); break; default: return RestError("Invalid Type: '" + type + "'"); } if (null == ban) return RestError("Ban " + type + " '" + name + "' doesn't exist"); return ban; } private object GroupFind(IParameterCollection parameters) { var name = parameters["group"]; if (string.IsNullOrWhiteSpace(name)) return RestMissingParam("group"); var group = TShock.Groups.GetGroupByName(name); if (null == group) return RestError("Group '" + name + "' doesn't exist"); return group; } private Dictionary PlayerFilter(TSPlayer tsPlayer, IParameterCollection parameters) { var player = new Dictionary { {"nickname", tsPlayer.Name}, {"username", null == tsPlayer.UserAccountName ? "" : tsPlayer.UserAccountName}, {"ip", tsPlayer.IP}, {"group", tsPlayer.Group.Name}, {"active", tsPlayer.Active}, {"state", tsPlayer.State}, {"team", tsPlayer.Team}, }; foreach (IParameter filter in parameters) { if (player.ContainsKey(filter.Name) && !player[filter.Name].Equals(filter.Value)) return null; } return player; } private object PlayerSetMute(IParameterCollection parameters, bool mute) { var ret = PlayerFind(parameters); if (ret is RestObject) return ret; TSPlayer player = (TSPlayer)ret; player.mute = mute; var verb = mute ? "muted" : "unmuted"; player.SendMessage("You have been remotely " + verb); return RestResponse("Player " + player.Name + " was " + verb); } #endregion } }