diff --git a/Pilz.Net/Api/ApiClient.cs b/Pilz.Net/Api/ApiClient.cs index 4a9f550..fe3626c 100644 --- a/Pilz.Net/Api/ApiClient.cs +++ b/Pilz.Net/Api/ApiClient.cs @@ -16,42 +16,62 @@ public class ApiClient(string apiUrl) : IApiClient public virtual Task SendRequest(string route) { - return SendRequest(route, null); + return SendRequest(route, HttpMethod.Post); + } + + public virtual Task SendRequest(string route, HttpMethod method) + { + return SendRequest(route, method, null); } public virtual Task SendRequest(string route, ApiMessage? message) { - return SendRequest(route, message, null); + return SendRequest(route, HttpMethod.Post, message); } - public virtual async Task SendRequest(string route, ApiMessage? message, IApiMessageSerializer? serializer) + public virtual Task SendRequest(string route, HttpMethod method, ApiMessage? message) + { + return SendRequest(route, method, message, null); + } + + public virtual async Task SendRequest(string route, HttpMethod method, ApiMessage? message, IApiMessageSerializer? serializer) { serializer ??= Serializer; Log.InfoFormat("Send message to {0}", route); - var res = await Send(route, message, serializer); + var res = await Send(route, method, message, serializer); return new(res.StatusCode); } public virtual Task> SendRequest(string route) where TResponse : ApiMessage { - return SendRequest(route, null); + return SendRequest(route, HttpMethod.Post); + } + + public virtual Task> SendRequest(string route, HttpMethod method) where TResponse : ApiMessage + { + return SendRequest(route, method, null); } public virtual Task> SendRequest(string route, ApiMessage? message) where TResponse : ApiMessage { - return SendRequest(route, message, null); + return SendRequest(route, HttpMethod.Post, message); } - public virtual async Task> SendRequest(string route, ApiMessage? message, IApiMessageSerializer? serializer) where TResponse : ApiMessage + public virtual Task> SendRequest(string route, HttpMethod method, ApiMessage? message) where TResponse : ApiMessage + { + return SendRequest(route, method, message, null); + } + + public virtual async Task> SendRequest(string route, HttpMethod method, ApiMessage? message, IApiMessageSerializer? serializer) where TResponse : ApiMessage { serializer ??= Serializer; Log.InfoFormat("Send request to {0}", route); - var res = await Send(route, message, serializer); + var res = await Send(route, method, message, serializer); TResponse? result = null; if (res.IsSuccessStatusCode) @@ -60,7 +80,7 @@ public class ApiClient(string apiUrl) : IApiClient return new(res.StatusCode, result); } - protected virtual async Task Send(string route, ApiMessage? message, IApiMessageSerializer serializer) + protected virtual async Task Send(string route, HttpMethod method, ApiMessage? message, IApiMessageSerializer serializer) { var url = ApiUrl + route; HttpContent content; @@ -80,7 +100,12 @@ public class ApiClient(string apiUrl) : IApiClient Log.Debug("Sending request"); - return await httpClient.PostAsync(url, content); + var httpmsg = new HttpRequestMessage(method, url) + { + Content = content + }; + + return await httpClient.SendAsync(httpmsg); } protected virtual string? EncodeAuthKey() diff --git a/Pilz.Net/Api/ApiMessageHandlerAttribute.cs b/Pilz.Net/Api/ApiMessageHandlerAttribute.cs index 3bff5e8..1fd357e 100644 --- a/Pilz.Net/Api/ApiMessageHandlerAttribute.cs +++ b/Pilz.Net/Api/ApiMessageHandlerAttribute.cs @@ -1,9 +1,21 @@ namespace Pilz.Net.Api; [AttributeUsage(AttributeTargets.Method)] -public class ApiMessageHandlerAttribute(string route) : Attribute +public class ApiMessageHandlerAttribute(string route, params string[] methods) : Attribute { - public string Route { get; set; } = route; + public ApiMessageHandlerAttribute(string route, params HttpMethod[] methods) + : this(route, methods.Select(m => m.Method).ToArray()) + { + + } + + public ApiMessageHandlerAttribute(string route) + : this(route, HttpMethod.Post) + { + } + + public string Route { get; } = route; + public string[] Methods { get; } = methods; public Type? Serializer { get; set; } public bool RequiesAuth { get; set; } } diff --git a/Pilz.Net/Api/ApiRequestInfo.cs b/Pilz.Net/Api/ApiRequestInfo.cs index e14a5be..2a09d79 100644 --- a/Pilz.Net/Api/ApiRequestInfo.cs +++ b/Pilz.Net/Api/ApiRequestInfo.cs @@ -6,4 +6,6 @@ public record class ApiRequestInfo( ApiMessage? Message, [property: MemberNotNullWhen(true, "AuthKey")] bool IsAuthenticated, - string? AuthKey); + string? AuthKey, + string Url, + string Method); diff --git a/Pilz.Net/Api/ApiServer.cs b/Pilz.Net/Api/ApiServer.cs index 425124a..feade8a 100644 --- a/Pilz.Net/Api/ApiServer.cs +++ b/Pilz.Net/Api/ApiServer.cs @@ -1,14 +1,20 @@ using Castle.Core.Logging; +using Newtonsoft.Json.Linq; using Pilz.Extensions.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.RegularExpressions; +using System.Web; using static Pilz.Net.Api.IApiServer; namespace Pilz.Net.Api; public class ApiServer(string apiUrl) : IApiServer { - protected readonly Dictionary handlers = []; + protected readonly List handlers = []; protected readonly Dictionary serializers = []; protected readonly HttpListener httpListener = new(); @@ -16,6 +22,10 @@ public class ApiServer(string apiUrl) : IApiServer public event OnCheckContextEventHandler? OnCheckContext; public event OnCheckContextEventHandler? OnCheckContextCompleted; + protected record PrivateParameterInfo(string Name, int Index); + + protected record PrivateMessageHandler(string Url, bool UseRegEx, Delegate Handler, PrivateParameterInfo[] Parameters, ApiMessageHandlerAttribute Attribute); + protected record PrivateApiResult(ApiResult Original, string? ResultJson); public string ApiUrl { get; } = apiUrl; @@ -72,10 +82,33 @@ public class ApiServer(string apiUrl) : IApiServer return; } + // Resolves parameters + var url = attribute.Route; + var useRegEx = false; + var nextBreacket = url.IndexOf('{'); + var parameters = new List(); + while (nextBreacket != -1) + { + var endBreacket = url.IndexOf('}', nextBreacket + 1); + if (endBreacket == -1) + { + var name = url.Substring(nextBreacket + 1, endBreacket - nextBreacket - 1); + const string regex = "[A-Za-z0-9%]+"; + url = url.Replace(url.Substring(nextBreacket, endBreacket + 1), regex); + + var index = url.Substring(0, nextBreacket + 1).Split('/').Length; + parameters.Add(new(name, index)); + + useRegEx = true; + nextBreacket = url.IndexOf('{', endBreacket + 1); + } + } + if (useRegEx) + url = url.Replace(".", "\\."); // Escape special characters + // Add handler - var fullUrl = attribute.Route; - Log.InfoFormat("Added handler for {0}", fullUrl); - handlers.Add(fullUrl, handler); + Log.InfoFormat("Added handler for {0}", attribute.Route); + handlers.Add(new(url, useRegEx, handler, [.. parameters], attribute)); } protected void Receive() @@ -143,16 +176,6 @@ public class ApiServer(string apiUrl) : IApiServer context.Response.OutputStream.Close(); } - Log.Debug("Sanity checks"); - if (context.Request.HttpMethod != HttpMethod.Post.Method - || context.Request.ContentType is not string contentType - || !contentType.Contains("application/json")) - { - Log.Info("Request has no json content"); - close(); - return; - } - // Parse url Log.Debug("Parse url"); var path = context.Request.Url?.PathAndQuery.Replace(ApiUrl, string.Empty); @@ -163,25 +186,51 @@ public class ApiServer(string apiUrl) : IApiServer return; } - // Read input content - Log.Debug("Read input content"); - string? contentJson; - if (context.Request.ContentLength64 > 0) + // Find handler + Log.Debug("Find handler"); + if (!TryGetHandler(path, context.Request.HttpMethod, out var handler)) { - using StreamReader input = new(context.Request.InputStream); - contentJson = input.ReadToEnd(); + Log.Info("Request handler couldn't be found"); + close(); + return; } - else - contentJson = null; // Get auth key Log.Debug("Get auth key"); if (context.Request.Headers.Get("API-AUTH-KEY") is not string authKey) authKey = null!; + // Read input content + Log.Debug("Read input content"); + string? contentJson; + if (context.Request.ContentType is string contentType + && contentType.Contains("application/json") + && context.Request.ContentLength64 > 0) + { + try + { + using StreamReader input = new(context.Request.InputStream); + contentJson = input.ReadToEnd(); + } + catch (OutOfMemoryException) + { + Log.Error("Error reading remote data due to missing memory"); + close(); + return; + } + catch (Exception ex) + { + Log.Error("Error reading remote data", ex); + close(); + return; + } + } + else + contentJson = null; + // Handle message Log.Debug("Handle mssage"); - if (HandleMessage(path, contentJson, authKey) is not PrivateApiResult result) + if (HandleMessage(path, context.Request.HttpMethod, handler, contentJson, authKey) is not PrivateApiResult result) { Log.Warn("Request couldn't be handled"); close(); @@ -206,28 +255,22 @@ public class ApiServer(string apiUrl) : IApiServer close(); } - protected virtual PrivateApiResult? HandleMessage(string url, string? json, string? authKey) + protected virtual PrivateApiResult? HandleMessage(string url, string method, PrivateMessageHandler handler, string? json, string? authKey) { - // Get handler - Log.Debug("Find handler"); - if (!handlers.TryGetValue(url, out var handler) - || handler.Method.GetCustomAttribute() is not ApiMessageHandlerAttribute attribute) - return null; - // Check authentication Log.Debug("Check authentication"); var isAuthenticated = false; if (!string.IsNullOrWhiteSpace(authKey) && DecodeAuthKey(authKey) is string authKeyDecoded) - isAuthenticated = CheckAuthentication(authKeyDecoded, handler); + isAuthenticated = CheckAuthentication(authKeyDecoded, handler.Handler); else authKeyDecoded = null!; - if (attribute.RequiesAuth && !isAuthenticated) + if (handler.Attribute.RequiesAuth && !isAuthenticated) return new(ApiResult.Unauthorized(), null); // Get required infos Log.Debug("Identify message parameter type and serializer"); - var targetType = handler.Method.GetParameters().FirstOrDefault(p => p.ParameterType.IsAssignableTo(typeof(ApiMessage)))?.ParameterType; - var serializer = GetSerializer(attribute.Serializer); + var targetType = handler.Handler.Method.GetParameters().FirstOrDefault(p => p.ParameterType.IsAssignableTo(typeof(ApiMessage)))?.ParameterType; + var serializer = GetSerializer(handler.Attribute.Serializer); // Deserialize Log.Debug("Deserialize message"); @@ -239,8 +282,8 @@ public class ApiServer(string apiUrl) : IApiServer // Invoke handler Log.Debug("Invoke handler"); - var parameters = BuildParameters(handler, () => message, () => new(message, isAuthenticated, authKeyDecoded)); - if (handler.DynamicInvoke(parameters) is not ApiResult result) + var parameters = BuildParameters(url, handler, () => message, () => new(message, isAuthenticated, authKeyDecoded, url, method)); + if (handler.Handler.DynamicInvoke(parameters) is not ApiResult result) return new(ApiResult.InternalServerError(), null); // Return result without message @@ -258,9 +301,25 @@ public class ApiServer(string apiUrl) : IApiServer return new(result, resultStr); } - protected virtual object?[]? BuildParameters(Delegate handler, Func getMessage, Func getRequestInfo) + protected virtual bool TryGetHandler(string url, string method, [NotNullWhen(true)] out PrivateMessageHandler? handler) { - var infos = handler.Method.GetParameters(); + handler = handlers.FirstOrDefault(handler => + { + if (!handler.Attribute.Methods.Contains(method)) + return false; + + if (handler.UseRegEx) + return Regex.IsMatch(url, handler.Url, RegexOptions.IgnoreCase); + + return handler.Url.Equals(url, StringComparison.InvariantCultureIgnoreCase); + }); + + return handler != null; + } + + protected virtual object?[]? BuildParameters(string url, PrivateMessageHandler handler, Func getMessage, Func getRequestInfo) + { + var infos = handler.Handler.Method.GetParameters(); var objs = new List(); foreach (var info in infos) @@ -269,6 +328,9 @@ public class ApiServer(string apiUrl) : IApiServer objs.Add(getMessage()); else if (info.ParameterType.IsAssignableTo(typeof(ApiRequestInfo))) objs.Add(getRequestInfo()); + else if (handler.Parameters.FirstOrDefault(p => p.Name.Equals(info.Name, StringComparison.InvariantCultureIgnoreCase)) is PrivateParameterInfo parameterInfo + && url.Split('/').ElementAtOrDefault(parameterInfo.Index) is string parameterValue) + objs.Add(Convert.ChangeType(HttpUtility.UrlDecode(parameterValue), info.ParameterType)); // or Uri.UnescapeDataString(); maybe run this line twice? else objs.Add(null); } diff --git a/Pilz.Net/Api/IApiClient.cs b/Pilz.Net/Api/IApiClient.cs index 4708913..e7173df 100644 --- a/Pilz.Net/Api/IApiClient.cs +++ b/Pilz.Net/Api/IApiClient.cs @@ -14,13 +14,21 @@ public interface IApiClient Task SendRequest(string route); + Task SendRequest(string route, HttpMethod method); + Task SendRequest(string route, ApiMessage? message); - Task SendRequest(string route, ApiMessage? message, IApiMessageSerializer? serializer); + Task SendRequest(string route, HttpMethod method, ApiMessage? message); + + Task SendRequest(string route, HttpMethod method, ApiMessage? message, IApiMessageSerializer? serializer); Task> SendRequest(string route) where TResponse : ApiMessage; + Task> SendRequest(string route, HttpMethod method) where TResponse : ApiMessage; + Task> SendRequest(string route, ApiMessage? message) where TResponse : ApiMessage; - Task> SendRequest(string route, ApiMessage? message, IApiMessageSerializer? serializer) where TResponse : ApiMessage; + Task> SendRequest(string route, HttpMethod method, ApiMessage? message) where TResponse : ApiMessage; + + Task> SendRequest(string route, HttpMethod method, ApiMessage? message, IApiMessageSerializer? serializer) where TResponse : ApiMessage; } diff --git a/Pilz.UI/Controls/ConfigurationManager/ConfigurationEntry.cs b/Pilz.UI/Controls/ConfigurationManager/ConfigurationEntry.cs new file mode 100644 index 0000000..c3558bd --- /dev/null +++ b/Pilz.UI/Controls/ConfigurationManager/ConfigurationEntry.cs @@ -0,0 +1,10 @@ +namespace Pilz.UI.Controls.ConfigurationManager; + +public class ConfigurationEntry(string name, string title, ConfigurationValueListener Listener) +{ + public event EventHandler? OnValueChanged; + + public string Name { get; } = name; + public string Title { get; } = title; + public ConfigurationValueListener Listener { get; } = Listener; +} diff --git a/Pilz.UI/Controls/ConfigurationManager/ConfigurationManager.cs b/Pilz.UI/Controls/ConfigurationManager/ConfigurationManager.cs new file mode 100644 index 0000000..de185b6 --- /dev/null +++ b/Pilz.UI/Controls/ConfigurationManager/ConfigurationManager.cs @@ -0,0 +1,20 @@ +namespace Pilz.UI.Controls.ConfigurationManager; + +public class ConfigurationManager +{ + private readonly List panels = []; + + public void Register(ConfigurationPanel panel) + { + panels.Add(panel); + } + + public IEnumerable Build() + { + foreach (var panel in panels) + { + panel.Build(this); + yield return panel; + } + } +} diff --git a/Pilz.UI/Controls/ConfigurationManager/ConfigurationPanel.cs b/Pilz.UI/Controls/ConfigurationManager/ConfigurationPanel.cs new file mode 100644 index 0000000..654be41 --- /dev/null +++ b/Pilz.UI/Controls/ConfigurationManager/ConfigurationPanel.cs @@ -0,0 +1,32 @@ +namespace Pilz.UI.Controls.ConfigurationManager; + +public class ConfigurationPanel : TableLayoutPanel +{ + private readonly List entries = []; + + public ConfigurationEntry CreateEntry(string name, string title) + { + return CreateEntry(name, title, null); + } + + public ConfigurationEntry CreateEntry(string name, string title, Action? create) + { + return CreateEntry(new(name, title, )); + } + + public ConfigurationEntry CreateEntry(ConfigurationEntry entry) + { + entries.Add(entry); + return entry; + } + + internal protected void Build(ConfigurationManager manager) + { + foreach (var entry in entries) + { + var control = ; + entry.Listener.Initialize(); + // ... + } + } +} diff --git a/Pilz.UI/Controls/ConfigurationManager/ConfigurationValueListener.cs b/Pilz.UI/Controls/ConfigurationManager/ConfigurationValueListener.cs new file mode 100644 index 0000000..7e674fc --- /dev/null +++ b/Pilz.UI/Controls/ConfigurationManager/ConfigurationValueListener.cs @@ -0,0 +1,15 @@ +namespace Pilz.UI.Controls.ConfigurationManager; + +public abstract class ConfigurationValueListener : IDisposable +{ + public event EventHandler? ValueChanged; + + protected virtual void OnValueChanged(object sender, EventArgs e) + { + ValueChanged?.Invoke(sender, e); + } + + internal protected abstract void Initialize(Control control); + + public abstract void Dispose(); +}