/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors 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 . */ using System; using System.Collections.Generic; using System.Linq; using System.Net; using HttpServer; using TShockAPI; using TShockAPI.DB; using Microsoft.Xna.Framework; using Terraria; using System.Security.Cryptography; namespace Rests { public class SecureRest : Rest { public struct TokenData { public static readonly TokenData None = default(TokenData); public string Username { get; set; } public string UserGroupName { get; set; } } public Dictionary Tokens { get; protected set; } public Dictionary AppTokens { get; protected set; } private RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); public SecureRest(IPAddress ip, int port) : base(ip, port) { Tokens = new Dictionary(); AppTokens = new Dictionary(); Register(new RestCommand("/v2/token/create", NewTokenV2) { DoLog = false }); Register(new SecureRestCommand("/token/destroy/{token}", DestroyToken)); Register(new SecureRestCommand("/v3/token/destroy/all", DestroyAllTokens, RestPermissions.restmanage)); foreach (KeyValuePair t in TShock.RESTStartupTokens) { AppTokens.Add(t.Key, t.Value); } foreach (KeyValuePair t in TShock.Config.Settings.ApplicationRestTokens) { AppTokens.Add(t.Key, t.Value); } } private void AddTokenToBucket(string ip) { if (tokenBucket.ContainsKey(ip)) { tokenBucket[ip] += 1; } else { tokenBucket.Add(ip, 1); } } private object DestroyToken(RestRequestArgs args) { var token = args.Verbs["token"]; try { Tokens.Remove(token); } catch (Exception) { return new RestObject("400") { Error = "The specified token queued for destruction failed to be deleted." }; } return new RestObject() { Response = "Requested token was successfully destroyed." }; } private object DestroyAllTokens(RestRequestArgs args) { Tokens.Clear(); return new RestObject() { Response = "All tokens were successfully destroyed." }; } private object NewTokenV2(RestRequestArgs args) { var user = Uri.UnescapeDataString(args.Parameters["username"]); var pass = Uri.UnescapeDataString(args.Parameters["password"]); var context = args.Context; return this.NewTokenInternal(user, pass, context); } private RestObject NewTokenInternal(string username, string password, IHttpContext context) { int tokens = 0; if (tokenBucket.TryGetValue(context.RemoteEndPoint.Address.ToString(), out tokens)) { if (tokens >= TShock.Config.Settings.RESTMaximumRequestsPerInterval) { TShock.Log.ConsoleError("A REST login from {0} was blocked as it currently has {1} rate-limit tokens and is at the RESTMaximumRequestsPerInterval threshold.", context.RemoteEndPoint.Address.ToString(), tokens); tokenBucket[context.RemoteEndPoint.Address.ToString()] += 1; // Tokens over limit, increment by one and reject request return new RestObject("403") { Error = "Username or password may be incorrect or this account may not have sufficient privileges." }; } tokenBucket[context.RemoteEndPoint.Address.ToString()] += 1; // Tokens under limit, increment by one and process request } else { tokenBucket.Add(context.RemoteEndPoint.Address.ToString(), 1); // First time request, set to one and process request } UserAccount userAccount = TShock.UserAccounts.GetUserAccountByName(username); if (userAccount == null) { AddTokenToBucket(context.RemoteEndPoint.Address.ToString()); return new RestObject("403") { Error = "Username or password may be incorrect or this account may not have sufficient privileges." }; } if (!userAccount.VerifyPassword(password)) { AddTokenToBucket(context.RemoteEndPoint.Address.ToString()); return new RestObject("403") { Error = "Username or password may be incorrect or this account may not have sufficient privileges." }; } Group userGroup = TShock.Groups.GetGroupByName(userAccount.Group); if (!userGroup.HasPermission(RestPermissions.restapi) && userAccount.Group != "superadmin") { AddTokenToBucket(context.RemoteEndPoint.Address.ToString()); return new RestObject("403") { Error = "Username or password may be incorrect or this account may not have sufficient privileges." }; } string tokenHash; var randbytes = new byte[32]; do { _rng.GetBytes(randbytes); tokenHash = randbytes.Aggregate("", (s, b) => s + b.ToString("X2")); } while (Tokens.ContainsKey(tokenHash)); Tokens.Add(tokenHash, new TokenData { Username = userAccount.Name, UserGroupName = userGroup.Name }); AddTokenToBucket(context.RemoteEndPoint.Address.ToString()); RestObject response = new RestObject() { Response = "Successful login" }; response["token"] = tokenHash; return response; } protected override object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms, IRequest request, IHttpContext context) { if (!cmd.RequiresToken) return base.ExecuteCommand(cmd, verbs, parms, request, context); var token = parms["token"]; if (token == null) return new RestObject("401") { Error = "Not authorized. The specified API endpoint requires a token." }; SecureRestCommand secureCmd = (SecureRestCommand)cmd; TokenData tokenData; if (!Tokens.TryGetValue(token, out tokenData) && !AppTokens.TryGetValue(token, out tokenData)) return new RestObject("403") { Error = "Not authorized. The specified API endpoint requires a token, but the provided token was not valid." }; Group userGroup = TShock.Groups.GetGroupByName(tokenData.UserGroupName); if (userGroup == null) { 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) }; } //Main.rand being null can cause issues in command execution. //This should solve that if (Main.rand == null) { Main.rand = new Terraria.Utilities.UnifiedRandom(); } object result = secureCmd.Execute(verbs, parms, tokenData, request, context); if (cmd.DoLog && TShock.Config.Settings.LogRest) TShock.Utils.SendLogs(string.Format( "\"{0}\" requested REST endpoint: {1}", tokenData.Username, this.BuildRequestUri(cmd, verbs, parms, false)), Color.PaleVioletRed); return result; } } }