Merge pull request #505 from CoderCow/patch-1

REST Security and General Improvements
This commit is contained in:
Lucas Nicodemus 2013-08-22 20:09:10 -07:00
commit f82bff1b17
12 changed files with 592 additions and 256 deletions

View file

@ -180,7 +180,7 @@ namespace TShockAPI
add(Permissions.ban, Ban, "ban"); add(Permissions.ban, Ban, "ban");
add(Permissions.whitelist, Whitelist, "whitelist"); add(Permissions.whitelist, Whitelist, "whitelist");
add(Permissions.maintenance, Off, "off", "exit"); 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, OffNoSave, "off-nosave", "exit-nosave");
add(Permissions.maintenance, CheckUpdates, "checkupdates"); add(Permissions.maintenance, CheckUpdates, "checkupdates");
add(Permissions.updateplugins, UpdatePlugins, "updateplugins"); add(Permissions.updateplugins, UpdatePlugins, "updateplugins");
@ -245,7 +245,8 @@ namespace TShockAPI
add(Permissions.savessi, OverrideSSI, "overridessi", "ossi"); add(Permissions.savessi, OverrideSSI, "overridessi", "ossi");
add(Permissions.xmas, ForceXmas, "forcexmas"); add(Permissions.xmas, ForceXmas, "forcexmas");
add(Permissions.settempgroup, TempGroup, "tempgroup"); add(Permissions.settempgroup, TempGroup, "tempgroup");
add(null, Aliases, "aliases"); add(null, Aliases, "aliases");
add(Rests.RestPermissions.restmanage, ManageRest, "rest");
//add(null, TestCallbackCommand, "test"); //add(null, TestCallbackCommand, "test");
TShockCommands = new ReadOnlyCollection<Command>(tshockCommands); TShockCommands = new ReadOnlyCollection<Command>(tshockCommands);
@ -1308,7 +1309,7 @@ namespace TShockAPI
string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!"); string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!");
TShock.Utils.StopServer(true, reason); TShock.Utils.StopServer(true, reason);
} }
//Added restart command
private static void Restart(CommandArgs args) private static void Restart(CommandArgs args)
{ {
if (Main.runningMono) if (Main.runningMono)
@ -1317,21 +1318,8 @@ namespace TShockAPI
} }
else else
{ {
if (TShock.Config.ServerSideInventory) string reason = ((args.Parameters.Count > 0) ? "Server shutting down: " + String.Join(" ", args.Parameters) : "Server shutting down!");
{ TShock.Utils.RestartServer(true, reason);
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);
} }
} }
@ -1352,7 +1340,59 @@ namespace TShockAPI
args.Player.SendInfoMessage("Starting plugin update process:"); args.Player.SendInfoMessage("Starting plugin update process:");
args.Player.SendInfoMessage("This may take a while, do not turn off the server!"); args.Player.SendInfoMessage("This may take a while, do not turn off the server!");
new PluginUpdaterThread(args.Player); new PluginUpdaterThread(args.Player);
} }
private static void ManageRest(CommandArgs args)
{
string subCommand = "help";
if (args.Parameters.Count > 0)
subCommand = args.Parameters[0];
switch(subCommand.ToLower())
{
case "listusers":
{
int pageNumber;
if (!PaginationTools.TryParsePageNumber(args.Parameters, 1, args.Player, out pageNumber))
return;
Dictionary<string,int> restUsersTokens = new Dictionary<string,int>();
foreach (Rests.SecureRest.TokenData tokenData in TShock.RestApi.Tokens.Values)
{
if (restUsersTokens.ContainsKey(tokenData.Username))
restUsersTokens[tokenData.Username]++;
else
restUsersTokens.Add(tokenData.Username, 1);
}
List<string> restUsers = new List<string>(
restUsersTokens.Select(ut => string.Format("{0} ({1} tokens)", ut.Key, ut.Value)));
PaginationTools.SendPage(
args.Player, pageNumber, PaginationTools.BuildLinesFromTerms(restUsers), new PaginationTools.Settings {
NothingToDisplayString = "There are currently no active REST users.",
HeaderFormat = "Active REST Users ({0}/{1}):",
FooterFormat = "Type /rest listusers {0} for more."
}
);
break;
}
case "destroytokens":
{
TShock.RestApi.Tokens.Clear();
args.Player.SendSuccessMessage("All REST tokens have been destroyed.");
break;
}
default:
{
args.Player.SendInfoMessage("Available REST Sub-Commands:");
args.Player.SendMessage("listusers - Lists all REST users and their current active tokens.", Color.White);
args.Player.SendMessage("destroytokens - Destroys all current REST tokens.", Color.White);
break;
}
}
}
#endregion Server Maintenence Commands #endregion Server Maintenence Commands
@ -2373,14 +2413,10 @@ namespace TShockAPI
private static void Reload(CommandArgs args) private static void Reload(CommandArgs args)
{ {
FileTools.SetupConfig(); TShock.Utils.Reload(args.Player);
TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs());
TShock.Groups.LoadPermisions();
TShock.Regions.ReloadAllRegions();
args.Player.SendSuccessMessage( args.Player.SendSuccessMessage(
"Configuration, permissions, and regions reload complete. Some changes may require a server restart."); "Configuration, permissions, and regions reload complete. Some changes may require a server restart.");
Hooks.GeneralHooks.OnReloadEvent(args.Player);
} }
private static void ServerPassword(CommandArgs args) private static void ServerPassword(CommandArgs args)

View file

@ -129,7 +129,7 @@ namespace TShockAPI
[Description("This will announce a player's location on join")] public bool EnableGeoIP; [Description("This will announce a player's location on join")] public bool EnableGeoIP;
[Description("This will turn on a token requirement for the /status API endpoint.")] public bool [Description("This will turn on token requirement for the public REST API endpoints.")] public bool
EnableTokenEndpointAuthentication; EnableTokenEndpointAuthentication;
[Description("Deprecated. Use ServerName instead.")] public string ServerNickname = "TShock Server"; [Description("Deprecated. Use ServerName instead.")] public string ServerNickname = "TShock Server";
@ -256,6 +256,11 @@ namespace TShockAPI
[Description("#.#.#. = Red/Blue/Green - RGB Colors for broadcasts. Max value: 255.")] public float[] BroadcastRGB = [Description("#.#.#. = Red/Blue/Green - RGB Colors for broadcasts. Max value: 255.")] public float[] BroadcastRGB =
{127,255,212}; {127,255,212};
// TODO: Get rid of this when the old REST permission model is removed.
[Description(
"Whether the REST API should use the new permission model. Note: The old permission model will become depracted in the future."
)] public bool RestUseNewPermissionModel = true;
/// <summary> /// <summary>
/// Reads a configuration file from a given path /// Reads a configuration file from a given path
/// </summary> /// </summary>

View file

@ -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 [Description("Allows you to bypass the max slots for up to 5 slots above your max")] public static readonly string
reservedslot; 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; [Description("User can kick others")] public static readonly string kick;
@ -166,9 +166,6 @@ namespace TShockAPI
[Description("User can save all the players SSI state.")] [Description("User can save all the players SSI state.")]
public static readonly string savessi; public static readonly string savessi;
[Description("User can use rest api calls.")]
public static readonly string restapi;
[Description("User can force the server to Christmas mode.")] public static readonly string xmas; [Description("User can force the server to Christmas mode.")] public static readonly string xmas;
[Description("User can use /home.")] public static readonly string home; [Description("User can use /home.")] public static readonly string home;
@ -177,16 +174,14 @@ namespace TShockAPI
[Description("User can elevate other users' groups temporarily.")] public static readonly string settempgroup; [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; [Description("User can download updates to plugins that are currently running.")] public static readonly string updateplugins;
static Permissions()
static Permissions()
{ {
foreach (var field in typeof (Permissions).GetFields()) foreach (var field in typeof (Permissions).GetFields())
{ {
field.SetValue(null, field.Name); field.SetValue(null, field.Name);
} }
//Backwards compatability.
restapi = "api";
} }
/// <summary> /// <summary>

View file

@ -36,7 +36,16 @@ namespace Rests
/// <param name="parameters">Parameters in the url</param> /// <param name="parameters">Parameters in the url</param>
/// <param name="verbs">{x} in urltemplate</param> /// <param name="verbs">{x} in urltemplate</param>
/// <returns>Response object or null to not handle request</returns> /// <returns>Response object or null to not handle request</returns>
public delegate object RestCommandD(RestVerbs verbs, IParameterCollection parameters); public delegate object RestCommandD(RestVerbs verbs, IParameterCollection parameters);
/// <summary>
/// Secure Rest command delegate including token data.
/// </summary>
/// <param name="parameters">Parameters in the url</param>
/// <param name="verbs">{x} in urltemplate</param>
/// <param name="tokenData">The data of stored for the provided token.</param>
/// <returns>Response object or null to not handle request</returns>
public delegate object SecureRestCommandD(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData);
public class Rest : IDisposable public class Rest : IDisposable
{ {
@ -165,24 +174,47 @@ namespace Rests
} }
catch (Exception exception) catch (Exception exception)
{ {
return new Dictionary<string, string> return new RestObject("500")
{ {
{"status", "500"},
{"error", "Internal server error."}, {"error", "Internal server error."},
{"errormsg", exception.Message}, {"errormsg", exception.Message},
{"stacktrace", exception.StackTrace}, {"stacktrace", exception.StackTrace},
}; };
} }
return new Dictionary<string, string> return new RestObject("404")
{ {
{"status", "404"},
{"error", "Specified API endpoint doesn't exist. Refer to the documentation for a list of valid endpoints."} {"error", "Specified API endpoint doesn't exist. Refer to the documentation for a list of valid endpoints."}
}; };
} }
protected virtual object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms) 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);
char separator = '?';
foreach (IParameter paramImpl in parms)
{
Parameter param = (paramImpl as Parameter);
if (param == null || (!includeToken && param.Name.Equals("token", StringComparison.InvariantCultureIgnoreCase)))
continue;
requestBuilder.Append(separator);
requestBuilder.Append(param.Name);
requestBuilder.Append('=');
requestBuilder.Append(param.Value);
separator = '&';
}
return requestBuilder.ToString();
} }
#region Dispose #region Dispose

View file

@ -16,8 +16,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using HttpServer;
namespace Rests namespace Rests
{ {
public class RestCommand public class RestCommand
@ -26,8 +27,10 @@ namespace Rests
public string UriTemplate { get; protected set; } public string UriTemplate { get; protected set; }
public string UriVerbMatch { get; protected set; } public string UriVerbMatch { get; protected set; }
public string[] UriVerbs { get; protected set; } public string[] UriVerbs { get; protected set; }
public RestCommandD Callback { get; protected set; } public virtual bool RequiresToken { get { return false; } }
public bool RequiresToken { get; set; } public bool DoLog { get; set; }
private RestCommandD callback;
/// <summary> /// <summary>
/// ///
@ -42,8 +45,8 @@ namespace Rests
UriVerbMatch = string.Format("^{0}$", string.Join("([^/]*)", Regex.Split(uritemplate, "\\{[^\\{\\}]*\\}"))); UriVerbMatch = string.Format("^{0}$", string.Join("([^/]*)", Regex.Split(uritemplate, "\\{[^\\{\\}]*\\}")));
var matches = Regex.Matches(uritemplate, "\\{([^\\{\\}]*)\\}"); var matches = Regex.Matches(uritemplate, "\\{([^\\{\\}]*)\\}");
UriVerbs = (from Match match in matches select match.Groups[1].Value).ToArray(); UriVerbs = (from Match match in matches select match.Groups[1].Value).ToArray();
Callback = callback; this.callback = callback;
RequiresToken = true; DoLog = true;
} }
/// <summary> /// <summary>
@ -59,6 +62,44 @@ namespace Rests
public bool HasVerbs public bool HasVerbs
{ {
get { return UriVerbs.Length > 0; } 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);
}
} }
} }

View file

@ -17,7 +17,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using HttpServer; using HttpServer;
using Rests; using Rests;
@ -37,92 +38,175 @@ namespace TShockAPI
public void RegisterRestfulCommands() public void RegisterRestfulCommands()
{ {
// Server Commands // Server Commands
Rest.Register(new RestCommand("/v2/server/broadcast", ServerBroadcast)); if (TShock.Config.EnableTokenEndpointAuthentication)
Rest.Register(new RestCommand("/v2/server/off", ServerOff)); {
Rest.Register(new RestCommand("/v2/server/rawcmd", ServerCommand)); Rest.Register(new SecureRestCommand("/v2/server/status", ServerStatusV2));
Rest.Register(new RestCommand("/v2/server/status", ServerStatusV2) { RequiresToken = false }); Rest.Register(new SecureRestCommand("/status", ServerStatus));
Rest.Register(new RestCommand("/tokentest", ServerTokenTest)); Rest.Register(new SecureRestCommand("/v3/server/motd", ServerMotd));
Rest.Register(new RestCommand("/status", ServerStatus) { RequiresToken = false }); Rest.Register(new SecureRestCommand("/v3/server/rules", ServerRules));
}
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)));
Rest.Register(new RestCommand("/v3/server/motd", (a, b) => this.ServerMotd(a, b, SecureRest.TokenData.None)));
Rest.Register(new RestCommand("/v3/server/rules", (a, b) => this.ServerRules(a, b, SecureRest.TokenData.None)));
}
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 // User Commands
Rest.Register(new RestCommand("/v2/users/activelist", UserActiveListV2)); Rest.Register(new SecureRestCommand("/v2/users/activelist", UserActiveListV2, RestPermissions.restviewusers));
Rest.Register(new RestCommand("/v2/users/create", UserCreateV2)); Rest.Register(new SecureRestCommand("/v2/users/create", UserCreateV2, RestPermissions.restmanageusers) { DoLog = false });
Rest.Register(new RestCommand("/v2/users/list", UserListV2)); Rest.Register(new SecureRestCommand("/v2/users/list", UserListV2, RestPermissions.restviewusers));
Rest.Register(new RestCommand("/v2/users/read", UserInfoV2)); Rest.Register(new SecureRestCommand("/v2/users/read", UserInfoV2, RestPermissions.restviewusers));
Rest.Register(new RestCommand("/v2/users/destroy", UserDestroyV2)); Rest.Register(new SecureRestCommand("/v2/users/destroy", UserDestroyV2, RestPermissions.restmanageusers));
Rest.Register(new RestCommand("/v2/users/update", UserUpdateV2)); Rest.Register(new SecureRestCommand("/v2/users/update", UserUpdateV2, RestPermissions.restmanageusers) { DoLog = false });
// Ban Commands // Ban Commands
Rest.Register(new RestCommand("/bans/create", BanCreate)); Rest.Register(new SecureRestCommand("/bans/create", BanCreate, RestPermissions.restmanagebans));
Rest.Register(new RestCommand("/v2/bans/list", BanListV2)); Rest.Register(new SecureRestCommand("/v2/bans/list", BanListV2, RestPermissions.restviewbans));
Rest.Register(new RestCommand("/v2/bans/read", BanInfoV2)); Rest.Register(new SecureRestCommand("/v2/bans/read", BanInfoV2, RestPermissions.restviewbans));
Rest.Register(new RestCommand("/v2/bans/destroy", BanDestroyV2)); Rest.Register(new SecureRestCommand("/v2/bans/destroy", BanDestroyV2, RestPermissions.restmanagebans));
// World Commands // World Commands
Rest.Register(new RestCommand("/world/read", WorldRead)); Rest.Register(new SecureRestCommand("/world/read", WorldRead));
Rest.Register(new RestCommand("/world/meteor", WorldMeteor)); Rest.Register(new SecureRestCommand("/world/meteor", WorldMeteor, RestPermissions.restcauseevents));
Rest.Register(new RestCommand("/world/bloodmoon/{bool}", WorldBloodmoon)); Rest.Register(new SecureRestCommand("/world/bloodmoon/{bool}", WorldBloodmoon, RestPermissions.restcauseevents));
Rest.Register(new RestCommand("/v2/world/save", WorldSave)); Rest.Register(new SecureRestCommand("/v2/world/save", WorldSave, RestPermissions.restcfg));
Rest.Register(new RestCommand("/v2/world/autosave/state/{bool}", WorldChangeSaveSettings)); Rest.Register(new SecureRestCommand("/v2/world/autosave/state/{bool}", WorldChangeSaveSettings, RestPermissions.restcfg));
Rest.Register(new RestCommand("/v2/world/butcher", WorldButcher)); Rest.Register(new SecureRestCommand("/v2/world/butcher", WorldButcher, RestPermissions.restbutcher));
// Player Commands // Player Commands
Rest.Register(new RestCommand("/lists/players", PlayerList)); Rest.Register(new SecureRestCommand("/lists/players", PlayerList));
Rest.Register(new RestCommand("/v2/players/list", PlayerListV2)); Rest.Register(new SecureRestCommand("/v2/players/list", PlayerListV2));
Rest.Register(new RestCommand("/v2/players/read", PlayerReadV2)); Rest.Register(new SecureRestCommand("/v2/players/read", PlayerReadV2, RestPermissions.restuserinfo));
Rest.Register(new RestCommand("/v2/players/kick", PlayerKickV2)); Rest.Register(new SecureRestCommand("/v2/players/kick", PlayerKickV2, RestPermissions.restkick));
Rest.Register(new RestCommand("/v2/players/ban", PlayerBanV2)); Rest.Register(new SecureRestCommand("/v2/players/ban", PlayerBanV2, RestPermissions.restban, RestPermissions.restmanagebans));
Rest.Register(new RestCommand("/v2/players/kill", PlayerKill)); Rest.Register(new SecureRestCommand("/v2/players/kill", PlayerKill, RestPermissions.restkill));
Rest.Register(new RestCommand("/v2/players/mute", PlayerMute)); Rest.Register(new SecureRestCommand("/v2/players/mute", PlayerMute, RestPermissions.restmute));
Rest.Register(new RestCommand("/v2/players/unmute", PlayerUnMute)); Rest.Register(new SecureRestCommand("/v2/players/unmute", PlayerUnMute, RestPermissions.restmute));
// Group Commands // Group Commands
Rest.Register(new RestCommand("/v2/groups/list", GroupList)); Rest.Register(new SecureRestCommand("/v2/groups/list", GroupList, RestPermissions.restviewgroups));
Rest.Register(new RestCommand("/v2/groups/read", GroupInfo)); Rest.Register(new SecureRestCommand("/v2/groups/read", GroupInfo, RestPermissions.restviewgroups));
Rest.Register(new RestCommand("/v2/groups/destroy", GroupDestroy)); Rest.Register(new SecureRestCommand("/v2/groups/destroy", GroupDestroy, RestPermissions.restmanagegroups));
Rest.Register(new RestCommand("/v2/groups/create", GroupCreate)); Rest.Register(new SecureRestCommand("/v2/groups/create", GroupCreate, RestPermissions.restmanagegroups));
Rest.Register(new RestCommand("/v2/groups/update", GroupUpdate)); Rest.Register(new SecureRestCommand("/v2/groups/update", GroupUpdate, RestPermissions.restmanagegroups));
} }
#region RestServerMethods #region RestServerMethods
private object ServerCommand(RestVerbs verbs, IParameterCollection parameters) private object ServerCommand(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
if (string.IsNullOrWhiteSpace(parameters["cmd"])) if (string.IsNullOrWhiteSpace(parameters["cmd"]))
return RestMissingParam("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(tokenData.UserGroupName);
else
restPlayerGroup = new SuperAdminGroup();
TSRestPlayer tr = new TSRestPlayer(); TSRestPlayer tr = new TSRestPlayer(tokenData.Username, restPlayerGroup);
Commands.HandleCommand(tr, parameters["cmd"]); Commands.HandleCommand(tr, parameters["cmd"]);
return RestResponse(string.Join("\n", tr.GetCommandOutput())); 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");
Group restPlayerGroup;
// TODO: Get rid of this when the old REST permission model is removed.
if (TShock.Config.RestUseNewPermissionModel)
restPlayerGroup = TShock.Groups.GetGroupByName(tokenData.UserGroupName);
else
restPlayerGroup = new SuperAdminGroup();
TSRestPlayer tr = new TSRestPlayer(tokenData.Username, restPlayerGroup);
Commands.HandleCommand(tr, parameters["cmd"]);
return new RestObject()
{
{"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)) if (!GetBool(parameters["confirm"], false))
return RestInvalidParam("confirm"); return RestInvalidParam("confirm");
// Inform players the server is shutting down // Inform players the server is shutting down
var msg = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; var reason = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"];
TShock.Utils.StopServer(!GetBool(parameters["nosave"], false), msg); TShock.Utils.StopServer(!GetBool(parameters["nosave"], false), reason);
return RestResponse("The server is shutting down"); 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 restarting" : 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)
{
TShock.Utils.Reload(new TSRestPlayer(tokenData.Username, TShock.Groups.GetGroupByName(tokenData.UserGroupName)));
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"]; var msg = parameters["msg"];
if (string.IsNullOrWhiteSpace(msg)) if (string.IsNullOrWhiteSpace(msg))
return RestMissingParam("msg"); return RestMissingParam("msg");
TShock.Utils.Broadcast(msg); TShock.Utils.Broadcast(msg);
return RestResponse("The message was broadcasted successfully"); return RestResponse("The message was broadcasted successfully");
}
private object ServerMotd(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{
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)}
};
}
private object ServerRules(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{
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)}
};
} }
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(); var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
return new RestObject() return new RestObject()
{ {
@ -133,18 +217,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() var ret = new RestObject()
{ {
{"name", TShock.Config.ServerName}, {"name", TShock.Config.ServerName},
{"port", TShock.Config.ServerPort}, {"port", TShock.Config.ServerPort},
{"playercount", Main.player.Where(p => null != p && p.active).Count()}, {"playercount", Main.player.Where(p => null != p && p.active).Count()},
{"maxplayers", TShock.Config.MaxSlots}, {"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)) if (GetBool(parameters["players"], false))
@ -173,28 +256,33 @@ namespace TShockAPI
rules.Add("HardcoreOnly", TShock.Config.HardcoreOnly); rules.Add("HardcoreOnly", TShock.Config.HardcoreOnly);
rules.Add("PvPMode", TShock.Config.PvPMode); rules.Add("PvPMode", TShock.Config.PvPMode);
rules.Add("SpawnProtection", TShock.Config.SpawnProtection); 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); ret.Add("rules", rules);
} }
return ret; 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"); return new RestObject()
{
{"response", "Token is valid and was passed through correctly."},
{"associateduser", tokenData.Username}
};
} }
#endregion #endregion
#region RestUserMethods #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)) } }; 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<string,object>(){ return new RestObject() { { "users", TShock.Users.GetUsers().Select(p => new Dictionary<string,object>(){
{"name", p.Name}, {"name", p.Name},
@ -204,7 +292,7 @@ namespace TShockAPI
}) } }; }) } };
} }
private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters) private object UserCreateV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var username = parameters["user"]; var username = parameters["user"];
if (string.IsNullOrWhiteSpace(username)) if (string.IsNullOrWhiteSpace(username))
@ -232,7 +320,7 @@ namespace TShockAPI
return RestResponse("User was successfully created"); 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); var ret = UserFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -274,7 +362,7 @@ namespace TShockAPI
return response; return response;
} }
private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters) private object UserDestroyV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var ret = UserFind(parameters); var ret = UserFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -292,7 +380,7 @@ namespace TShockAPI
return RestResponse("User deleted successfully"); 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); var ret = UserFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -306,7 +394,7 @@ namespace TShockAPI
#region RestBanMethods #region RestBanMethods
private object BanCreate(RestVerbs verbs, IParameterCollection parameters) private object BanCreate(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var ip = parameters["ip"]; var ip = parameters["ip"];
var name = parameters["name"]; var name = parameters["name"];
@ -325,7 +413,7 @@ namespace TShockAPI
return RestResponse("Ban created successfully"); 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); var ret = BanFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -357,7 +445,7 @@ namespace TShockAPI
return RestResponse("Ban deleted successfully"); 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); var ret = BanFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -371,7 +459,7 @@ namespace TShockAPI
}; };
} }
private object BanListV2(RestVerbs verbs, IParameterCollection parameters) private object BanListV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var banList = new ArrayList(); var banList = new ArrayList();
foreach (var ban in TShock.Bans.GetBans()) foreach (var ban in TShock.Bans.GetBans())
@ -393,7 +481,7 @@ namespace TShockAPI
#region RestWorldMethods #region RestWorldMethods
private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters) private object WorldChangeSaveSettings(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
bool autoSave; bool autoSave;
if (!bool.TryParse(verbs["bool"], out autoSave)) if (!bool.TryParse(verbs["bool"], out autoSave))
@ -403,14 +491,14 @@ namespace TShockAPI
return RestResponse("AutoSave has been set to " + autoSave); 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(); SaveManager.Instance.SaveWorld();
return RestResponse("World saved"); return RestResponse("World saved");
} }
private object WorldButcher(RestVerbs verbs, IParameterCollection parameters) private object WorldButcher(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
bool killFriendly; bool killFriendly;
if (!bool.TryParse(parameters["killfriendly"], out killFriendly)) if (!bool.TryParse(parameters["killfriendly"], out killFriendly))
@ -432,7 +520,7 @@ namespace TShockAPI
return RestResponse(killcount + " NPCs have been killed"); 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() return new RestObject()
{ {
@ -445,7 +533,7 @@ namespace TShockAPI
}; };
} }
private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters) private object WorldMeteor(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
if (null == WorldGen.genRand) if (null == WorldGen.genRand)
WorldGen.genRand = new Random(); WorldGen.genRand = new Random();
@ -453,7 +541,7 @@ namespace TShockAPI
return RestResponse("Meteor has been spawned"); 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; bool bloodmoon;
if (!bool.TryParse(verbs["bool"], out bloodmoon)) if (!bool.TryParse(verbs["bool"], out bloodmoon))
@ -467,23 +555,23 @@ namespace TShockAPI
#region RestPlayerMethods #region RestPlayerMethods
private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters) private object PlayerUnMute(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
return PlayerSetMute(parameters, false); 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); 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(); var activeplayers = Main.player.Where(p => null != p && p.active).ToList();
return new RestObject() { { "players", string.Join(", ", activeplayers.Select(p => p.name)) } }; 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(); var playerList = new ArrayList();
foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p)) foreach (TSPlayer tsPlayer in TShock.Players.Where(p => null != p))
@ -495,7 +583,7 @@ namespace TShockAPI
return new RestObject() { { "players", playerList } }; 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); var ret = PlayerFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -515,7 +603,7 @@ namespace TShockAPI
}; };
} }
private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters) private object PlayerKickV2(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var ret = PlayerFind(parameters); var ret = PlayerFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -526,7 +614,7 @@ namespace TShockAPI
return RestResponse("Player " + player.Name + " was kicked"); 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); var ret = PlayerFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -539,7 +627,7 @@ namespace TShockAPI
return RestResponse("Player " + player.Name + " was banned"); 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); var ret = PlayerFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -556,7 +644,7 @@ namespace TShockAPI
#region RestGroupMethods #region RestGroupMethods
private object GroupList(RestVerbs verbs, IParameterCollection parameters) private object GroupList(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var groups = new ArrayList(); var groups = new ArrayList();
foreach (Group group in TShock.Groups) foreach (Group group in TShock.Groups)
@ -566,7 +654,7 @@ namespace TShockAPI
return new RestObject() { { "groups", groups } }; 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); var ret = GroupFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -583,7 +671,7 @@ namespace TShockAPI
}; };
} }
private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters) private object GroupDestroy(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var ret = GroupFind(parameters); var ret = GroupFind(parameters);
if (ret is RestObject) if (ret is RestObject)
@ -602,7 +690,7 @@ namespace TShockAPI
return RestResponse("Group '" + group.Name + "' deleted successfully"); 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"]; var name = parameters["group"];
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
@ -619,7 +707,7 @@ namespace TShockAPI
return RestResponse("Group '" + name + "' created successfully"); 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); var ret = GroupFind(parameters);
if (ret is RestObject) if (ret is RestObject)

View file

@ -0,0 +1,93 @@
/*
TShock, a server mod for Terraria
Copyright (C) 2011-2012 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.ComponentModel;
using System.Linq;
namespace Rests
{
public static class RestPermissions
{
[Description("User can create REST tokens.")]
public static readonly string restapi;
[Description("User or REST user can destroy all REST tokens.")]
public static readonly string restmanage;
[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;
static RestPermissions()
{
foreach (var field in typeof (RestPermissions).GetFields())
{
field.SetValue(null, field.Name);
}
}
}
}

View file

@ -18,38 +18,66 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using HttpServer; using System.Text;
using HttpServer;
using TShockAPI;
using TShockAPI.DB;
namespace Rests namespace Rests
{ {
/// <summary>
///
/// </summary>
/// <param name="username">Username to verify</param>
/// <param name="password">Password to verify</param>
/// <returns>Returning a restobject with a null error means a successful verification.</returns>
public delegate RestObject VerifyD(string username, string password);
public class SecureRest : Rest public class SecureRest : Rest
{ {
public Dictionary<string, object> Tokens { get; protected set; } public struct TokenData
public event VerifyD Verify; {
public static readonly TokenData None = default(TokenData);
public string Username { get; set; }
public string UserGroupName { get; set; }
}
public Dictionary<string,TokenData> Tokens { get; protected set; }
public SecureRest(IPAddress ip, int port) public SecureRest(IPAddress ip, int port)
: base(ip, port) : base(ip, port)
{ {
Tokens = new Dictionary<string, object>(); Tokens = new Dictionary<string, TokenData>();
Register(new RestCommand("/token/create/{username}/{password}", NewToken) {RequiresToken = false});
Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { RequiresToken = false }); Register(new RestCommand("/token/create/{username}/{password}", NewToken) { DoLog = false });
Register(new RestCommand("/token/destroy/{token}", DestroyToken) {RequiresToken = true}); Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { DoLog = false });
foreach (KeyValuePair<string, string> t in TShockAPI.TShock.RESTStartupTokens) Register(new SecureRestCommand("/token/destroy/{token}", DestroyToken));
Register(new SecureRestCommand("/v3/token/destroy/all", DestroyAllTokens, RestPermissions.restmanage));
foreach (KeyValuePair<string, TokenData> t in TShockAPI.TShock.RESTStartupTokens)
{ {
Tokens.Add(t.Key, t.Value); Tokens.Add(t.Key, t.Value);
} }
// TODO: Get rid of this when the old REST permission model is removed.
if (!TShock.Config.RestUseNewPermissionModel)
{
string warningMessage = string.Concat(
"You're using the old REST permission model which is highly vulnerable in matter of security. ",
"The old model will be removed with the next maintenance release of TShock. In order to switch to the new model, ",
"change the config setting \"RestUseNewPermissionModel\" to true."
);
Log.Warn(warningMessage);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(warningMessage);
Console.ForegroundColor = ConsoleColor.Gray;
}
else
{
string warningMessage = string.Concat(
"You're using the new more secure REST permission model which can lead to compatibility problems ",
"with existing REST services. If compatibility problems occur, you can switch back to the unsecure permission ",
"model by changing the config setting \"RestUseNewPermissionModel\" to false, which is not recommended."
);
Log.ConsoleInfo(warningMessage);
}
} }
private object DestroyToken(RestVerbs verbs, IParameterCollection parameters) private object DestroyToken(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{ {
var token = verbs["token"]; var token = verbs["token"];
try try
@ -58,11 +86,19 @@ namespace Rests
} }
catch (Exception) catch (Exception)
{ {
return new Dictionary<string, string> return new RestObject("400")
{{"status", "400"}, {"error", "The specified token queued for destruction failed to be deleted."}}; { Error = "The specified token queued for destruction failed to be deleted." };
} }
return new Dictionary<string, string> return new RestObject()
{{"status", "200"}, {"response", "Requested token was successfully destroyed."}}; { Response = "Requested token was successfully destroyed." };
}
private object DestroyAllTokens(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{
Tokens.Clear();
return new RestObject()
{ Response = "All tokens were successfully destroyed." };
} }
private object NewTokenV2(RestVerbs verbs, IParameterCollection parameters) private object NewTokenV2(RestVerbs verbs, IParameterCollection parameters)
@ -70,29 +106,7 @@ namespace Rests
var user = parameters["username"]; var user = parameters["username"];
var pass = verbs["password"]; var pass = verbs["password"];
RestObject obj = null; return this.NewTokenInternal(user, pass);
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;
} }
private object NewToken(RestVerbs verbs, IParameterCollection parameters) private object NewToken(RestVerbs verbs, IParameterCollection parameters)
@ -100,55 +114,84 @@ namespace Rests
var user = verbs["username"]; var user = verbs["username"];
var pass = verbs["password"]; var pass = verbs["password"];
RestObject obj = null; RestObject response = this.NewTokenInternal(user, pass);
if (Verify != null) response["deprecated"] = "This endpoint is depracted and will be removed in the future.";
obj = Verify(user, pass); return response;
}
if (obj == null)
obj = new RestObject("401") private RestObject NewTokenInternal(string username, string password)
{Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair."}; {
User userAccount = TShock.Users.GetUserByName(username);
if (obj.Error != null) if (userAccount == null || !string.IsNullOrWhiteSpace(userAccount.Address))
return obj; return new RestObject("401")
{ Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." };
string hash;
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(RestPermissions.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 rand = new Random();
var randbytes = new byte[32]; var randbytes = new byte[32];
do do
{ {
rand.NextBytes(randbytes); rand.NextBytes(randbytes);
hash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2")); tokenHash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2"));
} while (Tokens.ContainsKey(hash)); } while (Tokens.ContainsKey(tokenHash));
Tokens.Add(hash, user); Tokens.Add(tokenHash, new TokenData { Username = userAccount.Name, UserGroupName = userGroup.Name });
obj["token"] = hash; RestObject response = new RestObject() { Response = "Successful login" };
obj["deprecated"] = "This method will be removed from TShock in 3.6."; response["token"] = tokenHash;
return obj; return response;
} }
protected override object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms) protected override object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms)
{ {
if (cmd.RequiresToken) if (!cmd.RequiresToken)
{ return base.ExecuteCommand(cmd, verbs, parms);
var strtoken = parms["token"];
if (strtoken == null) var token = parms["token"];
return new Dictionary<string, string> if (token == null)
{{"status", "401"}, {"error", "Not authorized. The specified API endpoint requires a token."}}; return new RestObject("401")
{ Error = "Not authorized. The specified API endpoint requires a token." };
object token;
if (!Tokens.TryGetValue(strtoken, out token)) SecureRestCommand secureCmd = (SecureRestCommand)cmd;
return new Dictionary<string, string> TokenData tokenData;
{ if (!Tokens.TryGetValue(token, out tokenData))
{"status", "403"}, return new RestObject("403")
{ { Error = "Not authorized. The specified API endpoint requires a token, but the provided token was not valid." };
"error",
"Not authorized. The specified API endpoint requires a token, but the provided token was not valid." // TODO: Get rid of this when the old REST permission model is removed.
} if (TShock.Config.RestUseNewPermissionModel) {
}; Group userGroup = TShock.Groups.GetGroupByName(tokenData.UserGroupName);
} if (userGroup == null)
return base.ExecuteCommand(cmd, verbs, parms); {
Tokens.Remove(token);
return new RestObject("403")
{ Error = "Not authorized. The provided token became invalid due to group changes, please create a new token." };
}
if (secureCmd.Permissions.Length > 0 && secureCmd.Permissions.All(perm => !userGroup.HasPermission(perm)))
{
return new RestObject("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;
} }
} }
} }

View file

@ -759,13 +759,13 @@ namespace TShockAPI
} }
} }
public class TSRestPlayer : TSServerPlayer public class TSRestPlayer : TSPlayer
{ {
internal List<string> CommandReturn = new List<string>(); internal List<string> CommandOutput = new List<string>();
public TSRestPlayer() public TSRestPlayer(string playerName, Group playerGroup): base(playerName)
{ {
Group = new SuperAdminGroup(); Group = playerGroup;
AwaitingResponse = new Dictionary<string, Action<object>>(); AwaitingResponse = new Dictionary<string, Action<object>>();
} }
@ -781,7 +781,7 @@ namespace TShockAPI
public override void SendMessage(string msg, byte red, byte green, byte blue) 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) public override void SendInfoMessage(string msg)
@ -806,7 +806,7 @@ namespace TShockAPI
public List<string> GetCommandOutput() public List<string> GetCommandOutput()
{ {
return CommandReturn; return this.CommandOutput;
} }
} }

View file

@ -72,7 +72,7 @@ namespace TShockAPI
/// <summary> /// <summary>
/// Used for implementing REST Tokens prior to the REST system starting up. /// Used for implementing REST Tokens prior to the REST system starting up.
/// </summary> /// </summary>
public static Dictionary<string, string> RESTStartupTokens = new Dictionary<string, string>(); public static Dictionary<string, SecureRest.TokenData> RESTStartupTokens = new Dictionary<string, SecureRest.TokenData>();
/// <summary> /// <summary>
/// Called after TShock is initialized. Useful for plugins that needs hooks before tshock but also depend on tshock being loaded. /// 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); RememberedPos = new RememberedPosManager(DB);
InventoryDB = new InventoryManager(DB); InventoryDB = new InventoryManager(DB);
RestApi = new SecureRest(Netplay.serverListenIP, Config.RestApiPort); RestApi = new SecureRest(Netplay.serverListenIP, Config.RestApiPort);
RestApi.Verify += RestApi_Verify;
RestApi.Port = Config.RestApiPort; RestApi.Port = Config.RestApiPort;
RestManager = new RestManager(RestApi); RestManager = new RestManager(RestApi);
RestManager.RegisterRestfulCommands(); RestManager.RegisterRestfulCommands();
@ -294,33 +293,6 @@ namespace TShockAPI
// ReSharper restore LocalizableElement // 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) protected override void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
@ -507,7 +479,7 @@ namespace TShockAPI
break; break;
case "-rest-token": case "-rest-token":
string token = Convert.ToString(parms[++i]); string token = Convert.ToString(parms[++i]);
RESTStartupTokens.Add(token, "null"); RESTStartupTokens.Add(token, new SecureRest.TokenData { Username = "null", UserGroupName = "superadmin" });
Console.WriteLine("Startup parameter overrode REST token."); Console.WriteLine("Startup parameter overrode REST token.");
break; break;
case "-rest-enabled": case "-rest-enabled":

View file

@ -89,6 +89,7 @@
<Compile Include="PluginUpdater\PluginUpdaterThread.cs" /> <Compile Include="PluginUpdater\PluginUpdaterThread.cs" />
<Compile Include="PluginUpdater\PluginVersionCheck.cs" /> <Compile Include="PluginUpdater\PluginVersionCheck.cs" />
<Compile Include="PluginUpdater\VersionInfo.cs" /> <Compile Include="PluginUpdater\VersionInfo.cs" />
<Compile Include="Rest\RestPermissions.cs" />
<Compile Include="SaveManager.cs" /> <Compile Include="SaveManager.cs" />
<Compile Include="DB\BanManager.cs" /> <Compile Include="DB\BanManager.cs" />
<Compile Include="DB\InventoryManager.cs" /> <Compile Include="DB\InventoryManager.cs" />
@ -191,7 +192,7 @@
</PropertyGroup> </PropertyGroup>
<ProjectExtensions> <ProjectExtensions>
<VisualStudio> <VisualStudio>
<UserProperties BuildVersion_IncrementBeforeBuild="False" BuildVersion_StartDate="2011/6/17" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_BuildAction="Both" BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" /> <UserProperties BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" BuildVersion_BuildAction="Both" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_StartDate="2011/6/17" BuildVersion_IncrementBeforeBuild="False" />
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View file

@ -560,6 +560,36 @@ namespace TShockAPI
// Disconnect after kick as that signifies server is exiting and could cause a race // Disconnect after kick as that signifies server is exiting and could cause a race
Netplay.disconnect = true; Netplay.disconnect = true;
}
/// <summary>
/// Stops the server after kicking all players with a reason message, and optionally saving the world then attempts to
/// restart it.
/// </summary>
/// <param name="save">bool perform a world save before stop (default: true)</param>
/// <param name="reason">string reason (default: "Server shutting down!")</param>
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);
}
/// <summary>
/// Reloads all configuration settings, groups, regions and raises the reload event.
/// </summary>
public void Reload(TSPlayer player)
{
FileTools.SetupConfig();
TShock.HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs());
TShock.Groups.LoadPermisions();
TShock.Regions.ReloadAllRegions();
Hooks.GeneralHooks.OnReloadEvent(player);
} }
#if COMPAT_SIGS #if COMPAT_SIGS