using Pilz.Extensions.Reflection; using System.Net; using System.Reflection; using static Pilz.Net.Api.IApiServer; namespace Pilz.Net.Api; public class ApiServer(string apiUrl) : IApiServer { protected readonly Dictionary handlers = []; protected readonly Dictionary serializers = []; protected readonly HttpListener httpListener = new(); public event OnCheckAuthenticationEventHandler? OnCheckAuthentication; protected record PrivateApiResult(ApiResult Original, string? ResultJson); public string ApiUrl { get; } = apiUrl; public virtual bool EnableAuth { get; set; } public IMessageSerializer Serializer { get; set; } = new DefaultMessageSerializer(); public virtual void Start() { httpListener.Start(); Listen(); } public virtual void Stop() { httpListener.Stop(); } public virtual void RegisterHandler(T instance) where T : class { // Get all public instance methods var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public); // Register each method foreach (var method in methods) RegisterHandler(method.CreateDelegate(instance)); } public virtual void RegisterHandler(Delegate handler) { var method = handler.Method; // Sanity checks if (method.GetCustomAttribute() is not MessageHandlerAttribute attribute || method.GetParameters().FirstOrDefault() is not ParameterInfo parameter || !parameter.ParameterType.IsAssignableTo(typeof(ApiMessage)) || !method.ReturnType.IsAssignableTo(typeof(ApiResult))) throw new NotSupportedException("The first parameter needs to be of type ApiMessage and must return an ApiResult object and the method must have the MessageHandlerAttribute."); // Add handler handlers.Add(ApiUrl + attribute.Route, handler); } protected virtual void Listen() { while (httpListener.IsListening) CheckContext(); } protected virtual void CheckContext() { var context = httpListener.GetContext(); void close() => context.Response.OutputStream.Close(); if (context.Request.HttpMethod != HttpMethod.Post.Method || context.Request.ContentType is not string contentType || !contentType.Contains("application/json")) { close(); return; } // Parse url var path = context.Request.Url?.PathAndQuery.Replace(ApiUrl, string.Empty); if (string.IsNullOrWhiteSpace(path) || context.Request.ContentLength64 <= 0) { close(); return; } // Read input content using StreamReader input = new(context.Request.InputStream); var contentJson = input.ReadToEnd(); // Get auth key if (context.Request.Headers.Get("API-AUTH-KEY") is not string authKey) authKey = null!; // Handle message if (HandleMessage(path, contentJson, authKey) is not PrivateApiResult result) { close(); return; } // Set response parameters context.Response.StatusCode = (int)result.Original.StatusCode; // Write response content if (result.ResultJson is not null) { context.Response.ContentType = "application/json"; using StreamWriter output = new(context.Response.OutputStream); output.Write(result); } close(); } protected virtual PrivateApiResult? HandleMessage(string url, string json, string? authKey) { // Get handler if (!handlers.TryGetValue(url, out var handler) || handler.Method.GetCustomAttribute() is not MessageHandlerAttribute attribute) return null; // Check authentication if (attribute.RequiesAuth && (string.IsNullOrWhiteSpace(authKey) || !CheckAuthentication(authKey))) return null; // Get required infos var targetType = handler.Method.GetParameters().First().ParameterType; var serializer = GetSerializer(attribute.Serializer); // Deserialize if (serializer.Deserialize(json, targetType) is not ApiMessage message) return null; // Invoke handler if (handler.DynamicInvoke(message) is not ApiResult result) return null; // Return result without message if (result.Message is null) return new(result, null); // Serializer if (serializer.Serialize(result.Message) is not string resultStr) return null; // Return result with message return new(result, resultStr); } protected virtual IMessageSerializer GetSerializer(Type? t) { if (t is not null) { if (serializers.TryGetValue(t, out var s) && s is not null) return s; else if (Activator.CreateInstance(t) is IMessageSerializer ss) { serializers.Add(t, ss); return ss; } } return Serializer; } protected virtual bool CheckAuthentication(string authKey) { if (OnCheckAuthentication != null) { var args = new ApiAuthCheckEventArgs(authKey); OnCheckAuthentication?.Invoke(this, args); return args.Valid; } return false; } }