diff --git a/CHANGELOG.md b/CHANGELOG.md index a1db2fe9..c101ba5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,11 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * Fixed server crashes caused by client disconnections when attempting to read closed sockets (@Enerdy) * Added some code to make trapdoors work better (@DogooFalchion) * AllowCutTilesAndBreakables config option now correctly allows flowers/vines/herbs to be cut in regions without breaking walls (@WhiteXZ) -* REST: `/status` has been re-added. It will now always point to `/v2/server/status` and includes an `upgrade` field describing the newest status route (@WhiteXZ) * REST: `/v3/players/read` now includes a `muted` field (@WhiteXZ) -* REST: Token creation is now more secure (@WhiteXZ) (Thanks to @Plazmaz for reporting the issue!) +* REST: Token creation is now more secure (Thanks to @Plazmaz for reporting the issue!) +* REST: Deprecated the RestRequestEvent. If you use this event, please let us know. +* REST: ALL endpoints now have a base route (eg you can use `/server/motd` instead of `/v3/server/motd`). These base routes will never change, but will provide an `upgrade` field describing any newer routes +* REST: Added `/v3/world/autosave` and `/v3/world/bloodmoon` which use GET parameter style arguments. I.e., `/v3/world/autosave?state=[true|false]` & `/v3/world/bloodmoon?state=[true|false]`. The state argument is optional * Fixed fishing quests not saving/loading correctly when login before join, UUID login, and SSC were enabled together (@DogooFalchion) ## TShock 4.3.21 diff --git a/TShockAPI/Rest/Rest.cs b/TShockAPI/Rest/Rest.cs index 91b042e1..e29ec7e1 100644 --- a/TShockAPI/Rest/Rest.cs +++ b/TShockAPI/Rest/Rest.cs @@ -35,18 +35,44 @@ namespace Rests /// /// Rest command delegate /// - /// RestRequestArgs object containing Verbs, Parameters, Request, and TokenData + /// object containing Verbs, Parameters, Request, and TokenData /// Response object or null to not handle request public delegate object RestCommandD(RestRequestArgs args); + /// + /// Describes the data contained in a REST request + /// public class RestRequestArgs { + /// + /// Verbs sent in the request + /// public RestVerbs Verbs { get; private set; } + /// + /// Parameters sent in the request + /// public IParameterCollection Parameters { get; private set; } + /// + /// The HTTP request + /// public IRequest Request { get; private set; } + /// + /// Token data used by the request + /// public SecureRest.TokenData TokenData { get; private set; } + /// + /// used by the request + /// public IHttpContext Context { get; private set; } + /// + /// Creates a new instance of with the given verbs, parameters, request, and context. + /// No token data is used + /// + /// Verbs used in the request + /// Parameters used in the request + /// The HTTP request + /// The HTTP context public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, IHttpContext context) { Verbs = verbs; @@ -56,6 +82,14 @@ namespace Rests Context = context; } + /// + /// Creates a new instance of with the given verbs, parameters, request, token data, and context. + /// + /// Verbs used in the request + /// Parameters used in the request + /// The HTTP request + /// Token data used in the request + /// The HTTP context public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, SecureRest.TokenData tokenData, IHttpContext context) { Verbs = verbs; @@ -65,16 +99,39 @@ namespace Rests Context = context; } } + + /// + /// A RESTful API service + /// public class Rest : IDisposable { private readonly List commands = new List(); + /// + /// Contains redirect URIs. The key is the base URI. The first item of the tuple is the redirect URI. + /// The second item of the tuple is an optional "upgrade" URI which will be added to the REST response. + /// + private Dictionary> redirects = new Dictionary>(); private HttpListener listener; private StringHeader serverHeader; - public Dictionary tokenBucket = new Dictionary(); private Timer tokenBucketTimer; + /// + /// Contains tokens used to manage REST authentication + /// + public Dictionary tokenBucket = new Dictionary(); + /// + /// the REST service is listening on + /// public IPAddress Ip { get; set; } + /// + /// Port the REST service is listening on + /// public int Port { get; set; } + /// + /// Creates a new instance of listening on the given IP and port + /// + /// to listen on + /// Port to listen on public Rest(IPAddress ip, int port) { Ip = ip; @@ -83,6 +140,9 @@ namespace Rests serverHeader = new StringHeader("Server", String.Format("{0}/{1}", assembly.Name, assembly.Version)); } + /// + /// Starts the RESTful API service + /// public virtual void Start() { try @@ -106,6 +166,11 @@ namespace Rests } } + /// + /// Starts the RESTful API service using the given and port + /// + /// to listen on + /// Port to listen on public void Start(IPAddress ip, int port) { Ip = ip; @@ -113,21 +178,56 @@ namespace Rests Start(); } + /// + /// Stops the RESTful API service + /// public virtual void Stop() { listener.Stop(); } + /// + /// Registers a command using the given route + /// + /// URL route + /// Command callback public void Register(string path, RestCommandD callback) { AddCommand(new RestCommand(path, callback)); } + /// + /// Registers a + /// + /// to register public void Register(RestCommand com) { AddCommand(com); } + /// + /// Registers a redirection from a given REST route to a target REST route, with an optional upgrade URI + /// + /// The base URI that will be requested + /// The target URI to redirect to from the base URI + /// The upgrade route that will be added as an object to the response of the target route + /// Whether the route uses parameterized querying or not. + public void RegisterRedirect(string baseRoute, string targetRoute, string upgradeRoute = null, bool parameterized = true) + { + if (redirects.ContainsKey(baseRoute)) + { + redirects.Add(baseRoute, Tuple.Create(targetRoute, upgradeRoute)); + } + else + { + redirects[baseRoute] = Tuple.Create(targetRoute, upgradeRoute); + } + } + + /// + /// Adds a to the service's command list + /// + /// to add protected void AddCommand(RestCommand com) { commands.Add(com); @@ -151,11 +251,13 @@ namespace Rests } #region Event + [Obsolete("This class will be removed in the next release")] public class RestRequestEventArgs : HandledEventArgs { public RequestEventArgs Request { get; set; } } + [Obsolete("This method will be removed in the next release")] public static HandlerList RestRequestEvent; private static bool OnRestRequestCall(RequestEventArgs request) @@ -172,7 +274,11 @@ namespace Rests } #endregion - + /// + /// Called when the receives a request + /// + /// Sender of the request + /// RequestEventArgs received protected virtual void OnRequest(object sender, RequestEventArgs e) { var obj = ProcessRequest(sender, e); @@ -196,12 +302,25 @@ namespace Rests e.Response.Status = HttpStatusCode.OK; } + /// + /// Attempts to process a request received by the + /// + /// Sender of the request + /// RequestEventArgs received + /// A describing the state of the request protected virtual object ProcessRequest(object sender, RequestEventArgs e) { try { var uri = e.Request.Uri.AbsolutePath; uri = uri.TrimEnd('/'); + string upgrade = null; + + if (redirects.ContainsKey(uri)) + { + upgrade = redirects[uri].Item2; + uri = redirects[uri].Item1; + } foreach (var com in commands) { @@ -224,7 +343,17 @@ namespace Rests var obj = ExecuteCommand(com, verbs, e.Request.Parameters, e.Request, e.Context); if (obj != null) + { + if (!string.IsNullOrWhiteSpace(upgrade) && obj is RestObject) + { + if (!(obj as RestObject).ContainsKey("upgrade")) + { + (obj as RestObject).Add("upgrade", upgrade); + } + } + return obj; + } } } catch (Exception exception) @@ -242,6 +371,15 @@ namespace Rests }; } + /// + /// Executes a using the provided verbs, parameters, request, and context objects + /// + /// The REST command to execute + /// The REST verbs used in the command + /// The REST parameters used in the command + /// The HTTP request object associated with the command + /// The HTTP context associated with the command + /// protected virtual object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms, IRequest request, IHttpContext context) { object result = cmd.Execute(verbs, parms, request, context); @@ -253,6 +391,14 @@ namespace Rests return result; } + /// + /// Builds a request URI from the parameters, verbs, and URI template of a + /// + /// The REST command to take the URI template from + /// Verbs used in building the URI string + /// Parameters used in building the URI string + /// Whether or not to include a token in the URI + /// protected virtual string BuildRequestUri( RestCommand cmd, RestVerbs verbs, IParameterCollection parms, bool includeToken = true ) { @@ -276,12 +422,19 @@ namespace Rests #region Dispose + /// + /// Disposes the RESTful API service + /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// + /// Disposes the RESTful API service + /// + /// protected virtual void Dispose(bool disposing) { if (disposing) @@ -294,6 +447,9 @@ namespace Rests } } + /// + /// Destructor + /// ~Rest() { Dispose(false); diff --git a/TShockAPI/Rest/RestCommand.cs b/TShockAPI/Rest/RestCommand.cs index e23efee2..f0a2b90a 100644 --- a/TShockAPI/Rest/RestCommand.cs +++ b/TShockAPI/Rest/RestCommand.cs @@ -34,7 +34,7 @@ namespace Rests private RestCommandD callback; /// - /// + /// Creates a new used with the REST API /// /// Used for identification /// Url template @@ -51,7 +51,7 @@ namespace Rests } /// - /// + /// Creates a new used with the REST API /// /// Url template /// Rest Command callback diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index a6152b70..8f407ea6 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -31,35 +31,78 @@ using TShockAPI.DB; namespace TShockAPI { + /// + /// Describes the permission required to use an API route + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class Permission : Attribute { + /// + /// Name of the permission + /// public string Name { get; set; } + /// + /// Creates a new instance of with the given name + /// + /// Permission required public Permission(string name) { Name = name; } } + /// + /// Describes the route of a REST API call + /// [AttributeUsage(AttributeTargets.Method)] public class RouteAttribute : Attribute { + /// + /// The route used to call the API + /// public string Route { get; set; } + /// + /// Creates a new instance of with the given route + /// + /// Route used to call the API public RouteAttribute(string route) { Route = route; } } + /// + /// Describes a parameter in a REST route + /// public class ParameterAttribute : Attribute { + /// + /// The parameter's name + /// public string Name { get; set; } + /// + /// Whether the parameter is required or not + /// public bool Required { get; set; } + /// + /// The parameter's description + /// public string Description { get; set; } + /// + /// The parameter's System Type + /// public Type ArgumentType { get; set; } + /// + /// Creates a new instance of with the given name, description, and type. + /// A ParameterAttribute may be optional or required. + /// + /// + /// + /// + /// public ParameterAttribute(string name, bool req, string desc, Type type) { Name = name; @@ -69,51 +112,136 @@ namespace TShockAPI } } + /// + /// Describes a parameter in a REST route + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class Noun : ParameterAttribute { + /// + /// Creates a new instance of with the given name, description, and type. + /// Nouns may be optional or required. A required Noun is akin to a + /// + /// Name of the noun + /// Whether the noun is required or not + /// Decription of the noun + /// System Type of the noun public Noun(string name, bool req, string desc, Type type) : base(name, req, desc, type) { } } + /// + /// Describes a parameter in a REST route + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class Verb : ParameterAttribute { + /// + /// Creates a new instance of with the given name, description, and type. + /// Verbs are required arguments. + /// + /// Name of the verb + /// Description of the verb + /// System Type of the verb public Verb(string name, string desc, Type type) : base(name, true, desc, type) { } } + /// + /// Describes a REST authentication token + /// [AttributeUsage(AttributeTargets.Method)] public class Token : Noun { + /// + /// Creates a new instance of + /// public Token() : base("token", true, "The REST authentication token.", typeof(String)){} } + /// + /// Manages a instance + /// public class RestManager { + /// + /// The RESTful API service that handles API requests + /// private Rest Rest; + /// + /// Creates a new instance of using the provided object + /// + /// public RestManager(Rest rest) { Rest = rest; } + /// + /// Registers default TShock REST commands + /// public void RegisterRestfulCommands() { // Server Commands if (TShock.Config.EnableTokenEndpointAuthentication) { - Rest.Register(new SecureRestCommand("/status", ServerStatusRoot)); Rest.Register(new SecureRestCommand("/v2/server/status", ServerStatusV2)); Rest.Register(new SecureRestCommand("/v3/server/motd", ServerMotd)); Rest.Register(new SecureRestCommand("/v3/server/rules", ServerRules)); } else { - Rest.Register(new RestCommand("/status", (a) => this.ServerStatusRoot(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context)))); - 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("/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 RestCommand("/v2/server/status", (a) => ServerStatusV2(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context)))); + Rest.Register(new RestCommand("/v3/server/motd", (a) => ServerMotd(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context)))); + Rest.Register(new RestCommand("/v3/server/rules", (a) => ServerRules(new RestRequestArgs(a.Verbs, a.Parameters, a.Request, SecureRest.TokenData.None, a.Context)))); } + Rest.RegisterRedirect("/status", "/v2/server/status"); + + //server commands + Rest.RegisterRedirect("/server/motd", "/v3/server/motd"); + Rest.RegisterRedirect("/server/rules", "/v3/server/rules"); + Rest.RegisterRedirect("/server/broadcast", "/v2/server/broadcast"); + Rest.RegisterRedirect("/server/reload", "/v2/server/reload"); + Rest.RegisterRedirect("/server/off", "/v2/server/off"); + Rest.RegisterRedirect("/server/restart", "/v3/server/restart"); + Rest.RegisterRedirect("/server/rawcmd", "/v3/server/rawcmd"); + + //user commands + Rest.RegisterRedirect("/users/activelist", "/v2/users/activelist"); + Rest.RegisterRedirect("/users/create", "/v2/users/create"); + Rest.RegisterRedirect("/users/list", "/v2/users/list"); + Rest.RegisterRedirect("/users/read", "/v2/users/read"); + Rest.RegisterRedirect("/users/destroy", "/v2/users/destroy"); + Rest.RegisterRedirect("/users/update", "/v2/users/update"); + + //ban commands + Rest.RegisterRedirect("/bans/list", "/v2/bans/list"); + Rest.RegisterRedirect("/bans/read", "/v2/bans/read"); + Rest.RegisterRedirect("/bans/destroy", "/v2/bans/destroy"); + + //world commands + Rest.RegisterRedirect("/world/bloodmoon", "v3/world/bloodmoon"); + Rest.RegisterRedirect("/world/save", "/v2/world/save"); + Rest.RegisterRedirect("/world/autosave", "/v3/world/autosave"); + + //player commands + Rest.RegisterRedirect("/lists/players", "/lists/players", "/v2/players/list"); + Rest.RegisterRedirect("/players/list", "/v2/players/list"); + Rest.RegisterRedirect("/players/read", "/v3/players/read"); + Rest.RegisterRedirect("/players/kick", "/v2/players/kick"); + Rest.RegisterRedirect("/players/ban", "/v2/players/ban"); + Rest.RegisterRedirect("/players/kill", "/v2/players/kill"); + Rest.RegisterRedirect("/players/mute", "/v2/players/mute"); + Rest.RegisterRedirect("/players/unmute", "/v2/players/unmute"); + + //group commands + Rest.RegisterRedirect("/groups/list", "/v2/groups/list"); + Rest.RegisterRedirect("/groups/read", "/v2/groups/read"); + Rest.RegisterRedirect("/groups/destroy", "/v2/groups/destroy"); + Rest.RegisterRedirect("/groups/create", "/v2/groups/create"); + Rest.RegisterRedirect("/groups/update", "/v2/groups/update"); + + 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)); @@ -139,8 +267,10 @@ namespace TShockAPI 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("/v3/world/bloomoon", WorldBloodmoonV3, 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("/v3/world/autosave", WorldChangeSaveSettingsV3, RestPermissions.restcfg)); Rest.Register(new SecureRestCommand("/v2/world/butcher", WorldButcher, RestPermissions.restbutcher)); // Player Commands @@ -161,7 +291,7 @@ namespace TShockAPI Rest.Register(new SecureRestCommand("/v2/groups/update", GroupUpdate, RestPermissions.restmanagegroups)); } - #region RestServerMethods + #region Rest Server Methods [Description("Executes a remote command on the server, and returns the output of the command.")] [RouteAttribute("/v3/server/rawcmd")] @@ -275,16 +405,6 @@ namespace TShockAPI }; } - [Description("Get a list of information about the current TShock server.")] - [Route("/status")] - [Token] - private object ServerStatusRoot(RestRequestArgs args) - { - RestObject status = (RestObject)ServerStatusV2(args); - status.Add("upgrade", "/v2/server/status"); - return status; - } - [Description("Get a list of information about the current TShock server.")] [Route("/v2/server/status")] [Token] @@ -351,7 +471,7 @@ namespace TShockAPI #endregion - #region RestUserMethods + #region Rest User Methods [Description("Returns the list of user accounts that are currently in use on the server.")] [Route("/v2/users/activelist")] @@ -502,7 +622,7 @@ namespace TShockAPI #endregion - #region RestBanMethods + #region Rest Ban Methods [Description("Create a new ban entry.")] [Route("/bans/create")] @@ -614,8 +734,8 @@ namespace TShockAPI #endregion - #region RestWorldMethods - + #region Rest World Methods + [Route("/v2/world/autosave/state/{state}")] [Permission(RestPermissions.restcfg)] [Verb("state", "The status for autosave.", typeof(bool))] @@ -627,7 +747,25 @@ namespace TShockAPI return RestInvalidParam("state"); TShock.Config.AutoSave = autoSave; - return RestResponse("AutoSave has been set to " + autoSave); + var resp = RestResponse("AutoSave has been set to " + autoSave); + resp.Add("upgrade", "/v3/world/autosave"); + return resp; + } + + [Route("/v3/world/autosave")] + [Permission(RestPermissions.restcfg)] + [Parameter("state", false, "The status for autosave.", typeof(bool))] + [Token] + private object WorldChangeSaveSettingsV3(RestRequestArgs args) + { + bool autoSave; + if (!bool.TryParse(args.Parameters["state"], out autoSave)) + { + return RestResponse($"Autosave is currently {(TShock.Config.AutoSave ? "enabled" : "disabled")}"); + } + TShock.Config.AutoSave = autoSave; + + return RestResponse($"AutoSave has been {(TShock.Config.AutoSave ? "enabled" : "disabled")}"); } [Description("Save the world.")] @@ -704,12 +842,31 @@ namespace TShockAPI return RestInvalidParam("bloodmoon"); Main.bloodMoon = bloodmoon; - return RestResponse("Blood Moon has been set to " + bloodmoon); + var resp = RestResponse("Blood Moon has been set to " + bloodmoon); + resp.Add("upgrade", "/v3/world/bloodmoon"); + return resp; + } + + [Description("Toggle the status of blood moon.")] + [Route("/v3/world/bloodmoon")] + [Permission(RestPermissions.restcauseevents)] + [Parameter("state", false, "Sets the state of the bloodmoon.", typeof(bool))] + [Token] + private object WorldBloodmoonV3(RestRequestArgs args) + { + bool bloodmoon; + if (!bool.TryParse(args.Verbs["state"], out bloodmoon)) + { + return RestResponse($"Bloodmoon state: {(Main.bloodMoon ? "Enabled" : "Disabled")}"); + } + Main.bloodMoon = bloodmoon; + + return RestResponse($"Blood Moon has been {(Main.bloodMoon ? "enabled" : "disabled")}"); } #endregion - #region RestPlayerMethods + #region Rest Player Methods [Description("Unmute a player.")] [Route("/v2/players/unmute")] @@ -844,7 +1001,7 @@ namespace TShockAPI #endregion - #region RestGroupMethods + #region Rest Group Methods [Description("View all groups in the TShock database.")] [Route("/v2/groups/list")]