From 0ea83746cf82236ffac1ec514fb28b64ae0aeaac Mon Sep 17 00:00:00 2001 From: CoderCow Date: Thu, 25 Jul 2013 12:31:11 +0200 Subject: [PATCH] -Added REST per-endpoint permissions. -Added REST endpoint "/v2/server/restart". -Added REST endpoint "/v2/server/reload". -Added REST endpoint "/v3/server/rawcmd", will output all returned lines as an array instead. -Added "uptime", "serverpassword", "rules/ServerSideInventory" fields to REST endpoint "/v2/server/status". -REST requests are now logged. -Endpoint "/v2/server/rawcmd" does now check whether the user has the sufficient permission to execute the command. -Fixed Config.EnableTokenEndpointAuthentication not working properly before. -Removed obsolete "api" permission (only "restapi" now). --- TShockAPI/Commands.cs | 29 +---- TShockAPI/Permissions.cs | 64 ++++++++++- TShockAPI/Rest/Rest.cs | 47 +++++++- TShockAPI/Rest/RestCommand.cs | 53 ++++++++- TShockAPI/Rest/RestManager.cs | 210 +++++++++++++++++++++------------- TShockAPI/Rest/SecureRest.cs | 175 +++++++++++++++------------- TShockAPI/TSPlayer.cs | 12 +- TShockAPI/TShock.cs | 32 +----- TShockAPI/Utils.cs | 30 +++++ 9 files changed, 415 insertions(+), 237 deletions(-) diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index ce231c60..776b12d6 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -180,7 +180,7 @@ namespace TShockAPI add(Permissions.ban, Ban, "ban"); add(Permissions.whitelist, Whitelist, "whitelist"); add(Permissions.maintenance, Off, "off", "exit"); - add(Permissions.maintenance, Restart, "restart"); //Added restart command + add(Permissions.maintenance, Restart, "restart"); add(Permissions.maintenance, OffNoSave, "off-nosave", "exit-nosave"); add(Permissions.maintenance, CheckUpdates, "checkupdates"); add(Permissions.updateplugins, UpdatePlugins, "updateplugins"); @@ -1312,7 +1312,7 @@ namespace TShockAPI string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!"); TShock.Utils.StopServer(true, reason); } - //Added restart command + private static void Restart(CommandArgs args) { if (Main.runningMono) @@ -1321,21 +1321,8 @@ namespace TShockAPI } else { - if (TShock.Config.ServerSideInventory) - { - foreach (TSPlayer player in TShock.Players) - { - if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan) - { - TShock.InventoryDB.InsertPlayerData(player); - } - } - } - - string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!"); - TShock.Utils.StopServer(true, reason); - System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase); - Environment.Exit(0); + string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!"); + TShock.Utils.RestartServer(true, reason); } } @@ -2326,14 +2313,10 @@ namespace TShockAPI private static void Reload(CommandArgs args) { - FileTools.SetupConfig(); - TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs()); - TShock.Groups.LoadPermisions(); - TShock.Regions.ReloadAllRegions(); + TShock.Utils.Reload(args.Player); + args.Player.SendSuccessMessage( "Configuration, permissions, and regions reload complete. Some changes may require a server restart."); - - Hooks.GeneralHooks.OnReloadEvent(args.Player); } private static void ServerPassword(CommandArgs args) diff --git a/TShockAPI/Permissions.cs b/TShockAPI/Permissions.cs index b853b32f..82945147 100644 --- a/TShockAPI/Permissions.cs +++ b/TShockAPI/Permissions.cs @@ -73,7 +73,7 @@ namespace TShockAPI [Description("Allows you to bypass the max slots for up to 5 slots above your max")] public static readonly string reservedslot; - [Description("User is notified when an update is available")] public static readonly string maintenance; + [Description("User is notified when an update is available, user can turn off / restart the server.")] public static readonly string maintenance; [Description("User can kick others")] public static readonly string kick; @@ -177,16 +177,68 @@ namespace TShockAPI [Description("User can elevate other users' groups temporarily.")] public static readonly string settempgroup; - [Description("User can download updates to plugins that are currently running.")] public static readonly string updateplugins; - static Permissions() + [Description("User can download updates to plugins that are currently running.")] public static readonly string updateplugins; + + #region Rest Endpoint Permissions + [Description("Rest user can turn off / restart the server.")] + public static readonly string restmaintenance; + + [Description("Rest user can reload configurations, save the world and set auto save settings.")] + public static readonly string restcfg; + + + [Description("Rest user can list and get detailed information about users.")] + public static readonly string restviewusers; + + [Description("Rest user can alter users.")] + public static readonly string restmanageusers; + + [Description("Rest user can list and get detailed information about bans.")] + public static readonly string restviewbans; + + [Description("Rest user can alter bans.")] + public static readonly string restmanagebans; + + [Description("Rest user can list and get detailed information about groups.")] + public static readonly string restviewgroups; + + [Description("Rest user can alter groups.")] + public static readonly string restmanagegroups; + + + [Description("Rest user can get user information.")] + public static readonly string restuserinfo; + + [Description("Rest user can kick players.")] + public static readonly string restkick; + + [Description("Rest user can ban players.")] + public static readonly string restban; + + [Description("Rest user can mute and unmute players.")] + public static readonly string restmute; + + [Description("Rest user can kill players.")] + public static readonly string restkill; + + + [Description("Rest user can drop meteors or change bloodmoon.")] + public static readonly string restcauseevents; + + [Description("Rest user can butcher npcs.")] + public static readonly string restbutcher; + + + [Description("Rest user can run raw TShock commands (the raw command permissions are also checked though).")] + public static readonly string restrawcommand; + #endregion + + static Permissions() { foreach (var field in typeof (Permissions).GetFields()) { field.SetValue(null, field.Name); } - - //Backwards compatability. - restapi = "api"; } /// diff --git a/TShockAPI/Rest/Rest.cs b/TShockAPI/Rest/Rest.cs index 3f2ae2e2..2bed82d9 100644 --- a/TShockAPI/Rest/Rest.cs +++ b/TShockAPI/Rest/Rest.cs @@ -36,7 +36,16 @@ namespace Rests /// Parameters in the url /// {x} in urltemplate /// Response object or null to not handle request - public delegate object RestCommandD(RestVerbs verbs, IParameterCollection parameters); + public delegate object RestCommandD(RestVerbs verbs, IParameterCollection parameters); + + /// + /// Secure Rest command delegate including a token. + /// + /// Parameters in the url + /// {x} in urltemplate + /// The data of stored for the provided token. + /// Response object or null to not handle request + public delegate object SecureRestCommandD(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData); public class Rest : IDisposable { @@ -182,7 +191,41 @@ namespace Rests protected virtual object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms) { - return cmd.Callback(verbs, parms); + object result = cmd.Execute(verbs, parms); + if (cmd.DoLog) + Log.ConsoleInfo("Anonymous requested REST endpoint: " + BuildRequestUri(cmd, verbs, parms, false)); + + return result; + } + + protected virtual string BuildRequestUri( + RestCommand cmd, RestVerbs verbs, IParameterCollection parms, bool includeToken = true + ) { + StringBuilder requestBuilder = new StringBuilder(cmd.UriTemplate); + if (parms.Count > 0) + { + bool isFirstParam = true; + foreach (IParameter paramImpl in parms) + { + Parameter param = (paramImpl as Parameter); + if (param == null || (!includeToken && param.Name.Equals("token", StringComparison.InvariantCultureIgnoreCase))) + continue; + + if (!isFirstParam) + requestBuilder.Append('&'); + else + { + requestBuilder.Append('?'); + isFirstParam = false; + } + + requestBuilder.Append(param.Name); + requestBuilder.Append('='); + requestBuilder.Append(param.Value); + } + } + + return requestBuilder.ToString(); } #region Dispose diff --git a/TShockAPI/Rest/RestCommand.cs b/TShockAPI/Rest/RestCommand.cs index 334752de..afe96d33 100644 --- a/TShockAPI/Rest/RestCommand.cs +++ b/TShockAPI/Rest/RestCommand.cs @@ -16,8 +16,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ using System.Linq; -using System.Text.RegularExpressions; - +using System.Text.RegularExpressions; +using HttpServer; + namespace Rests { public class RestCommand @@ -26,8 +27,10 @@ namespace Rests public string UriTemplate { get; protected set; } public string UriVerbMatch { get; protected set; } public string[] UriVerbs { get; protected set; } - public RestCommandD Callback { get; protected set; } - public bool RequiresToken { get; set; } + public virtual bool RequiresToken { get { return false; } } + public bool DoLog { get; set; } + + private RestCommandD callback; /// /// @@ -42,8 +45,8 @@ namespace Rests UriVerbMatch = string.Format("^{0}$", string.Join("([^/]*)", Regex.Split(uritemplate, "\\{[^\\{\\}]*\\}"))); var matches = Regex.Matches(uritemplate, "\\{([^\\{\\}]*)\\}"); UriVerbs = (from Match match in matches select match.Groups[1].Value).ToArray(); - Callback = callback; - RequiresToken = true; + this.callback = callback; + DoLog = true; } /// @@ -59,6 +62,44 @@ namespace Rests public bool HasVerbs { get { return UriVerbs.Length > 0; } + } + + public virtual object Execute(RestVerbs verbs, IParameterCollection parameters) + { + return callback(verbs, parameters); } + } + + public class SecureRestCommand: RestCommand + { + public override bool RequiresToken { get { return true; } } + public string[] Permissions { get; set; } + + private SecureRestCommandD callback; + + public SecureRestCommand(string name, string uritemplate, SecureRestCommandD callback, params string[] permissions) + : base(name, uritemplate, null) + { + this.callback = callback; + Permissions = permissions; + } + + public SecureRestCommand(string uritemplate, SecureRestCommandD callback, params string[] permissions) + : this(string.Empty, uritemplate, callback, permissions) + { + } + + public override object Execute(RestVerbs verbs, IParameterCollection parameters) + { + return new RestObject("401") { Error = "Not authorized. The specified API endpoint requires a token." }; + } + + public object Execute(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) + { + if (tokenData.Equals(SecureRest.TokenData.None)) + return new RestObject("401") { Error = "Not authorized. The specified API endpoint requires a token." }; + + return callback(verbs, parameters, tokenData); + } } } \ No newline at end of file diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index 45cdb309..7ef91fc1 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -38,78 +38,127 @@ namespace TShockAPI 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 }); + Rest.Register(new SecureRestCommand("/v2/server/broadcast", ServerBroadcast)); + Rest.Register(new SecureRestCommand("/v2/server/off", ServerOff, Permissions.restmaintenance)); + Rest.Register(new SecureRestCommand("/v2/server/restart", ServerRestart, Permissions.restmaintenance)); + Rest.Register(new SecureRestCommand("/v2/server/reload", ServerReload, Permissions.restcfg)); + Rest.Register(new SecureRestCommand("/v2/server/rawcmd", ServerCommand, Permissions.restrawcommand)); + Rest.Register(new SecureRestCommand("/v3/server/rawcmd", ServerCommandV3, Permissions.restrawcommand)); + Rest.Register(new SecureRestCommand("/tokentest", ServerTokenTest)); + + if (TShock.Config.EnableTokenEndpointAuthentication) + { + Rest.Register(new SecureRestCommand("/v2/server/status", ServerStatusV2)); + Rest.Register(new SecureRestCommand("/status", ServerStatus)); + } + else + { + Rest.Register(new RestCommand("/v2/server/status", (a, b) => this.ServerStatusV2(a, b, SecureRest.TokenData.None))); + Rest.Register(new RestCommand("/status", (a, b) => this.ServerStatusV2(a, b, SecureRest.TokenData.None))); + } // 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)); + Rest.Register(new SecureRestCommand("/v2/users/activelist", UserActiveListV2, Permissions.restviewusers)); + Rest.Register(new SecureRestCommand("/v2/users/create", UserCreateV2, Permissions.restmanageusers) { DoLog = false }); + Rest.Register(new SecureRestCommand("/v2/users/list", UserListV2, Permissions.restviewusers)); + Rest.Register(new SecureRestCommand("/v2/users/read", UserInfoV2, Permissions.restviewusers)); + Rest.Register(new SecureRestCommand("/v2/users/destroy", UserDestroyV2, Permissions.restmanageusers)); + Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, Permissions.restmanageusers) { DoLog = false }); // 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)); + Rest.Register(new SecureRestCommand("/bans/create", BanCreate, Permissions.restmanagebans)); + Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, Permissions.restviewbans)); + Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, Permissions.restviewbans)); + Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, Permissions.restmanagebans)); // 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)); + Rest.Register(new SecureRestCommand("/world/read", WorldRead)); + Rest.Register(new SecureRestCommand("/world/meteor", WorldMeteor, Permissions.restcauseevents)); + Rest.Register(new SecureRestCommand("/world/bloodmoon/{bool}", WorldBloodmoon, Permissions.restcauseevents)); + Rest.Register(new SecureRestCommand("/v2/world/save", WorldSave, Permissions.restcfg)); + Rest.Register(new SecureRestCommand("/v2/world/autosave/state/{bool}", WorldChangeSaveSettings, Permissions.restcfg)); + Rest.Register(new SecureRestCommand("/v2/world/butcher", WorldButcher, Permissions.restbutcher)); // 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)); + Rest.Register(new SecureRestCommand("/lists/players", PlayerList)); + Rest.Register(new SecureRestCommand("/v2/players/list", PlayerListV2)); + Rest.Register(new SecureRestCommand("/v2/players/read", PlayerReadV2, Permissions.restuserinfo)); + Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, Permissions.restkick)); + Rest.Register(new SecureRestCommand("/v2/players/ban", PlayerBanV2, Permissions.restban, Permissions.restmanagebans)); + Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, Permissions.restkill)); + Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, Permissions.restmute)); + Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, Permissions.restmute)); // 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)); + Rest.Register(new SecureRestCommand("/v2/groups/list", GroupList, Permissions.restviewgroups)); + Rest.Register(new SecureRestCommand("/v2/groups/read", GroupInfo, Permissions.restviewgroups)); + Rest.Register(new SecureRestCommand("/v2/groups/destroy", GroupDestroy, Permissions.restmanagegroups)); + Rest.Register(new SecureRestCommand("/v2/groups/create", GroupCreate, Permissions.restmanagegroups)); + Rest.Register(new SecureRestCommand("/v2/groups/update", GroupUpdate, Permissions.restmanagegroups)); } #region RestServerMethods - private object ServerCommand(RestVerbs verbs, IParameterCollection parameters) + private object ServerCommand(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { if (string.IsNullOrWhiteSpace(parameters["cmd"])) - return RestMissingParam("cmd"); + return RestMissingParam("cmd"); - TSRestPlayer tr = new TSRestPlayer(); + TSRestPlayer tr = new TSRestPlayer(tokenData.Username, tokenData.UserGroup); Commands.HandleCommand(tr, parameters["cmd"]); return RestResponse(string.Join("\n", tr.GetCommandOutput())); + } + + private object ServerCommandV3(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) + { + if (string.IsNullOrWhiteSpace(parameters["cmd"])) + return RestMissingParam("cmd"); + + TSRestPlayer tr = new TSRestPlayer(tokenData.Username, tokenData.UserGroup); + Commands.HandleCommand(tr, parameters["cmd"]); + return new Dictionary + { + {"status", "200"}, + {"response", tr.GetCommandOutput()} + }; } - private object ServerOff(RestVerbs verbs, IParameterCollection parameters) + private object ServerOff(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { if (!GetBool(parameters["confirm"], false)) return RestInvalidParam("confirm"); // Inform players the server is shutting down - var msg = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; - TShock.Utils.StopServer(!GetBool(parameters["nosave"], false), msg); + var reason = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; + TShock.Utils.StopServer(!GetBool(parameters["nosave"], false), reason); return RestResponse("The server is shutting down"); + } + + private object ServerRestart(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) + { + if (!GetBool(parameters["confirm"], false)) + return RestInvalidParam("confirm"); + + // Inform players the server is shutting down + var reason = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; + TShock.Utils.RestartServer(!GetBool(parameters["nosave"], false), reason); + + return RestResponse("The server is shutting down and will attempt to restart"); + } + + private object ServerReload(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) + { + try { + TShock.Utils.Reload(new TSRestPlayer(tokenData.Username, tokenData.UserGroup)); + } catch (Exception ex) { + return RestError("Exception was thrown during the reload: " + ex); + } + + return RestResponse("Configuration, permissions, and regions reload complete. Some changes may require a server restart."); } - private object ServerBroadcast(RestVerbs verbs, IParameterCollection parameters) + private object ServerBroadcast(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var msg = parameters["msg"]; if (string.IsNullOrWhiteSpace(msg)) @@ -118,11 +167,8 @@ namespace TShockAPI return RestResponse("The message was broadcasted successfully"); } - private object ServerStatus(RestVerbs verbs, IParameterCollection parameters) + private object ServerStatus(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { - 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() { @@ -133,18 +179,17 @@ namespace TShockAPI }; } - private object ServerStatusV2(RestVerbs verbs, IParameterCollection parameters) + private object ServerStatusV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { - if (TShock.Config.EnableTokenEndpointAuthentication) - return RestError("Server settings require a token for this API call"); - var ret = new RestObject() { {"name", TShock.Config.ServerName}, {"port", TShock.Config.ServerPort}, {"playercount", Main.player.Where(p => null != p && p.active).Count()}, {"maxplayers", TShock.Config.MaxSlots}, - {"world", Main.worldName} + {"world", Main.worldName}, + {"uptime", (DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime).ToString(@"d'.'hh':'mm':'ss")}, + {"serverpassword", !string.IsNullOrEmpty(TShock.Config.ServerPassword)} }; if (GetBool(parameters["players"], false)) @@ -173,14 +218,15 @@ namespace TShockAPI 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("SpawnProtectionRadius", TShock.Config.SpawnProtectionRadius); + rules.Add("ServerSideInventory", TShock.Config.ServerSideInventory); ret.Add("rules", rules); } return ret; } - private object ServerTokenTest(RestVerbs verbs, IParameterCollection parameters) + private object ServerTokenTest(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { return RestResponse("Token is valid and was passed through correctly"); } @@ -189,12 +235,12 @@ namespace TShockAPI #region RestUserMethods - private object UserActiveListV2(RestVerbs verbs, IParameterCollection parameters) + private object UserActiveListV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { 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) + private object UserListV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary(){ {"name", p.Name}, @@ -204,7 +250,7 @@ namespace TShockAPI }) } }; } - private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters) + private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var username = parameters["user"]; if (string.IsNullOrWhiteSpace(username)) @@ -232,7 +278,7 @@ namespace TShockAPI return RestResponse("User was successfully created"); } - private object UserUpdateV2(RestVerbs verbs, IParameterCollection parameters) + private object UserUpdateV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = UserFind(parameters); if (ret is RestObject) @@ -274,7 +320,7 @@ namespace TShockAPI return response; } - private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters) + private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = UserFind(parameters); if (ret is RestObject) @@ -292,7 +338,7 @@ namespace TShockAPI return RestResponse("User deleted successfully"); } - private object UserInfoV2(RestVerbs verbs, IParameterCollection parameters) + private object UserInfoV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = UserFind(parameters); if (ret is RestObject) @@ -306,7 +352,7 @@ namespace TShockAPI #region RestBanMethods - private object BanCreate(RestVerbs verbs, IParameterCollection parameters) + private object BanCreate(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ip = parameters["ip"]; var name = parameters["name"]; @@ -325,7 +371,7 @@ namespace TShockAPI return RestResponse("Ban created successfully"); } - private object BanDestroyV2(RestVerbs verbs, IParameterCollection parameters) + private object BanDestroyV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = BanFind(parameters); if (ret is RestObject) @@ -357,7 +403,7 @@ namespace TShockAPI return RestResponse("Ban deleted successfully"); } - private object BanInfoV2(RestVerbs verbs, IParameterCollection parameters) + private object BanInfoV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = BanFind(parameters); if (ret is RestObject) @@ -371,7 +417,7 @@ namespace TShockAPI }; } - private object BanListV2(RestVerbs verbs, IParameterCollection parameters) + private object BanListV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var banList = new ArrayList(); foreach (var ban in TShock.Bans.GetBans()) @@ -393,7 +439,7 @@ namespace TShockAPI #region RestWorldMethods - private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters) + private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { bool autoSave; if (!bool.TryParse(verbs["bool"], out autoSave)) @@ -403,14 +449,14 @@ namespace TShockAPI return RestResponse("AutoSave has been set to " + autoSave); } - private object WorldSave(RestVerbs verbs, IParameterCollection parameters) + private object WorldSave(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { SaveManager.Instance.SaveWorld(); return RestResponse("World saved"); } - private object WorldButcher(RestVerbs verbs, IParameterCollection parameters) + private object WorldButcher(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { bool killFriendly; if (!bool.TryParse(parameters["killfriendly"], out killFriendly)) @@ -432,7 +478,7 @@ namespace TShockAPI return RestResponse(killcount + " NPCs have been killed"); } - private object WorldRead(RestVerbs verbs, IParameterCollection parameters) + private object WorldRead(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { return new RestObject() { @@ -445,7 +491,7 @@ namespace TShockAPI }; } - private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters) + private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { if (null == WorldGen.genRand) WorldGen.genRand = new Random(); @@ -453,7 +499,7 @@ namespace TShockAPI return RestResponse("Meteor has been spawned"); } - private object WorldBloodmoon(RestVerbs verbs, IParameterCollection parameters) + private object WorldBloodmoon(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { bool bloodmoon; if (!bool.TryParse(verbs["bool"], out bloodmoon)) @@ -467,23 +513,23 @@ namespace TShockAPI #region RestPlayerMethods - private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters) + private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { return PlayerSetMute(parameters, false); } - private object PlayerMute(RestVerbs verbs, IParameterCollection parameters) + private object PlayerMute(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { return PlayerSetMute(parameters, true); } - private object PlayerList(RestVerbs verbs, IParameterCollection parameters) + private object PlayerList(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { 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) + private object PlayerListV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var playerList = new ArrayList(); foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) @@ -495,7 +541,7 @@ namespace TShockAPI return new RestObject() { { "players", playerList } }; } - private object PlayerReadV2(RestVerbs verbs, IParameterCollection parameters) + private object PlayerReadV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = PlayerFind(parameters); if (ret is RestObject) @@ -515,7 +561,7 @@ namespace TShockAPI }; } - private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters) + private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = PlayerFind(parameters); if (ret is RestObject) @@ -526,7 +572,7 @@ namespace TShockAPI return RestResponse("Player " + player.Name + " was kicked"); } - private object PlayerBanV2(RestVerbs verbs, IParameterCollection parameters) + private object PlayerBanV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = PlayerFind(parameters); if (ret is RestObject) @@ -539,7 +585,7 @@ namespace TShockAPI return RestResponse("Player " + player.Name + " was banned"); } - private object PlayerKill(RestVerbs verbs, IParameterCollection parameters) + private object PlayerKill(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = PlayerFind(parameters); if (ret is RestObject) @@ -556,7 +602,7 @@ namespace TShockAPI #region RestGroupMethods - private object GroupList(RestVerbs verbs, IParameterCollection parameters) + private object GroupList(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var groups = new ArrayList(); foreach (Group group in TShock.Groups) @@ -566,7 +612,7 @@ namespace TShockAPI return new RestObject() { { "groups", groups } }; } - private object GroupInfo(RestVerbs verbs, IParameterCollection parameters) + private object GroupInfo(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = GroupFind(parameters); if (ret is RestObject) @@ -583,7 +629,7 @@ namespace TShockAPI }; } - private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters) + private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = GroupFind(parameters); if (ret is RestObject) @@ -602,7 +648,7 @@ namespace TShockAPI return RestResponse("Group '" + group.Name + "' deleted successfully"); } - private object GroupCreate(RestVerbs verbs, IParameterCollection parameters) + private object GroupCreate(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var name = parameters["group"]; if (string.IsNullOrWhiteSpace(name)) @@ -619,7 +665,7 @@ namespace TShockAPI return RestResponse("Group '" + name + "' created successfully"); } - private object GroupUpdate(RestVerbs verbs, IParameterCollection parameters) + private object GroupUpdate(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var ret = GroupFind(parameters); if (ret is RestObject) diff --git a/TShockAPI/Rest/SecureRest.cs b/TShockAPI/Rest/SecureRest.cs index 987cd295..0daeb424 100644 --- a/TShockAPI/Rest/SecureRest.cs +++ b/TShockAPI/Rest/SecureRest.cs @@ -18,38 +18,42 @@ along with this program. If not, see . using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using HttpServer; - +using System.Net; +using System.Text; +using HttpServer; +using TShockAPI; +using TShockAPI.DB; + namespace Rests { - /// - /// - /// - /// Username to verify - /// Password to verify - /// Returning a restobject with a null error means a successful verification. - public delegate RestObject VerifyD(string username, string password); - public class SecureRest : Rest - { - public Dictionary Tokens { get; protected set; } - public event VerifyD Verify; + { + public struct TokenData + { + public static readonly TokenData None = default(TokenData); + + public string Username { get; set; } + public Group UserGroup { get; set; } + } + + public Dictionary Tokens { get; protected set; } public SecureRest(IPAddress ip, int port) : base(ip, port) { - Tokens = new Dictionary(); - Register(new RestCommand("/token/create/{username}/{password}", NewToken) {RequiresToken = false}); - Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { RequiresToken = false }); - Register(new RestCommand("/token/destroy/{token}", DestroyToken) {RequiresToken = true}); - foreach (KeyValuePair t in TShockAPI.TShock.RESTStartupTokens) + Tokens = new Dictionary(); + + Register(new RestCommand("/token/create/{username}/{password}", NewToken) { DoLog = false }); + Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { DoLog = false }); + Register(new SecureRestCommand("/token/destroy/{token}", DestroyToken)); + + foreach (KeyValuePair t in TShockAPI.TShock.RESTStartupTokens) { Tokens.Add(t.Key, t.Value); } } - private object DestroyToken(RestVerbs verbs, IParameterCollection parameters) + private object DestroyToken(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData) { var token = verbs["token"]; try @@ -70,29 +74,7 @@ namespace Rests var user = parameters["username"]; var pass = verbs["password"]; - RestObject obj = null; - if (Verify != null) - obj = Verify(user, pass); - - if (obj == null) - obj = new RestObject("401") { Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." }; - - if (obj.Error != null) - return obj; - - string hash; - var rand = new Random(); - var randbytes = new byte[32]; - do - { - rand.NextBytes(randbytes); - hash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2")); - } while (Tokens.ContainsKey(hash)); - - Tokens.Add(hash, user); - - obj["token"] = hash; - return obj; + return this.NewTokenInternal(user, pass); } private object NewToken(RestVerbs verbs, IParameterCollection parameters) @@ -100,55 +82,84 @@ namespace Rests var user = verbs["username"]; var pass = verbs["password"]; - RestObject obj = null; - if (Verify != null) - obj = Verify(user, pass); - - if (obj == null) - obj = new RestObject("401") - {Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair."}; - - if (obj.Error != null) - return obj; - - string hash; + RestObject response = this.NewTokenInternal(user, pass); + response["deprecated"] = "This endpoint is depracted and will be removed in the future."; + return response; + } + + private RestObject NewTokenInternal(string username, string password) + { + User userAccount = TShock.Users.GetUserByName(username); + if (userAccount == null || !string.IsNullOrWhiteSpace(userAccount.Address)) + return new RestObject("401") + { Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." }; + + if (!TShock.Utils.HashPassword(password).Equals(userAccount.Password, StringComparison.InvariantCultureIgnoreCase)) + return new RestObject("401") + { Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." }; + + Group userGroup = TShock.Utils.GetGroup(userAccount.Group); + if (!userGroup.HasPermission(Permissions.restapi) && userAccount.Group != "superadmin") + return new RestObject("403") + { Error = "Although your account was successfully found and identified, your account lacks the permission required to use the API. (restapi)" }; + + string tokenHash; var rand = new Random(); var randbytes = new byte[32]; do { rand.NextBytes(randbytes); - hash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2")); - } while (Tokens.ContainsKey(hash)); + tokenHash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2")); + } while (Tokens.ContainsKey(tokenHash)); - Tokens.Add(hash, user); - - obj["token"] = hash; - obj["deprecated"] = "This method will be removed from TShock in 3.6."; - return obj; + Tokens.Add(tokenHash, new TokenData { Username = userAccount.Name, UserGroup = userGroup }); + + RestObject response = new RestObject("200") { Response = "Successful login" }; + response["token"] = tokenHash; + return response; } - protected override object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms) { - if (cmd.RequiresToken) - { - var strtoken = parms["token"]; - if (strtoken == null) - return new Dictionary - {{"status", "401"}, {"error", "Not authorized. The specified API endpoint requires a token."}}; - - object token; - if (!Tokens.TryGetValue(strtoken, out token)) - return new Dictionary - { - {"status", "403"}, - { - "error", - "Not authorized. The specified API endpoint requires a token, but the provided token was not valid." - } - }; - } - return base.ExecuteCommand(cmd, verbs, parms); + if (!cmd.RequiresToken) + return base.ExecuteCommand(cmd, verbs, parms); + + var token = parms["token"]; + if (token == null) + return new Dictionary + {{"status", "401"}, {"error", "Not authorized. The specified API endpoint requires a token."}}; + + SecureRestCommand secureCmd = (SecureRestCommand)cmd; + TokenData tokenData; + if (!Tokens.TryGetValue(token, out tokenData)) + return new Dictionary + { + {"status", "403"}, + { + "error", + "Not authorized. The specified API endpoint requires a token, but the provided token was not valid." + } + }; + + if (secureCmd.Permissions.Length > 0 && secureCmd.Permissions.All(perm => !tokenData.UserGroup.HasPermission(perm))) + { + return new Dictionary + { + {"status", "403"}, + { + "error", + string.Format("Not authorized. User \"{0}\" has no access to use the specified API endpoint.", tokenData.Username) + } + }; + } + + object result = secureCmd.Execute(verbs, parms, tokenData); + if (cmd.DoLog) + TShock.Utils.SendLogs(string.Format( + "\"{0}\" requested REST endpoint: {1}", tokenData.Username, this.BuildRequestUri(cmd, verbs, parms, false)), + Color.PaleVioletRed); + + return result; } } } \ No newline at end of file diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index bd13cfd4..b8443a16 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -759,13 +759,13 @@ namespace TShockAPI } } - public class TSRestPlayer : TSServerPlayer + public class TSRestPlayer : TSPlayer { - internal List CommandReturn = new List(); + internal List CommandOutput = new List(); - public TSRestPlayer() + public TSRestPlayer(string playerName, Group playerGroup): base(playerName) { - Group = new SuperAdminGroup(); + Group = playerGroup; AwaitingResponse = new Dictionary>(); } @@ -781,7 +781,7 @@ namespace TShockAPI public override void SendMessage(string msg, byte red, byte green, byte blue) { - CommandReturn.Add(msg); + this.CommandOutput.Add(msg); } public override void SendInfoMessage(string msg) @@ -806,7 +806,7 @@ namespace TShockAPI public List GetCommandOutput() { - return CommandReturn; + return this.CommandOutput; } } diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 7449e780..b3101d5c 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -72,7 +72,7 @@ namespace TShockAPI /// /// Used for implementing REST Tokens prior to the REST system starting up. /// - public static Dictionary RESTStartupTokens = new Dictionary(); + public static Dictionary RESTStartupTokens = new Dictionary(); /// /// Called after TShock is initialized. Useful for plugins that needs hooks before tshock but also depend on tshock being loaded. @@ -220,7 +220,6 @@ namespace TShockAPI RememberedPos = new RememberedPosManager(DB); InventoryDB = new InventoryManager(DB); RestApi = new SecureRest(Netplay.serverListenIP, Config.RestApiPort); - RestApi.Verify += RestApi_Verify; RestApi.Port = Config.RestApiPort; RestManager = new RestManager(RestApi); RestManager.RegisterRestfulCommands(); @@ -294,33 +293,6 @@ namespace TShockAPI // ReSharper restore LocalizableElement } - private RestObject RestApi_Verify(string username, string password) - { - var userAccount = Users.GetUserByName(username); - if (userAccount == null) - { - return new RestObject("401") - {Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair."}; - } - - if (Utils.HashPassword(password).ToUpper() != userAccount.Password.ToUpper()) - { - return new RestObject("401") - {Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair."}; - } - - if (!Utils.GetGroup(userAccount.Group).HasPermission(Permissions.restapi) && userAccount.Group != "superadmin") - { - return new RestObject("403") - { - Error = - "Although your account was successfully found and identified, your account lacks the permission required to use the API. (api)" - }; - } - - return new RestObject("200") {Response = "Successful login"}; //Maybe return some user info too? - } - protected override void Dispose(bool disposing) { if (disposing) @@ -507,7 +479,7 @@ namespace TShockAPI break; case "-rest-token": string token = Convert.ToString(parms[++i]); - RESTStartupTokens.Add(token, "null"); + RESTStartupTokens.Add(token, new SecureRest.TokenData { Username = "null", UserGroup = new SuperAdminGroup() }); Console.WriteLine("Startup parameter overrode REST token."); break; case "-rest-enabled": diff --git a/TShockAPI/Utils.cs b/TShockAPI/Utils.cs index 5f0d8706..c51e3d5e 100644 --- a/TShockAPI/Utils.cs +++ b/TShockAPI/Utils.cs @@ -560,6 +560,36 @@ namespace TShockAPI // Disconnect after kick as that signifies server is exiting and could cause a race Netplay.disconnect = true; + } + + /// + /// Stops the server after kicking all players with a reason message, and optionally saving the world then attempts to + /// restart it. + /// + /// bool perform a world save before stop (default: true) + /// string reason (default: "Server shutting down!") + public void RestartServer(bool save = true, string reason = "Server shutting down!") + { + if (TShock.Config.ServerSideInventory) + foreach (TSPlayer player in TShock.Players) + if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan) + TShock.InventoryDB.InsertPlayerData(player); + + StopServer(true, reason); + System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase); + Environment.Exit(0); + } + + /// + /// Reloads all configuration settings, groups, regions and raises the reload event. + /// + public void Reload(TSPlayer player) + { + FileTools.SetupConfig(); + TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs()); + TShock.Groups.LoadPermisions(); + TShock.Regions.ReloadAllRegions(); + Hooks.GeneralHooks.OnReloadEvent(player); } #if COMPAT_SIGS