-Added REST per-endpoint permissions.

-Added REST endpoint "/v2/server/restart".
-Added REST endpoint "/v2/server/reload".
-Added REST endpoint "/v3/server/rawcmd", will output all returned lines as an array instead.
-Added "uptime", "serverpassword", "rules/ServerSideInventory" fields to REST endpoint "/v2/server/status".
-REST requests are now logged.
-Endpoint "/v2/server/rawcmd" does now check whether the user has the sufficient permission to execute the command.
-Fixed Config.EnableTokenEndpointAuthentication not working properly before.
-Removed obsolete "api" permission (only "restapi" now).
This commit is contained in:
CoderCow 2013-07-25 12:31:11 +02:00
parent 4e7b497ae4
commit 0ea83746cf
9 changed files with 415 additions and 237 deletions

View file

@ -18,38 +18,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using HttpServer;
using System.Net;
using System.Text;
using HttpServer;
using TShockAPI;
using TShockAPI.DB;
namespace Rests
{
/// <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 Dictionary<string, object> Tokens { get; protected set; }
public event VerifyD Verify;
{
public struct TokenData
{
public static readonly TokenData None = default(TokenData);
public string Username { get; set; }
public Group UserGroup { get; set; }
}
public Dictionary<string,TokenData> Tokens { get; protected set; }
public SecureRest(IPAddress ip, int port)
: base(ip, port)
{
Tokens = new Dictionary<string, object>();
Register(new RestCommand("/token/create/{username}/{password}", NewToken) {RequiresToken = false});
Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { RequiresToken = false });
Register(new RestCommand("/token/destroy/{token}", DestroyToken) {RequiresToken = true});
foreach (KeyValuePair<string, string> t in TShockAPI.TShock.RESTStartupTokens)
Tokens = new Dictionary<string, TokenData>();
Register(new RestCommand("/token/create/{username}/{password}", NewToken) { DoLog = false });
Register(new RestCommand("/v2/token/create/{password}", NewTokenV2) { DoLog = false });
Register(new SecureRestCommand("/token/destroy/{token}", DestroyToken));
foreach (KeyValuePair<string, TokenData> t in TShockAPI.TShock.RESTStartupTokens)
{
Tokens.Add(t.Key, t.Value);
}
}
private object DestroyToken(RestVerbs verbs, IParameterCollection parameters)
private object DestroyToken(RestVerbs verbs, IParameterCollection parameters, SecureRest.TokenData tokenData)
{
var token = verbs["token"];
try
@ -70,29 +74,7 @@ namespace Rests
var user = parameters["username"];
var pass = verbs["password"];
RestObject obj = null;
if (Verify != null)
obj = Verify(user, pass);
if (obj == null)
obj = new RestObject("401") { Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." };
if (obj.Error != null)
return obj;
string hash;
var rand = new Random();
var randbytes = new byte[32];
do
{
rand.NextBytes(randbytes);
hash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2"));
} while (Tokens.ContainsKey(hash));
Tokens.Add(hash, user);
obj["token"] = hash;
return obj;
return this.NewTokenInternal(user, pass);
}
private object NewToken(RestVerbs verbs, IParameterCollection parameters)
@ -100,55 +82,84 @@ namespace Rests
var user = verbs["username"];
var pass = verbs["password"];
RestObject obj = null;
if (Verify != null)
obj = Verify(user, pass);
if (obj == null)
obj = new RestObject("401")
{Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair."};
if (obj.Error != null)
return obj;
string hash;
RestObject response = this.NewTokenInternal(user, pass);
response["deprecated"] = "This endpoint is depracted and will be removed in the future.";
return response;
}
private RestObject NewTokenInternal(string username, string password)
{
User userAccount = TShock.Users.GetUserByName(username);
if (userAccount == null || !string.IsNullOrWhiteSpace(userAccount.Address))
return new RestObject("401")
{ Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." };
if (!TShock.Utils.HashPassword(password).Equals(userAccount.Password, StringComparison.InvariantCultureIgnoreCase))
return new RestObject("401")
{ Error = "Invalid username/password combination provided. Please re-submit your query with a correct pair." };
Group userGroup = TShock.Utils.GetGroup(userAccount.Group);
if (!userGroup.HasPermission(Permissions.restapi) && userAccount.Group != "superadmin")
return new RestObject("403")
{ Error = "Although your account was successfully found and identified, your account lacks the permission required to use the API. (restapi)" };
string tokenHash;
var rand = new Random();
var randbytes = new byte[32];
do
{
rand.NextBytes(randbytes);
hash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2"));
} while (Tokens.ContainsKey(hash));
tokenHash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2"));
} while (Tokens.ContainsKey(tokenHash));
Tokens.Add(hash, user);
obj["token"] = hash;
obj["deprecated"] = "This method will be removed from TShock in 3.6.";
return obj;
Tokens.Add(tokenHash, new TokenData { Username = userAccount.Name, UserGroup = userGroup });
RestObject response = new RestObject("200") { Response = "Successful login" };
response["token"] = tokenHash;
return response;
}
protected override object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms)
{
if (cmd.RequiresToken)
{
var strtoken = parms["token"];
if (strtoken == null)
return new Dictionary<string, string>
{{"status", "401"}, {"error", "Not authorized. The specified API endpoint requires a token."}};
object token;
if (!Tokens.TryGetValue(strtoken, out token))
return new Dictionary<string, string>
{
{"status", "403"},
{
"error",
"Not authorized. The specified API endpoint requires a token, but the provided token was not valid."
}
};
}
return base.ExecuteCommand(cmd, verbs, parms);
if (!cmd.RequiresToken)
return base.ExecuteCommand(cmd, verbs, parms);
var token = parms["token"];
if (token == null)
return new Dictionary<string, string>
{{"status", "401"}, {"error", "Not authorized. The specified API endpoint requires a token."}};
SecureRestCommand secureCmd = (SecureRestCommand)cmd;
TokenData tokenData;
if (!Tokens.TryGetValue(token, out tokenData))
return new Dictionary<string, string>
{
{"status", "403"},
{
"error",
"Not authorized. The specified API endpoint requires a token, but the provided token was not valid."
}
};
if (secureCmd.Permissions.Length > 0 && secureCmd.Permissions.All(perm => !tokenData.UserGroup.HasPermission(perm)))
{
return new Dictionary<string, string>
{
{"status", "403"},
{
"error",
string.Format("Not authorized. User \"{0}\" has no access to use the specified API endpoint.", tokenData.Username)
}
};
}
object result = secureCmd.Execute(verbs, parms, tokenData);
if (cmd.DoLog)
TShock.Utils.SendLogs(string.Format(
"\"{0}\" requested REST endpoint: {1}", tokenData.Username, this.BuildRequestUri(cmd, verbs, parms, false)),
Color.PaleVioletRed);
return result;
}
}
}