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")]