From d6ba5734569325a55665dbea38d1425306684a0a Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 28 Jan 2021 19:54:04 +0100 Subject: [PATCH] Web server - Plugin end point API WIP --- .../WebServer/Interfaces/IWebServerService.cs | 14 ++++ .../WebServer/PluginEndPointRegistration.cs | 45 ++++++++++++ .../Services/WebServer/PluginsModule.cs | 68 ++++++++++++++++++- .../Services/WebServer/WebServerService.cs | 26 ++++++- 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index b70ed629d..8be29ebfa 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -14,6 +14,20 @@ namespace Artemis.Core.Services /// WebServer? Server { get; } + /// + /// Adds a new endpoint for the given plugin feature + /// + /// 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); + + /// + /// Removes an existing endpoint + /// + /// The end point to remove + void RemovePluginEndPoint(PluginEndPoint endPoint); + /// /// Adds a new Web API controller and restarts the web server /// diff --git a/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs b/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs new file mode 100644 index 000000000..695bc8991 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/PluginEndPointRegistration.cs @@ -0,0 +1,45 @@ +using System; +using EmbedIO; + +namespace Artemis.Core.Services +{ + public class PluginEndPoint + { + private readonly PluginsModule _pluginsModule; + + internal PluginEndPoint(PluginFeature pluginFeature, string name, PluginsModule pluginsModule) + { + _pluginsModule = pluginsModule; + PluginFeature = pluginFeature; + Name = name; + + PluginFeature.Disabled += OnDisabled; + } + + /// + /// Gets the plugin the data model is associated with + /// + public PluginFeature PluginFeature { get; } + + /// + /// Gets the name of the end point + /// + public string Name { get; } + + /// + /// 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(); + } + + private void OnDisabled(object? sender, EventArgs e) + { + PluginFeature.Disabled -= OnDisabled; + _pluginsModule.RemovePluginEndPoint(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index b3e635153..6208783df 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -1,13 +1,54 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; using EmbedIO; +using Newtonsoft.Json; namespace Artemis.Core.Services { internal class PluginsModule : WebModuleBase { + private readonly Dictionary> _pluginEndPoints; + /// public PluginsModule(string baseRoute) : base(baseRoute) { + _pluginEndPoints = new Dictionary>(); + OnUnhandledException += HandleUnhandledExceptionJson; + } + + public void AddPluginEndPoint(PluginEndPoint registration) + { + string id = registration.PluginFeature.Plugin.Guid.ToString(); + if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) + { + registrations = new Dictionary(); + _pluginEndPoints.Add(id, registrations); + } + + if (registrations.ContainsKey(registration.Name)) + throw new ArtemisPluginException(registration.PluginFeature.Plugin, $"Plugin already registered an endpoint at {registration.Name}."); + registrations.Add(registration.Name, registration); + } + + public void RemovePluginEndPoint(PluginEndPoint registration) + { + string id = registration.PluginFeature.Plugin.Guid.ToString(); + if (!_pluginEndPoints.TryGetValue(id, out Dictionary? registrations)) + return; + if (!registrations.ContainsKey(registration.Name)) + return; + registrations.Remove(registration.Name); + } + + private async Task HandleUnhandledExceptionJson(IHttpContext context, Exception exception) + { + await context.SendStringAsync( + JsonConvert.SerializeObject(new ArtemisPluginException("The plugin failed to process the request", exception), Formatting.Indented), + MimeType.Json, + Encoding.UTF8 + ); } #region Overrides of WebModuleBase @@ -15,6 +56,31 @@ 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); + + // No need to return ourselves, assume the request is fully handled by the end point + context.SetHandled(); } /// diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 64af5c2ef..07fb5ec73 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; +using System.Threading.Tasks; using EmbedIO; using EmbedIO.Actions; using EmbedIO.WebApi; +using Newtonsoft.Json; using Ninject; using Serilog; @@ -46,7 +49,7 @@ namespace Artemis.Core.Services .WithLocalSessionManager() .WithModule(apiModule) .WithModule(_pluginModule) - .WithModule(new ActionModule("/", HttpVerbs.Any, ctx => ctx.SendDataAsync(new {Message = "Error"}))); + .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)); // Add controllers to the API module foreach (WebApiControllerRegistration registration in _controllers) @@ -62,6 +65,11 @@ namespace Artemis.Core.Services return server; } + private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) + { + await context.SendStringAsync(JsonConvert.SerializeObject(httpException, Formatting.Indented), MimeType.Json, Encoding.UTF8); + } + #endregion #region Event handlers @@ -85,6 +93,22 @@ namespace Artemis.Core.Services #endregion + #region Plugin endpoint management + + public PluginEndPoint AddPluginEndPoint(PluginFeature feature, string endPointName) + { + PluginEndPoint endPoint = new(feature, endPointName, _pluginModule); + _pluginModule.AddPluginEndPoint(endPoint); + return endPoint; + } + + public void RemovePluginEndPoint(PluginEndPoint endPoint) + { + _pluginModule.RemovePluginEndPoint(endPoint); + } + + #endregion + #region Controller management public void AddController() where T : WebApiController