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