-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:
parent
4e7b497ae4
commit
0ea83746cf
9 changed files with 415 additions and 237 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue