TShock/TShockAPI/Rest/Rest.cs
Lucas Nicodemus 3a59280834 Pre-initialize TShock hooks for Register method
Hooks have this fancy .Register method when they're of type
HandlerList<Args> that high made but we never bother to initialize
any of them, so .Register doesn't work because it's null.

This solves that problem by just initializing all handlers. Thus,
.Register works, and thus, you can register hooks with priorities.
2017-12-20 12:35:42 -07:00

460 lines
No EOL
14 KiB
C#

/*
TShock, a server mod for Terraria
Copyright (C) 2011-2017 Nyx Studios (fka. 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.Collections.Generic;
using System.ComponentModel;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Reflection;
using HttpServer;
using HttpServer.Headers;
using Newtonsoft.Json;
using TShockAPI;
using HttpListener = HttpServer.HttpListener;
namespace Rests
{
/// <summary>
/// Rest command delegate
/// </summary>
/// <param name="args"><see cref="RestRequestArgs"/> object containing Verbs, Parameters, Request, and TokenData</param>
/// <returns>Response object or null to not handle request</returns>
public delegate object RestCommandD(RestRequestArgs args);
/// <summary>
/// Describes the data contained in a REST request
/// </summary>
public class RestRequestArgs
{
/// <summary>
/// Verbs sent in the request
/// </summary>
public RestVerbs Verbs { get; private set; }
/// <summary>
/// Parameters sent in the request
/// </summary>
public IParameterCollection Parameters { get; private set; }
/// <summary>
/// The HTTP request
/// </summary>
public IRequest Request { get; private set; }
/// <summary>
/// Token data used by the request
/// </summary>
public SecureRest.TokenData TokenData { get; private set; }
/// <summary>
/// <see cref="IHttpContext"/> used by the request
/// </summary>
public IHttpContext Context { get; private set; }
/// <summary>
/// Creates a new instance of <see cref="RestRequestArgs"/> with the given verbs, parameters, request, and context.
/// No token data is used
/// </summary>
/// <param name="verbs">Verbs used in the request</param>
/// <param name="param">Parameters used in the request</param>
/// <param name="request">The HTTP request</param>
/// <param name="context">The HTTP context</param>
public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, IHttpContext context)
{
Verbs = verbs;
Parameters = param;
Request = request;
TokenData = SecureRest.TokenData.None;
Context = context;
}
/// <summary>
/// Creates a new instance of <see cref="RestRequestArgs"/> with the given verbs, parameters, request, token data, and context.
/// </summary>
/// <param name="verbs">Verbs used in the request</param>
/// <param name="param">Parameters used in the request</param>
/// <param name="request">The HTTP request</param>
/// <param name="tokenData">Token data used in the request</param>
/// <param name="context">The HTTP context</param>
public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, SecureRest.TokenData tokenData, IHttpContext context)
{
Verbs = verbs;
Parameters = param;
Request = request;
TokenData = tokenData;
Context = context;
}
}
/// <summary>
/// A RESTful API service
/// </summary>
public class Rest : IDisposable
{
private readonly List<RestCommand> commands = new List<RestCommand>();
/// <summary>
/// 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.
/// </summary>
private Dictionary<string, Tuple<string, string>> redirects = new Dictionary<string, Tuple<string, string>>();
private HttpListener listener;
private StringHeader serverHeader;
private Timer tokenBucketTimer;
/// <summary>
/// Contains tokens used to manage REST authentication
/// </summary>
public Dictionary<string, int> tokenBucket = new Dictionary<string, int>();
/// <summary>
/// <see cref="IPAddress"/> the REST service is listening on
/// </summary>
public IPAddress Ip { get; set; }
/// <summary>
/// Port the REST service is listening on
/// </summary>
public int Port { get; set; }
/// <summary>
/// Creates a new instance of <see cref="Rest"/> listening on the given IP and port
/// </summary>
/// <param name="ip"><see cref="IPAddress"/> to listen on</param>
/// <param name="port">Port to listen on</param>
public Rest(IPAddress ip, int port)
{
Ip = ip;
Port = port;
AssemblyName assembly = this.GetType().Assembly.GetName();
serverHeader = new StringHeader("Server", String.Format("{0}/{1}", assembly.Name, assembly.Version));
}
/// <summary>
/// Starts the RESTful API service
/// </summary>
public virtual void Start()
{
try
{
listener = HttpListener.Create(Ip, Port);
listener.RequestReceived += OnRequest;
listener.Start(int.MaxValue);
tokenBucketTimer = new Timer((e) =>
{
DegradeBucket();
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(Math.Max(TShock.Config.RESTRequestBucketDecreaseIntervalMinutes, 1)));
}
catch (Exception ex)
{
TShock.Log.Error("Fatal Startup Exception");
TShock.Log.Error(ex.ToString());
TShock.Log.ConsoleError("Invalid REST configuration: \nYou may already have a REST service bound to port {0}. \nPlease adjust your configuration and restart the server. \nPress any key to exit.", Port);
Console.ReadLine();
Environment.Exit(1);
}
}
/// <summary>
/// Starts the RESTful API service using the given <see cref="IPAddress"/> and port
/// </summary>
/// <param name="ip"><see cref="IPAddress"/> to listen on</param>
/// <param name="port">Port to listen on</param>
public void Start(IPAddress ip, int port)
{
Ip = ip;
Port = port;
Start();
}
/// <summary>
/// Stops the RESTful API service
/// </summary>
public virtual void Stop()
{
listener.Stop();
}
/// <summary>
/// Registers a command using the given route
/// </summary>
/// <param name="path">URL route</param>
/// <param name="callback">Command callback</param>
public void Register(string path, RestCommandD callback)
{
AddCommand(new RestCommand(path, callback));
}
/// <summary>
/// Registers a <see cref="RestCommand"/>
/// </summary>
/// <param name="com"><see cref="RestCommand"/> to register</param>
public void Register(RestCommand com)
{
AddCommand(com);
}
/// <summary>
/// Registers a redirection from a given REST route to a target REST route, with an optional upgrade URI
/// </summary>
/// <param name="baseRoute">The base URI that will be requested</param>
/// <param name="targetRoute">The target URI to redirect to from the base URI</param>
/// <param name="upgradeRoute">The upgrade route that will be added as an object to the <see cref="RestObject"/> response of the target route</param>
/// <param name="parameterized">Whether the route uses parameterized querying or not.</param>
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);
}
}
/// <summary>
/// Adds a <see cref="RestCommand"/> to the service's command list
/// </summary>
/// <param name="com"><see cref="RestCommand"/> to add</param>
protected void AddCommand(RestCommand com)
{
commands.Add(com);
}
private void DegradeBucket()
{
var _bucket = new List<string>(tokenBucket.Keys); // Duplicate the keys so we can modify tokenBucket whilst iterating
foreach(string key in _bucket)
{
int tokens = tokenBucket[key];
if(tokens > 0)
{
tokenBucket[key] -= 1;
}
if(tokens <= 0)
{
tokenBucket.Remove(key);
}
}
}
#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<RestRequestEventArgs> RestRequestEvent = new HandlerList<RestRequestEventArgs>();
private static bool OnRestRequestCall(RequestEventArgs request)
{
if (RestRequestEvent == null)
return false;
var args = new RestRequestEventArgs
{
Request = request,
};
RestRequestEvent.Invoke(null, args);
return args.Handled;
}
#endregion
/// <summary>
/// Called when the <see cref="HttpListener"/> receives a request
/// </summary>
/// <param name="sender">Sender of the request</param>
/// <param name="e">RequestEventArgs received</param>
protected virtual void OnRequest(object sender, RequestEventArgs e)
{
var obj = ProcessRequest(sender, e);
if (obj == null)
throw new NullReferenceException("obj");
if (OnRestRequestCall(e))
return;
var str = JsonConvert.SerializeObject(obj, Formatting.Indented);
var jsonp = e.Request.Parameters["jsonp"];
if (!string.IsNullOrWhiteSpace(jsonp))
{
str = string.Format("{0}({1});", jsonp, str);
}
e.Response.Connection.Type = ConnectionType.Close;
e.Response.ContentType = new ContentTypeHeader("application/json; charset=utf-8");
e.Response.Add(serverHeader);
var bytes = Encoding.UTF8.GetBytes(str);
e.Response.Body.Write(bytes, 0, bytes.Length);
e.Response.Status = HttpStatusCode.OK;
}
/// <summary>
/// Attempts to process a request received by the <see cref="HttpListener"/>
/// </summary>
/// <param name="sender">Sender of the request</param>
/// <param name="e">RequestEventArgs received</param>
/// <returns>A <see cref="RestObject"/> describing the state of the request</returns>
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)
{
var verbs = new RestVerbs();
if (com.HasVerbs)
{
var match = Regex.Match(uri, com.UriVerbMatch);
if (!match.Success)
continue;
if ((match.Groups.Count - 1) != com.UriVerbs.Length)
continue;
for (int i = 0; i < com.UriVerbs.Length; i++)
verbs.Add(com.UriVerbs[i], match.Groups[i + 1].Value);
}
else if (com.UriTemplate.ToLower() != uri.ToLower())
{
continue;
}
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)
{
return new RestObject("500")
{
{"error", "Internal server error."},
{"errormsg", exception.Message},
{"stacktrace", exception.StackTrace},
};
}
return new RestObject("404")
{
{"error", "Specified API endpoint doesn't exist. Refer to the documentation for a list of valid endpoints."}
};
}
/// <summary>
/// Executes a <see cref="RestCommand"/> using the provided verbs, parameters, request, and context objects
/// </summary>
/// <param name="cmd">The REST command to execute</param>
/// <param name="verbs">The REST verbs used in the command</param>
/// <param name="parms">The REST parameters used in the command</param>
/// <param name="request">The HTTP request object associated with the command</param>
/// <param name="context">The HTTP context associated with the command</param>
/// <returns></returns>
protected virtual object ExecuteCommand(RestCommand cmd, RestVerbs verbs, IParameterCollection parms, IRequest request, IHttpContext context)
{
object result = cmd.Execute(verbs, parms, request, context);
if (cmd.DoLog && TShock.Config.LogRest)
{
TShock.Log.ConsoleInfo("Anonymous requested REST endpoint: " + BuildRequestUri(cmd, verbs, parms, false));
}
return result;
}
/// <summary>
/// Builds a request URI from the parameters, verbs, and URI template of a <see cref="RestCommand"/>
/// </summary>
/// <param name="cmd">The REST command to take the URI template from</param>
/// <param name="verbs">Verbs used in building the URI string</param>
/// <param name="parms">Parameters used in building the URI string</param>
/// <param name="includeToken">Whether or not to include a token in the URI</param>
/// <returns></returns>
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
/// <summary>
/// Disposes the RESTful API service
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the RESTful API service
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (listener != null)
{
listener.Stop();
listener = null;
}
}
}
/// <summary>
/// Destructor
/// </summary>
~Rest()
{
Dispose(false);
}
#endregion
}
}