From fe847ad8f46730d71d7a52a37eb6460434680251 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Fri, 29 Jan 2021 00:54:27 +0100 Subject: [PATCH] Web server - Added serveral plugin end point types --- .../Artemis.Core.csproj.DotSettings | 1 + .../WebServer/EndPoints/JsonPluginEndPoint.cs | 76 +++++++++++++++ .../PluginEndPoint.cs} | 11 +-- .../WebServer/EndPoints/RawPluginEndPoint.cs | 36 ++++++++ .../EndPoints/StringPluginEndPoint.cs | 55 +++++++++++ .../WebServer/Interfaces/IWebServerService.cs | 51 +++++++++- .../Services/WebServer/PluginsModule.cs | 11 +-- .../Services/WebServer/WebServerService.cs | 92 ++++++++++++++----- 8 files changed, 292 insertions(+), 41 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs rename src/Artemis.Core/Services/WebServer/{PluginEndPointRegistration.cs => EndPoints/PluginEndPoint.cs} (79%) create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs create mode 100644 src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 8aa37a05c..5ac92d7ad 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -65,6 +65,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs new file mode 100644 index 000000000..5a8ebcd98 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using EmbedIO; +using Newtonsoft.Json; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint receiving an object of type and returning any + /// or . + /// Note: Both will be deserialized and serialized respectively using JSON. + /// + public class JsonPluginEndPoint : PluginEndPoint + { + private readonly Action? _requestHandler; + private readonly Func? _responseRequestHandler; + + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + _requestHandler = requestHandler; + ThrowOnFail = true; + } + + internal JsonPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func responseRequestHandler) : base(pluginFeature, name, pluginsModule) + { + _responseRequestHandler = responseRequestHandler; + ThrowOnFail = true; + } + + /// + /// Whether or not the end point should throw an exception if deserializing the received JSON fails. + /// If set to malformed JSON is silently ignored; if set to malformed + /// JSON throws a . + /// + public bool ThrowOnFail { get; set; } + + #region Overrides of PluginEndPoint + + /// + 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(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 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs similarity index 79% rename from src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs rename to src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index 695bc8991..e2250aa8b 100644 --- a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -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; } /// - /// Gets the name of the end point + /// Gets the name of the end point /// public string Name { get; } /// - /// Gets the full URL of the end point + /// Gets the full URL of the end point /// 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) { diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs new file mode 100644 index 000000000..49dc63614 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/RawPluginEndPoint.cs @@ -0,0 +1,36 @@ +using System; +using EmbedIO; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint that handles a raw . + /// + /// Note: This requires that you reference the EmbedIO + /// Nuget package. + /// + /// + public class RawPluginEndPoint : PluginEndPoint + { + /// + internal RawPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + RequestHandler = requestHandler; + } + + /// + /// Gets or sets the handler used to handle incoming requests to this endpoint + /// + public Action RequestHandler { get; } + + #region Overrides of PluginEndPoint + + /// + internal override void ProcessRequest(IHttpContext context) + { + RequestHandler(context); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs new file mode 100644 index 000000000..20cb823c3 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using EmbedIO; + +namespace Artemis.Core.Services +{ + /// + /// Represents a plugin web endpoint receiving an a and returning a or + /// . + /// + public class StringPluginEndPoint : PluginEndPoint + { + private readonly Action? _requestHandler; + private readonly Func? _responseRequestHandler; + + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Action requestHandler) : base(pluginFeature, name, pluginsModule) + { + _requestHandler = requestHandler; + } + + internal StringPluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule, Func requestHandler) : base(pluginFeature, name, pluginsModule) + { + _responseRequestHandler = requestHandler; + } + + #region Overrides of PluginEndPoint + + /// + 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 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index 8be29ebfa..223f0b505 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -15,12 +15,59 @@ namespace Artemis.Core.Services WebServer? Server { get; } /// - /// Adds a new endpoint for the given plugin feature + /// Adds a new endpoint for the given plugin feature receiving an object of type + /// Note: Object will be deserialized using JSON. + /// + /// The type of object to be received + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an object of type and + /// returning any . + /// Note: Both will be deserialized and serialized respectively using JSON. + /// + /// The type of object to be received + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an a . /// /// The plugin feature the end point is associated with /// The name of the end point, must be unique + /// /// The resulting end point - PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName); + StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature receiving an a and returning a + /// or . + /// + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + StringPluginEndPoint AddResponsiveStringEndPoint(PluginFeature feature, string endPointName, Func requestHandler); + + /// + /// Adds a new endpoint for the given plugin feature that handles a raw . + /// + /// Note: This requires that you reference the EmbedIO + /// Nuget package. + /// + /// + /// The plugin feature the end point is associated with + /// The name of the end point, must be unique + /// + /// The resulting end point + RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointName, Action requestHandler); /// /// Removes an existing endpoint diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index 6208783df..c19af8add 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -56,26 +56,23 @@ namespace Artemis.Core.Services /// 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? 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); diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 07fb5ec73..13d7ed7fb 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -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 _controllers; private readonly IKernel _kernel; private readonly ILogger _logger; private readonly PluginsModule _pluginModule; private readonly PluginSetting _webServerPortSetting; - private readonly List _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 + + /// + 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 - - /// - public void Dispose() - { - Server?.Dispose(); - _webServerPortSetting.SettingChanged -= WebServerPortSettingOnSettingChanged; - } - - #endregion - #region Plugin endpoint management - public PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName) + public JsonPluginEndPoint AddJsonEndPoint(PluginFeature feature, string endPointName, Action 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 endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func 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 endPoint = new(feature, endPointName, _pluginModule, requestHandler); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public StringPluginEndPoint AddStringEndPoint(PluginFeature feature, string endPointName, Action 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 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 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; }