TShock/TShockAPI/Rest/RestManager.cs
George 09a8f95a70 Added rate limiting to RESTful API
Added token bucket and timer to degrade token bucket

Modified REST API rate limiting

Changed limiting to only be on token create and v2 token create
Added config options to choose time limits
Passed HttpContext to the execute method of endpoints

Made blocking failed API logins optional

Changed error codes on failed login to be ambiguous
Added config to decide whether all or failed logins are limited

Changed config variable names

Cleaned up duplicate code in REST rate limiting

Fixed my typo

Changed error 431 to 403
2015-09-24 15:47:46 +01:00

1251 lines
42 KiB
C#

/*
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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using HttpServer;
using Rests;
using Terraria;
using TShockAPI.DB;
namespace TShockAPI
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class Permission : Attribute
{
public string Name { get; set; }
public Permission(string name)
{
Name = name;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class RouteAttribute : Attribute
{
public string Route { get; set; }
public RouteAttribute(string route)
{
Route = route;
}
}
public class ParameterAttribute : Attribute
{
public string Name { get; set; }
public bool Required { get; set; }
public string Description { get; set; }
public Type ArgumentType { get; set; }
public ParameterAttribute(string name, bool req, string desc, Type type)
{
Name = name;
Required = req;
Description = desc;
ArgumentType = type;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class Noun : ParameterAttribute
{
public Noun(string name, bool req, string desc, Type type) : base(name, req, desc, type) { }
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class Verb : ParameterAttribute
{
public Verb(string name, string desc, Type type) : base(name, true, desc, type) { }
}
[AttributeUsage(AttributeTargets.Method)]
public class Token : Noun
{
public Token() : base("token", true, "The REST authentication token.", typeof(String)){}
}
public class RestManager
{
private Rest Rest;
public RestManager(Rest rest)
{
Rest = rest;
}
public void RegisterRestfulCommands()
{
// Server Commands
if (TShock.Config.EnableTokenEndpointAuthentication)
{
Rest.Register(new SecureRestCommand("/v2/server/status", ServerStatusV2));
Rest.Register(new SecureRestCommand("/status", ServerStatus));
Rest.Register(new SecureRestCommand("/v3/server/motd", ServerMotd));
Rest.Register(new SecureRestCommand("/v3/server/rules", ServerRules));
}
else
{
Rest.Register(new RestCommand("/v2/server/status", (a) => this.ServerStatusV2(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
Rest.Register(new RestCommand("/status", (a) => this.ServerStatus(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
Rest.Register(new RestCommand("/v3/server/motd", (a) => this.ServerMotd(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
Rest.Register(new RestCommand("/v3/server/rules", (a) => this.ServerRules(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context))));
}
Rest.Register(new SecureRestCommand("/v2/server/broadcast", ServerBroadcast));
Rest.Register(new SecureRestCommand("/v3/server/reload", ServerReload, RestPermissions.restcfg));
Rest.Register(new SecureRestCommand("/v2/server/off", ServerOff, RestPermissions.restmaintenance));
Rest.Register(new SecureRestCommand("/v3/server/restart", ServerRestart, RestPermissions.restmaintenance));
Rest.Register(new SecureRestCommand("/v2/server/rawcmd", ServerCommand, RestPermissions.restrawcommand));
Rest.Register(new SecureRestCommand("/v3/server/rawcmd", ServerCommandV3, RestPermissions.restrawcommand));
Rest.Register(new SecureRestCommand("/tokentest", ServerTokenTest));
// User Commands
Rest.Register(new SecureRestCommand("/v2/users/activelist", UserActiveListV2, RestPermissions.restviewusers));
Rest.Register(new SecureRestCommand("/v2/users/create", UserCreateV2, RestPermissions.restmanageusers) { DoLog = false });
Rest.Register(new SecureRestCommand("/v2/users/list", UserListV2, RestPermissions.restviewusers));
Rest.Register(new SecureRestCommand("/v2/users/read", UserInfoV2, RestPermissions.restviewusers));
Rest.Register(new SecureRestCommand("/v2/users/destroy", UserDestroyV2, RestPermissions.restmanageusers));
Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, RestPermissions.restmanageusers) { DoLog = false });
// Ban Commands
Rest.Register(new SecureRestCommand("/bans/create", BanCreate, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, RestPermissions.restviewbans));
Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, RestPermissions.restmanagebans));
// World Commands
Rest.Register(new SecureRestCommand("/world/read", WorldRead));
Rest.Register(new SecureRestCommand("/world/meteor", WorldMeteor, RestPermissions.restcauseevents));
Rest.Register(new SecureRestCommand("/world/bloodmoon/{bloodmoon}", WorldBloodmoon, RestPermissions.restcauseevents));
Rest.Register(new SecureRestCommand("/v2/world/save", WorldSave, RestPermissions.restcfg));
Rest.Register(new SecureRestCommand("/v2/world/autosave/state/{state}", WorldChangeSaveSettings, RestPermissions.restcfg));
Rest.Register(new SecureRestCommand("/v2/world/butcher", WorldButcher, RestPermissions.restbutcher));
// Player Commands
Rest.Register(new SecureRestCommand("/lists/players", PlayerList));
Rest.Register(new SecureRestCommand("/v2/players/list", PlayerListV2));
Rest.Register(new SecureRestCommand("/v2/players/read", PlayerReadV2, RestPermissions.restuserinfo));
Rest.Register(new SecureRestCommand("/v3/players/read", PlayerReadV3, RestPermissions.restuserinfo));
Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, RestPermissions.restkick));
Rest.Register(new SecureRestCommand("/v2/players/ban", PlayerBanV2, RestPermissions.restban, RestPermissions.restmanagebans));
Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, RestPermissions.restkill));
Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, RestPermissions.restmute));
Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute));
// Group Commands
Rest.Register(new SecureRestCommand("/v2/groups/list", GroupList, RestPermissions.restviewgroups));
Rest.Register(new SecureRestCommand("/v2/groups/read", GroupInfo, RestPermissions.restviewgroups));
Rest.Register(new SecureRestCommand("/v2/groups/destroy", GroupDestroy, RestPermissions.restmanagegroups));
Rest.Register(new SecureRestCommand("/v2/groups/create", GroupCreate, RestPermissions.restmanagegroups));
Rest.Register(new SecureRestCommand("/v2/groups/update", GroupUpdate, RestPermissions.restmanagegroups));
}
#region RestServerMethods
[Description("Executes a remote command on the server, and returns the output of the command.")]
[RouteAttribute("/v2/server/rawcmd")]
[Permission(RestPermissions.restrawcommand)]
[Noun("cmd", true, "The command and arguments to execute.", typeof(String))]
[Token]
private object ServerCommand(RestRequestArgs args)
{
if (string.IsNullOrWhiteSpace(args.Parameters["cmd"]))
return RestMissingParam("cmd");
Group restPlayerGroup;
// TODO: Get rid of this when the old REST permission model is removed.
if (TShock.Config.RestUseNewPermissionModel)
restPlayerGroup = TShock.Groups.GetGroupByName(args.TokenData.UserGroupName);
else
restPlayerGroup = new SuperAdminGroup();
TSRestPlayer tr = new TSRestPlayer(args.TokenData.Username, restPlayerGroup);
Commands.HandleCommand(tr, args.Parameters["cmd"]);
return RestResponse(string.Join("\n", tr.GetCommandOutput()));
}
[Description("Executes a remote command on the server, and returns the output of the command.")]
[RouteAttribute("/v3/server/rawcmd")]
[Permission(RestPermissions.restrawcommand)]
[Noun("cmd", true, "The command and arguments to execute.", typeof(String))]
[Token]
private object ServerCommandV3(RestRequestArgs args)
{
if (string.IsNullOrWhiteSpace(args.Parameters["cmd"]))
return RestMissingParam("cmd");
Group restPlayerGroup;
// TODO: Get rid of this when the old REST permission model is removed.
if (TShock.Config.RestUseNewPermissionModel)
restPlayerGroup = TShock.Groups.GetGroupByName(args.TokenData.UserGroupName);
else
restPlayerGroup = new SuperAdminGroup();
TSRestPlayer tr = new TSRestPlayer(args.TokenData.Username, restPlayerGroup);
Commands.HandleCommand(tr, args.Parameters["cmd"]);
return new RestObject()
{
{"response", tr.GetCommandOutput()}
};
}
[Description("Turn the server off.")]
[Route("/v2/server/off")]
[Permission(RestPermissions.restmaintenance)]
[Noun("confirm", true, "Required to confirm that actually want to turn the server off.", typeof(bool))]
[Noun("message", false, "The shutdown message.", typeof(String))]
[Noun("nosave", false, "Shutdown without saving.", typeof(bool))]
[Token]
private object ServerOff(RestRequestArgs args)
{
if (!GetBool(args.Parameters["confirm"], false))
return RestInvalidParam("confirm");
// Inform players the server is shutting down
var reason = string.IsNullOrWhiteSpace(args.Parameters["message"]) ? "Server is shutting down" : args.Parameters["message"];
TShock.Utils.StopServer(!GetBool(args.Parameters["nosave"], false), reason);
return RestResponse("The server is shutting down");
}
[Description("Attempt to restart the server.")]
[Route("/v3/server/restart")]
[Permission(RestPermissions.restmaintenance)]
[Noun("confirm", true, "Confirm that you actually want to restart the server", typeof(bool))]
[Noun("message", false, "The shutdown message.", typeof(String))]
[Noun("nosave", false, "Shutdown without saving.", typeof(bool))]
[Token]
private object ServerRestart(RestRequestArgs args)
{
if (!GetBool(args.Parameters["confirm"], false))
return RestInvalidParam("confirm");
// Inform players the server is shutting down
var reason = string.IsNullOrWhiteSpace(args.Parameters["message"]) ? "Server is restarting" : args.Parameters["message"];
TShock.Utils.RestartServer(!GetBool(args.Parameters["nosave"], false), reason);
return RestResponse("The server is shutting down and will attempt to restart");
}
[Description("Reload config files for the server.")]
[Route("/v3/server/reload")]
[Permission(RestPermissions.restcfg)]
[Token]
private object ServerReload(RestRequestArgs args)
{
TShock.Utils.Reload(new TSRestPlayer(args.TokenData.Username, TShock.Groups.GetGroupByName(args.TokenData.UserGroupName)));
return RestResponse("Configuration, permissions, and regions reload complete. Some changes may require a server restart.");
}
[Description("Broadcast a server wide message.")]
[Route("/v2/server/broadcast")]
[Noun("msg", true, "The message to broadcast.", typeof(String))]
[Token]
private object ServerBroadcast(RestRequestArgs args)
{
var msg = args.Parameters["msg"];
if (string.IsNullOrWhiteSpace(msg))
return RestMissingParam("msg");
TSPlayer.All.SendInfoMessage(msg);
return RestResponse("The message was broadcasted successfully");
}
[Description("Returns the motd, if it exists.")]
[Route("/v3/server/motd")]
[Token]
private object ServerMotd(RestRequestArgs args)
{
string motdFilePath = Path.Combine(TShock.SavePath, "motd.txt");
if (!File.Exists(motdFilePath))
return this.RestError("The motd.txt was not found.", "500");
return new RestObject()
{
{"motd", File.ReadAllLines(motdFilePath)}
};
}
[Description("Returns the rules, if they exist.")]
[Route("/v3/server/rules")]
[Token]
private object ServerRules(RestRequestArgs args)
{
string rulesFilePath = Path.Combine(TShock.SavePath, "rules.txt");
if (!File.Exists(rulesFilePath))
return this.RestError("The rules.txt was not found.", "500");
return new RestObject()
{
{"rules", File.ReadAllLines(rulesFilePath)}
};
}
[Description("Returns the current status of the server.")]
[Route("/status")]
[Token]
private object ServerStatus(RestRequestArgs args)
{
var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
return new RestObject()
{
{"name", TShock.Config.ServerName},
{"port", Convert.ToString(Netplay.ListenPort)},
{"playercount", Convert.ToString(activeplayers.Count())},
{"players", string.Join(", ", activeplayers.Select(p => p.name))},
};
}
[Description("Get a list of information about the current TShock server.")]
[Route("/v2/server/status")]
[Token]
private object ServerStatusV2(RestRequestArgs args)
{
var ret = new RestObject()
{
{"name", TShock.Config.ServerName},
{"serverversion", Main.versionNumber},
{"tshockversion", TShock.VersionNum},
{"port", TShock.Config.ServerPort},
{"playercount", Main.player.Where(p => null != p && p.active).Count()},
{"maxplayers", TShock.Config.MaxSlots},
{"world", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)},
{"uptime", (DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime).ToString(@"d'.'hh':'mm':'ss")},
{"serverpassword", !string.IsNullOrEmpty(TShock.Config.ServerPassword)}
};
if (GetBool(args.Parameters["players"], false))
{
var players = new ArrayList();
foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p))
{
var p = PlayerFilter(tsPlayer, args.Parameters, ((args.TokenData.UserGroupName) != "" && TShock.Utils.GetGroup(args.TokenData.UserGroupName).HasPermission(RestPermissions.viewips)));
if (null != p)
players.Add(p);
}
ret.Add("players", players);
}
if (GetBool(args.Parameters["rules"], false))
{
var rules = new Dictionary<string,object>();
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);
rules.Add("ServerSideInventory", Main.ServerSideCharacter);
ret.Add("rules", rules);
}
return ret;
}
[Description("Test if a token is still valid.")]
[Route("/tokentest")]
[Token]
private object ServerTokenTest(RestRequestArgs args)
{
return new RestObject()
{
{"response", "Token is valid and was passed through correctly."},
{"associateduser", args.TokenData.Username}
};
}
#endregion
#region RestUserMethods
[Description("Returns the list of user accounts that are currently in use on the server.")]
[Route("/v2/users/activelist")]
[Permission(RestPermissions.restviewusers)]
[Token]
private object UserActiveListV2(RestRequestArgs args)
{
return new RestObject() { { "activeusers", string.Join("\t", TShock.Players.Where(p => null != p && null != p.User && p.Active).Select(p => p.User.Name)) } };
}
[Description("Lists all user accounts in the TShock database.")]
[Route("/v2/users/list")]
[Permission(RestPermissions.restviewusers)]
[Token]
private object UserListV2(RestRequestArgs args)
{
return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary<string,object>(){
{"name", p.Name},
{"id", p.ID},
{"group", p.Group},
}) } };
}
[Description("Create a new TShock user account.")]
[Route("/v2/users/create")]
[Permission(RestPermissions.restmanageusers)]
[Noun("user", true, "The user account name for the new account.", typeof(String))]
[Noun("group", false, "The group the new account should be assigned.", typeof(String))]
[Noun("password", true, "The password for the new account.", typeof(String))]
[Token]
private object UserCreateV2(RestRequestArgs args)
{
var username = args.Parameters["user"];
if (string.IsNullOrWhiteSpace(username))
return RestMissingParam("user");
var group = args.Parameters["group"];
if (string.IsNullOrWhiteSpace(group))
group = TShock.Config.DefaultRegistrationGroupName;
var password = args.Parameters["password"];
if (string.IsNullOrWhiteSpace(password))
return RestMissingParam("password");
// NOTE: ip can be blank
User user = new User(username, password, "", group, "", "", "");
try
{
TShock.Users.AddUser(user);
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("User was successfully created");
}
[Description("Update a users information.")]
[Route("/v2/users/update")]
[Permission(RestPermissions.restmanageusers)]
[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
[Noun("password", false, "The users new password, and at least this or group must be defined.", typeof(String))]
[Noun("group", false, "The new group for the user, at least this or password must be defined.", typeof(String))]
[Token]
private object UserUpdateV2(RestRequestArgs args)
{
var ret = UserFind(args.Parameters);
if (ret is RestObject)
return ret;
var password = args.Parameters["password"];
var group = args.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;
}
[Description("Destroy a TShock user account.")]
[Route("/v2/users/destroy")]
[Permission(RestPermissions.restmanageusers)]
[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
[Token]
private object UserDestroyV2(RestRequestArgs args)
{
var ret = UserFind(args.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");
}
[Description("List detailed information for a user account.")]
[Route("/v2/users/read")]
[Permission(RestPermissions.restviewusers)]
[Noun("user", true, "The search criteria (name or id of account to lookup).", typeof(String))]
[Noun("type", true, "The search criteria type (name for name lookup, id for id lookup).", typeof(String))]
[Token]
private object UserInfoV2(RestRequestArgs args)
{
var ret = UserFind(args.Parameters);
if (ret is RestObject)
return ret;
User user = (User)ret;
return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() }, { "name", user.Name } };
}
#endregion
#region RestBanMethods
[Description("Create a new ban entry.")]
[Route("/bans/create")]
[Permission(RestPermissions.restmanagebans)]
[Noun("ip", false, "The IP to ban, at least this or name must be specified.", typeof(String))]
[Noun("name", false, "The name to ban, at least this or ip must be specified.", typeof(String))]
[Noun("reason", false, "The reason to assign to the ban.", typeof(String))]
[Token]
private object BanCreate(RestRequestArgs args)
{
var ip = args.Parameters["ip"];
var name = args.Parameters["name"];
if (string.IsNullOrWhiteSpace(ip) && string.IsNullOrWhiteSpace(name))
return RestMissingParam("ip", "name");
try
{
TShock.Bans.AddBan(ip, name, "", args.Parameters["reason"], true, args.TokenData.Username);
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("Ban created successfully");
}
[Description("Delete an existing ban entry.")]
[Route("/v2/bans/destroy")]
[Permission(RestPermissions.restmanagebans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))]
[Noun("type", true, "The type of search criteria, 'ip' or 'name'. Also used as the method of removing from the database.", typeof(String))]
[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
[Token]
private object BanDestroyV2(RestRequestArgs args)
{
var ret = BanFind(args.Parameters);
if (ret is RestObject)
return ret;
try
{
Ban ban = (Ban)ret;
switch (args.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(args.Parameters["caseinsensitive"], true)))
return RestResponse("Failed to delete ban (already deleted?)");
break;
default:
return RestError("Invalid Type: '" + args.Parameters["type"] + "'");
}
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("Ban deleted successfully");
}
[Description("View the details of a specific ban.")]
[Route("/v2/bans/read")]
[Permission(RestPermissions.restviewbans)]
[Noun("ban", true, "The search criteria, either an IP address or a name.", typeof(String))]
[Noun("type", true, "The type of search criteria, 'ip' or 'name'.", typeof(String))]
[Noun("caseinsensitive", false, "Name lookups should be case insensitive.", typeof(bool))]
[Token]
private object BanInfoV2(RestRequestArgs args)
{
var ret = BanFind(args.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},
};
}
[Description("View all bans in the TShock database.")]
[Route("/v2/bans/list")]
[Permission(RestPermissions.restviewbans)]
[Token]
private object BanListV2(RestRequestArgs args)
{
var banList = new ArrayList();
foreach (var ban in TShock.Bans.GetBans())
{
banList.Add(
new Dictionary<string, string>
{
{"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
[Route("/v2/world/autosave/state/{state}")]
[Permission(RestPermissions.restcfg)]
[Verb("state", "The status for autosave.", typeof(bool))]
[Token]
private object WorldChangeSaveSettings(RestRequestArgs args)
{
bool autoSave;
if (!bool.TryParse(args.Verbs["state"], out autoSave))
return RestInvalidParam("state");
TShock.Config.AutoSave = autoSave;
return RestResponse("AutoSave has been set to " + autoSave);
}
[Description("Save the world.")]
[Route("/v2/world/save")]
[Permission(RestPermissions.restcfg)]
[Token]
private object WorldSave(RestRequestArgs args)
{
SaveManager.Instance.SaveWorld();
return RestResponse("World saved");
}
[Description("Butcher npcs.")]
[Route("/v2/world/butcher")]
[Permission(RestPermissions.restbutcher)]
[Noun("killfriendly", false, "Should friendly npcs be butchered.", typeof(bool))]
[Token]
private object WorldButcher(RestRequestArgs args)
{
bool killFriendly;
if (!bool.TryParse(args.Parameters["killfriendly"], out killFriendly))
return RestInvalidParam("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");
}
[Description("Get information regarding the world.")]
[Route("/world/read")]
[Token]
private object WorldRead(RestRequestArgs args)
{
return new RestObject()
{
{"name", (TShock.Config.UseServerName ? TShock.Config.ServerName : Main.worldName)},
{"size", Main.maxTilesX + "*" + Main.maxTilesY},
{"time", Main.time},
{"daytime", Main.dayTime},
{"bloodmoon", Main.bloodMoon},
{"invasionsize", Main.invasionSize}
};
}
[Description("Drops a meteor on the world.")]
[Route("/world/meteor")]
[Permission(RestPermissions.restcauseevents)]
[Token]
private object WorldMeteor(RestRequestArgs args)
{
if (null == WorldGen.genRand)
WorldGen.genRand = new Random();
WorldGen.dropMeteor();
return RestResponse("Meteor has been spawned");
}
[Description("Toggle the status of blood moon.")]
[Route("/world/bloodmoon/{bloodmoon}")]
[Permission(RestPermissions.restcauseevents)]
[Verb("bloodmoon", "State of bloodmoon.", typeof(bool))]
[Token]
private object WorldBloodmoon(RestRequestArgs args)
{
bool bloodmoon;
if (!bool.TryParse(args.Verbs["bloodmoon"], out bloodmoon))
return RestInvalidParam("bloodmoon");
Main.bloodMoon = bloodmoon;
return RestResponse("Blood Moon has been set to " + bloodmoon);
}
#endregion
#region RestPlayerMethods
[Description("Unmute a player.")]
[Route("/v2/players/unmute")]
[Permission(RestPermissions.restmute)]
[Noun("player", true, "The player to mute.", typeof(String))]
[Token]
private object PlayerUnMute(RestRequestArgs args)
{
return PlayerSetMute(args.Parameters, false);
}
[Description("Mute a player.")]
[Route("/v2/players/mute")]
[Permission(RestPermissions.restmute)]
[Noun("player", true, "The player to mute.", typeof(String))]
[Token]
private object PlayerMute(RestRequestArgs args)
{
return PlayerSetMute(args.Parameters, true);
}
[Description("List all player names that are currently on the server.")]
[Route("/lists/players")]
[Token]
private object PlayerList(RestRequestArgs args)
{
var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } };
}
[Description("Fetches detailed user information on all connected users, and can be filtered by specifying a key value pair filter users where the key is a field and the value is a users field value.")]
[Route("/v2/players/list")]
[Token]
private object PlayerListV2(RestRequestArgs args)
{
var playerList = new ArrayList();
foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p))
{
var p = PlayerFilter(tsPlayer, args.Parameters);
if (null != p)
playerList.Add(p);
}
return new RestObject() { { "players", playerList } };
}
[Description("Get information for a user.")]
[Route("/v2/players/read")]
[Permission(RestPermissions.restuserinfo)]
[Noun("player", true, "The player to lookup", typeof(String))]
[Token]
private object PlayerReadV2(RestRequestArgs args)
{
var ret = PlayerFind(args.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.User ? "" : player.User.Name},
{"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)}
};
}
[Description("Get information for a user.")]
[Route("/v3/players/read")]
[Permission(RestPermissions.restuserinfo)]
[Noun("player", true, "The player to lookup", typeof(String))]
[Token]
private object PlayerReadV3(RestRequestArgs args)
{
var ret = PlayerFind(args.Parameters);
if (ret is RestObject)
return ret;
TSPlayer player = (TSPlayer)ret;
var inventory = player.TPlayer.inventory.Where(p => p.active).ToList();
var equipment = player.TPlayer.armor.Where(p => p.active).ToList();
var dyes = player.TPlayer.dye.Where(p => p.active).ToList();
return new RestObject()
{
{"nickname", player.Name},
{"username", null == player.User ? "" : player.User.Name},
{"ip", player.IP},
{"group", player.Group.Name},
{"position", player.TileX + "," + player.TileY},
{"inventory", string.Join(", ", inventory.Select(p => (p.name + ":" + p.stack)))},
{"armor", string.Join(", ", equipment.Select(p => (p.netID + ":" + p.prefix)))},
{"dyes", string.Join(", ", dyes.Select(p => (p.name)))},
{"buffs", string.Join(", ", player.TPlayer.buffType)}
};
}
[Description("Kick a player off the server.")]
[Route("/v2/players/kick")]
[Permission(RestPermissions.restkick)]
[Noun("player", true, "The player to kick.", typeof(String))]
[Noun("reason", false, "The reason the player was kicked.", typeof(String))]
[Token]
private object PlayerKickV2(RestRequestArgs args)
{
var ret = PlayerFind(args.Parameters);
if (ret is RestObject)
return ret;
TSPlayer player = (TSPlayer)ret;
TShock.Utils.ForceKick(player, null == args.Parameters["reason"] ? "Kicked via web" : args.Parameters["reason"], false, true);
return RestResponse("Player " + player.Name + " was kicked");
}
[Description("Add a ban to the database.")]
[Route("/v2/players/ban")]
[Permission(RestPermissions.restban)]
[Permission(RestPermissions.restmanagebans)]
[Noun("player", true, "The player to kick.", typeof(String))]
[Noun("reason", false, "The reason the user was banned.", typeof(String))]
[Token]
private object PlayerBanV2(RestRequestArgs args)
{
var ret = PlayerFind(args.Parameters);
if (ret is RestObject)
return ret;
TSPlayer player = (TSPlayer)ret;
var reason = null == args.Parameters["reason"] ? "Banned via web" : args.Parameters["reason"];
TShock.Bans.AddBan(player.IP, player.Name, "", reason);
TShock.Utils.ForceKick(player, reason, false, true);
return RestResponse("Player " + player.Name + " was banned");
}
[Description("Kill a player.")]
[Route("/v2/players/kill")]
[Permission(RestPermissions.restkill)]
[Noun("player", true, "The player to kick.", typeof(String))]
[Noun("from", false, "Who killed the player.", typeof(String))]
[Token]
private object PlayerKill(RestRequestArgs args)
{
var ret = PlayerFind(args.Parameters);
if (ret is RestObject)
return ret;
TSPlayer player = (TSPlayer)ret;
player.DamagePlayer(999999);
var from = string.IsNullOrWhiteSpace(args.Parameters["from"]) ? "Server Admin" : args.Parameters["from"];
player.SendInfoMessage(string.Format("{0} just killed you!", from));
return RestResponse("Player " + player.Name + " was killed");
}
#endregion
#region RestGroupMethods
[Description("View all groups in the TShock database.")]
[Route("/v2/groups/list")]
[Permission(RestPermissions.restviewgroups)]
[Token]
private object GroupList(RestRequestArgs args)
{
var groups = new ArrayList();
foreach (Group group in TShock.Groups)
{
groups.Add(new Dictionary<string, object> {{"name", group.Name}, {"parent", group.ParentName}, {"chatcolor", group.ChatColor}});
}
return new RestObject() { { "groups", groups } };
}
[Description("Display information of a group.")]
[Route("/v2/groups/read")]
[Permission(RestPermissions.restviewgroups)]
[Noun("group", true, "The group name to get information on.", typeof(String))]
[Token]
private object GroupInfo(RestRequestArgs args)
{
var ret = GroupFind(args.Parameters);
if (ret is RestObject)
return ret;
Group group = (Group)ret;
return new RestObject() {
{"name", group.Name},
{"parent", group.ParentName},
{"chatcolor", string.Format("{0},{1},{2}", group.R, group.G, group.B)},
{"permissions", group.permissions},
{"negatedpermissions", group.negatedpermissions},
{"totalpermissions", group.TotalPermissions}
};
}
[Description("Delete a group.")]
[Route("/v2/groups/destroy")]
[Permission(RestPermissions.restmanagegroups)]
[Noun("group", true, "The group name to delete.", typeof(String))]
[Token]
private object GroupDestroy(RestRequestArgs args)
{
var ret = GroupFind(args.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");
}
[Description("Create a new group.")]
[Route("/v2/groups/create")]
[Permission(RestPermissions.restmanagegroups)]
[Noun("group", true, "The name of the new group.", typeof(String))]
[Noun("parent", false, "The name of the parent group.", typeof(String))]
[Noun("permissions", false, "A comma seperated list of permissions for the new group.", typeof(String))]
[Noun("chatcolor", false, "A r,g,b string representing the color for this groups chat.", typeof(String))]
[Token]
private object GroupCreate(RestRequestArgs args)
{
var name = args.Parameters["group"];
if (string.IsNullOrWhiteSpace(name))
return RestMissingParam("group");
try
{
TShock.Groups.AddGroup(name, args.Parameters["parent"], args.Parameters["permissions"], args.Parameters["chatcolor"]);
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("Group '" + name + "' created successfully");
}
[Route("/v2/groups/update")]
[Permission(RestPermissions.restmanagegroups)]
[Noun("group", true, "The name of the group to modify.", typeof(String))]
[Noun("parent", false, "The name of the new parent for this group.", typeof(String))]
[Noun("chatcolor", false, "The new chat color r,g,b.", typeof(String))]
[Noun("permisisons", false, "The new comma seperated list of permissions.", typeof(String))]
[Token]
private object GroupUpdate(RestRequestArgs args)
{
var ret = GroupFind(args.Parameters);
if (ret is RestObject)
return ret;
Group group = (Group)ret;
var parent = (null == args.Parameters["parent"]) ? group.ParentName : args.Parameters["parent"];
var chatcolor = (null == args.Parameters["chatcolor"]) ? string.Format("{0}.{1}.{2}", group.R, group.G, group.B) : args.Parameters["chatcolor"];
var permissions = (null == args.Parameters["permissions"]) ? group.Permissions : args.Parameters["permissions"];
try
{
TShock.Groups.UpdateGroup(group.Name, parent, permissions, chatcolor, group.Suffix, group.Prefix);
}
catch (Exception e)
{
return RestError(e.Message);
}
return RestResponse("Group '" + group.Name + "' updated successfully");
}
#endregion
#region Utility Methods
public static void DumpDescriptions()
{
var sb = new StringBuilder();
var rest = new RestManager(null);
foreach (var method in rest.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).OrderBy(f => f.Name))
{
if (method.IsStatic)
continue;
var name = method.Name;
var descattr =
method.GetCustomAttributes(false).FirstOrDefault(o => o is DescriptionAttribute) as DescriptionAttribute;
var routeattr =
method.GetCustomAttributes(false).FirstOrDefault(o => o is RouteAttribute) as RouteAttribute;
if (descattr != null && !string.IsNullOrWhiteSpace(descattr.Description) && routeattr != null && !string.IsNullOrWhiteSpace(routeattr.Route))
{
sb.AppendLine("{0} ".SFormat(name));
sb.AppendLine("Description: {0} ".SFormat(descattr.Description));
var permission = method.GetCustomAttributes(false).Where(o => o is Permission);
if (permission.Count() > 0)
{
sb.AppendLine("Permissions: {0}".SFormat(String.Join(", ", permission.Select(p => ((Permission)p).Name))));
}
else
{
sb.AppendLine("No special permissions are required for this route.");
}
var verbs = method.GetCustomAttributes(false).Where(o => o is Verb);
if (verbs.Count() > 0)
{
sb.AppendLine("Verbs:");
foreach (Verb verb in verbs)
{
sb.AppendLine("\t{0}({1}) [{2}] - {3}".SFormat(verb.Name, verb.Required ? "Required" : "Optional", verb.ArgumentType.Name, verb.Description));
}
}
var nouns = method.GetCustomAttributes(false).Where(o => o is Noun);
if (nouns.Count() > 0)
{
sb.AppendLine("Nouns:");
foreach (Noun noun in nouns)
{
sb.AppendLine("\t{0}({1}) [{2}] - {3}".SFormat(noun.Name, noun.Required ? "Required" : "Optional", noun.ArgumentType.Name, noun.Description));
}
}
sb.AppendLine("Example Usage: {0}?{1}".SFormat(routeattr.Route,
string.Join("&", nouns.Select(n => String.Format("{0}={0}", ((Noun) n).Name)))));
sb.AppendLine();
}
}
File.WriteAllText("RestDescriptions.txt", sb.ToString());
}
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;
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<string, object> PlayerFilter(TSPlayer tsPlayer, IParameterCollection parameters, bool viewips = false)
{
var player = new Dictionary<string, object>
{
{"nickname", tsPlayer.Name},
{"username", tsPlayer.User == null ? "" : tsPlayer.User.Name},
{"group", tsPlayer.Group.Name},
{"active", tsPlayer.Active},
{"state", tsPlayer.State},
{"team", tsPlayer.Team},
};
if (viewips)
{
player.Add("ip", tsPlayer.IP);
}
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.SendInfoMessage("You have been remotely " + verb);
return RestResponse("Player " + player.Name + " was " + verb);
}
#endregion
}
}