/*
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.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;
using System.Collections;
namespace Rests
{
///
/// Wraps an , providing URI-unescaping for its value
///
public class EscapedParameter
{
private IParameter _parameter;
///
/// Name of the parameter
///
public string Name => _parameter.Name;
///
/// URI-unescaped value of the parameter
///
public string Value => Uri.UnescapeDataString(_parameter.Value);
///
/// Constructs a new EscapedParameter wrapping the given
///
///
public EscapedParameter(IParameter parameter)
{
_parameter = parameter;
}
}
///
/// Wraps an , providing URI-unescaping for the parameters in the collection
///
public class EscapedParameterCollection : IEnumerable
{
private readonly IParameterCollection _collection;
///
/// Retrieve a parameter by name, returning the URI-unescaped value
///
///
///
public string this[string key]
{
get
{
string value = _collection[key];
return value == null ? value : Uri.UnescapeDataString(value);
}
}
///
/// Constructs a new EscapedParameterCollection wrapping the given
///
///
public EscapedParameterCollection(IParameterCollection collection)
{
_collection = collection;
}
///
/// Returns an enumerator that can be used to iterate over this collection
///
///
public IEnumerator GetEnumerator()
{
foreach (IParameter param in _collection)
{
yield return new EscapedParameter(param);
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
///
/// Rest command delegate
///
/// object containing Verbs, Parameters, Request, and TokenData
/// Response object or null to not handle request
public delegate object RestCommandD(RestRequestArgs args);
///
/// Describes the data contained in a REST request
///
public class RestRequestArgs
{
///
/// Verbs sent in the request
///
public RestVerbs Verbs { get; private set; }
///
/// Parameters sent in the request
///
public EscapedParameterCollection Parameters { get; private set; }
///
/// The HTTP request
///
public IRequest Request { get; private set; }
///
/// Token data used by the request
///
public SecureRest.TokenData TokenData { get; private set; }
///
/// used by the request
///
public IHttpContext Context { get; private set; }
///
/// Creates a new instance of with the given verbs, parameters, request, and context.
/// No token data is used
///
/// Verbs used in the request
/// Parameters used in the request
/// The HTTP request
/// The HTTP context
public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, IHttpContext context)
: this(verbs, param, request, SecureRest.TokenData.None, context)
{
}
///
/// Creates a new instance of with the given verbs, parameters, request, token data, and context.
///
/// Verbs used in the request
/// Parameters used in the request
/// The HTTP request
/// Token data used in the request
/// The HTTP context
public RestRequestArgs(RestVerbs verbs, IParameterCollection param, IRequest request, SecureRest.TokenData tokenData, IHttpContext context)
: this(verbs, new EscapedParameterCollection(param), request, tokenData, context)
{
}
///
/// Creates a new instance of with the given verbs, escaped parameters, request, token data, and context.
///
///
///
///
///
///
public RestRequestArgs(RestVerbs verbs, EscapedParameterCollection param, IRequest request, SecureRest.TokenData tokenData, IHttpContext context)
{
Verbs = verbs;
Parameters = param;
Request = request;
TokenData = tokenData;
Context = context;
}
}
///
/// A RESTful API service
///
public class Rest : IDisposable
{
private readonly List commands = new List();
///
/// 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.
///
private Dictionary> redirects = new Dictionary>();
private HttpListener listener;
private StringHeader serverHeader;
private Timer tokenBucketTimer;
///
/// Contains tokens used to manage REST authentication
///
public Dictionary tokenBucket = new Dictionary();
///
/// the REST service is listening on
///
public IPAddress Ip { get; set; }
///
/// Port the REST service is listening on
///
public int Port { get; set; }
///
/// Creates a new instance of listening on the given IP and port
///
/// to listen on
/// Port to listen on
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));
}
///
/// Starts the RESTful API service
///
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.Settings.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);
}
}
///
/// Starts the RESTful API service using the given and port
///
/// to listen on
/// Port to listen on
public void Start(IPAddress ip, int port)
{
Ip = ip;
Port = port;
Start();
}
///
/// Stops the RESTful API service
///
public virtual void Stop()
{
listener.Stop();
}
///
/// Registers a command using the given route
///
/// URL route
/// Command callback
public void Register(string path, RestCommandD callback)
{
AddCommand(new RestCommand(path, callback));
}
///
/// Registers a
///
/// to register
public void Register(RestCommand com)
{
AddCommand(com);
}
///
/// Registers a redirection from a given REST route to a target REST route, with an optional upgrade URI
///
/// The base URI that will be requested
/// The target URI to redirect to from the base URI
/// The upgrade route that will be added as an object to the response of the target route
/// Whether the route uses parameterized querying or not.
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);
}
}
///
/// Adds a to the service's command list
///
/// to add
protected void AddCommand(RestCommand com)
{
commands.Add(com);
}
private void DegradeBucket()
{
var _bucket = new List(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);
}
}
}
///
/// Called when the receives a request
///
/// Sender of the request
/// RequestEventArgs received
protected virtual void OnRequest(object sender, RequestEventArgs e)
{
var obj = ProcessRequest(sender, e);
if (obj == null)
throw new NullReferenceException("obj");
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;
if (obj is RestObject rObj && Enum.TryParse(rObj.Status, out HttpStatusCode status))
{
e.Response.Status = status;
}
}
///
/// Attempts to process a request received by the
///
/// Sender of the request
/// RequestEventArgs received
/// A describing the state of the request
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."}
};
}
///
/// Executes a using the provided verbs, parameters, request, and context objects
///
/// The REST command to execute
/// The REST verbs used in the command
/// The REST parameters used in the command
/// The HTTP request object associated with the command
/// The HTTP context associated with the command
///
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.Settings.LogRest)
{
TShock.Log.ConsoleInfo("Anonymous requested REST endpoint: " + BuildRequestUri(cmd, verbs, parms, false));
}
return result;
}
///
/// Builds a request URI from the parameters, verbs, and URI template of a
///
/// The REST command to take the URI template from
/// Verbs used in building the URI string
/// Parameters used in building the URI string
/// Whether or not to include a token in the URI
///
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
///
/// Disposes the RESTful API service
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Disposes the RESTful API service
///
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (listener != null)
{
listener.Stop();
listener = null;
}
}
}
///
/// Destructor
///
~Rest()
{
Dispose(false);
}
#endregion
}
}