1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Web server - Added serveral plugin end point types

This commit is contained in:
SpoinkyNL 2021-01-29 00:54:27 +01:00
parent d6ba573456
commit fe847ad8f4
8 changed files with 292 additions and 41 deletions

View File

@ -65,6 +65,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cstorage/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cstorage_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwebserver/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwebserver_005Cendpoints/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwebserver_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores_005Cregistrations/@EntryIndexedValue">True</s:Boolean>

View File

@ -0,0 +1,76 @@
using System;
using System.IO;
using EmbedIO;
using Newtonsoft.Json;
namespace Artemis.Core.Services
{
/// <summary>
/// Represents a plugin web endpoint receiving an object of type <typeparamref name="T" /> and returning any
/// <see cref="object" /> or <see langword="null" />.
/// <para>Note: Both will be deserialized and serialized respectively using JSON.</para>
/// </summary>
public class JsonPluginEndPoint<T> : PluginEndPoint
{
private readonly Action<T>? _requestHandler;
private readonly Func<T, object?>? _responseRequestHandler;
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action<T> requestHandler) : base(pluginFeature, name, pluginsModule)
{
_requestHandler = requestHandler;
ThrowOnFail = true;
}
internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<T, object?> responseRequestHandler) : base(pluginFeature, name, pluginsModule)
{
_responseRequestHandler = responseRequestHandler;
ThrowOnFail = true;
}
/// <summary>
/// Whether or not the end point should throw an exception if deserializing the received JSON fails.
/// If set to <see langword="false" /> malformed JSON is silently ignored; if set to <see langword="true" /> malformed
/// JSON throws a <see cref="JsonException" />.
/// </summary>
public bool ThrowOnFail { get; set; }
#region Overrides of PluginEndPoint
/// <inheritdoc />
internal override void ProcessRequest(IHttpContext context)
{
if (context.Request.HttpVerb != HttpVerbs.Post)
throw HttpException.MethodNotAllowed("This end point only accepts POST calls");
context.Response.ContentType = MimeType.Json;
using TextReader reader = context.OpenRequestText();
object? response = null;
try
{
T deserialized = JsonConvert.DeserializeObject<T>(reader.ReadToEnd());
if (_requestHandler != null)
{
_requestHandler(deserialized);
return;
}
if (_responseRequestHandler != null)
response = _responseRequestHandler(deserialized);
else
throw new ArtemisCoreException("JSON plugin end point has no request handler");
}
catch (JsonException)
{
if (ThrowOnFail)
throw;
}
using TextWriter writer = context.OpenResponseText();
writer.Write(JsonConvert.SerializeObject(response));
}
#endregion
}
}

View File

@ -3,7 +3,7 @@ using EmbedIO;
namespace Artemis.Core.Services
{
public class PluginEndPoint
public abstract class PluginEndPoint
{
private readonly PluginsModule _pluginsModule;
@ -22,19 +22,16 @@ namespace Artemis.Core.Services
public PluginFeature PluginFeature { get; }
/// <summary>
/// Gets the name of the end point
/// Gets the name of the end point
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the full URL of the end point
/// Gets the full URL of the end point
/// </summary>
public string Url => $"{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}";
internal void ProcessRequest(IHttpContext context)
{
throw new NotImplementedException();
}
internal abstract void ProcessRequest(IHttpContext context);
private void OnDisabled(object? sender, EventArgs e)
{

View File

@ -0,0 +1,36 @@
using System;
using EmbedIO;
namespace Artemis.Core.Services
{
/// <summary>
/// Represents a plugin web endpoint that handles a raw <see cref="IHttpContext" />.
/// <para>
/// Note: This requires that you reference the EmbedIO
/// <see href="https://www.nuget.org/packages/embedio">Nuget package</see>.
/// </para>
/// </summary>
public class RawPluginEndPoint : PluginEndPoint
{
/// <inheritdoc />
internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action<IHttpContext> requestHandler) : base(pluginFeature, name, pluginsModule)
{
RequestHandler = requestHandler;
}
/// <summary>
/// Gets or sets the handler used to handle incoming requests to this endpoint
/// </summary>
public Action<IHttpContext> RequestHandler { get; }
#region Overrides of PluginEndPoint
/// <inheritdoc />
internal override void ProcessRequest(IHttpContext context)
{
RequestHandler(context);
}
#endregion
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.IO;
using EmbedIO;
namespace Artemis.Core.Services
{
/// <summary>
/// Represents a plugin web endpoint receiving an a <see cref="string" /> and returning a <see cref="string" /> or
/// <see langword="null" />.
/// </summary>
public class StringPluginEndPoint : PluginEndPoint
{
private readonly Action<string>? _requestHandler;
private readonly Func<string, string?>? _responseRequestHandler;
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action<string> requestHandler) : base(pluginFeature, name, pluginsModule)
{
_requestHandler = requestHandler;
}
internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func<string, string?> requestHandler) : base(pluginFeature, name, pluginsModule)
{
_responseRequestHandler = requestHandler;
}
#region Overrides of PluginEndPoint
/// <inheritdoc />
internal override void ProcessRequest(IHttpContext context)
{
if (context.Request.HttpVerb != HttpVerbs.Post)
throw HttpException.MethodNotAllowed("This end point only accepts POST calls");
context.Response.ContentType = MimeType.PlainText;
using TextReader reader = context.OpenRequestText();
string? response;
if (_requestHandler != null)
{
_requestHandler(reader.ReadToEnd());
return;
}
else if (_responseRequestHandler != null)
response = _responseRequestHandler(reader.ReadToEnd());
else
throw new ArtemisCoreException("String plugin end point has no request handler");
using TextWriter writer = context.OpenResponseText();
writer.Write(response);
}
#endregion
}
}

View File

@ -15,12 +15,59 @@ namespace Artemis.Core.Services
WebServer? Server { get; }
/// <summary>
/// Adds a new endpoint for the given plugin feature
/// Adds a new endpoint for the given plugin feature receiving an object of type <typeparamref name="T" />
/// <para>Note: Object will be deserialized using JSON.</para>
/// </summary>
/// <typeparam name="T">The type of object to be received</typeparam>
/// <param name="feature">The plugin feature the end point is associated with</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <param name="requestHandler"></param>
/// <returns>The resulting end point</returns>
JsonPluginEndPoint<T> AddJsonEndPoint<T>(PluginFeature feature, string endPointName, Action<T> requestHandler);
/// <summary>
/// Adds a new endpoint for the given plugin feature receiving an object of type <typeparamref name="T" /> and
/// returning any <see cref="object" />.
/// <para>Note: Both will be deserialized and serialized respectively using JSON.</para>
/// </summary>
/// <typeparam name="T">The type of object to be received</typeparam>
/// <param name="feature">The plugin feature the end point is associated with</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <param name="requestHandler"></param>
/// <returns>The resulting end point</returns>
JsonPluginEndPoint<T> AddResponsiveJsonEndPoint<T>(PluginFeature feature, string endPointName, Func<T, object?> requestHandler);
/// <summary>
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />.
/// </summary>
/// <param name="feature">The plugin feature the end point is associated with</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <param name="requestHandler"></param>
/// <returns>The resulting end point</returns>
PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName);
StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action<string> requestHandler);
/// <summary>
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" /> and returning a
/// <see cref="string" /> or <see langword="null" />.
/// </summary>
/// <param name="feature">The plugin feature the end point is associated with</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <param name="requestHandler"></param>
/// <returns>The resulting end point</returns>
StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func<string, string?> requestHandler);
/// <summary>
/// Adds a new endpoint for the given plugin feature that handles a raw <see cref="IHttpContext" />.
/// <para>
/// Note: This requires that you reference the EmbedIO
/// <see href="https://www.nuget.org/packages/embedio">Nuget package</see>.
/// </para>
/// </summary>
/// <param name="feature">The plugin feature the end point is associated with</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <param name="requestHandler"></param>
/// <returns>The resulting end point</returns>
RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action<IHttpContext> requestHandler);
/// <summary>
/// Removes an existing endpoint

View File

@ -56,26 +56,23 @@ namespace Artemis.Core.Services
/// <inheritdoc />
protected override async Task OnRequestAsync(IHttpContext context)
{
// Always stick to JSON
context.Response.ContentType = MimeType.Json;
if (context.Route.SubPath == null)
throw HttpException.NotFound();
// Split the sub path
string[] pathParts = context.Route.SubPath.Substring(1).Split('/');
// Expect a plugin ID and an endpoint
if (pathParts == null || pathParts.Length != 2)
throw HttpException.BadRequest("Path must contain a plugin ID and endpoint and nothing else.");
// Find a matching plugin
if (!_pluginEndPoints.TryGetValue(pathParts[0], out Dictionary<string, PluginEndPoint>? endPoints))
throw HttpException.NotFound($"Found no plugin with ID {pathParts[0]}.");
// Find a matching endpoint
if (!endPoints.TryGetValue(pathParts[1], out PluginEndPoint? endPoint))
throw HttpException.NotFound($"Found no endpoint called {pathParts[1]} for plugin with ID {pathParts[0]}.");
// It is up to the registration how the request is eventually handled, it might even set a response here
endPoint.ProcessRequest(context);

View File

@ -4,7 +4,6 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Actions;
using EmbedIO.WebApi;
using Newtonsoft.Json;
using Ninject;
@ -14,11 +13,11 @@ namespace Artemis.Core.Services
{
internal class WebServerService : IWebServerService, IDisposable
{
private readonly List<WebApiControllerRegistration> _controllers;
private readonly IKernel _kernel;
private readonly ILogger _logger;
private readonly PluginsModule _pluginModule;
private readonly PluginSetting<int> _webServerPortSetting;
private readonly List<WebApiControllerRegistration> _controllers;
public WebServerService(IKernel kernel, ILogger logger, ISettingsService settingsService)
{
@ -34,6 +33,27 @@ namespace Artemis.Core.Services
Server.Start();
}
#region Event handlers
private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e)
{
Server = CreateWebServer();
Server.Start();
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
Server?.Dispose();
_webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged;
}
#endregion
public WebServer? Server { get; private set; }
#region Web server managament
@ -72,32 +92,54 @@ namespace Artemis.Core.Services
#endregion
#region Event handlers
private void WebServerPortSettingOnSettingChanged(object? sender, EventArgs e)
{
Server = CreateWebServer();
Server.Start();
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
Server?.Dispose();
_webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged;
}
#endregion
#region Plugin endpoint management
public PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName)
public JsonPluginEndPoint<T> AddJsonEndPoint<T>(PluginFeature feature, string endPointName, Action<T> requestHandler)
{
PluginEndPoint endPoint = new(feature, endPointName, _pluginModule);
if (feature == null) throw new ArgumentNullException(nameof(feature));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, _pluginModule, requestHandler);
_pluginModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public JsonPluginEndPoint<T> AddResponsiveJsonEndPoint<T>(PluginFeature feature, string endPointName, Func<T, object?> requestHandler)
{
if (feature == null) throw new ArgumentNullException(nameof(feature));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
JsonPluginEndPoint<T> endPoint = new(feature, endPointName, _pluginModule, requestHandler);
_pluginModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action<string> requestHandler)
{
if (feature == null) throw new ArgumentNullException(nameof(feature));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler);
_pluginModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func<string, string?> requestHandler)
{
if (feature == null) throw new ArgumentNullException(nameof(feature));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
StringPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler);
_pluginModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action<IHttpContext> requestHandler)
{
if (feature == null) throw new ArgumentNullException(nameof(feature));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
if (requestHandler == null) throw new ArgumentNullException(nameof(requestHandler));
RawPluginEndPoint endPoint = new(feature, endPointName, _pluginModule, requestHandler);
_pluginModule.AddPluginEndPoint(endPoint);
return endPoint;
}