using Castle.Core.Logging; 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 IApiMessageSerializer Serializer { get; set; } = new DefaultApiMessageSerializer(); public ILogger Log { get; set; } = NullLogger.Instance; public virtual void Start() { Log.Info("Start listening"); httpListener.Prefixes.Add(ApiUrl + "/"); httpListener.Start(); Task.Run(Listen); } public virtual void Stop() { httpListener.Stop(); Log.Info("Stopped listening"); } 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), false); } public virtual void RegisterHandler(Delegate handler) { RegisterHandler(handler, true); } public virtual void RegisterHandler(Delegate handler, bool throwOnError) { var method = handler.Method; // Sanity checks if (method.GetCustomAttribute() is not ApiMessageHandlerAttribute attribute || !method.ReturnType.IsAssignableTo(typeof(ApiResult))) { if (throwOnError) 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."); return; } // Add handler var fullUrl = attribute.Route; Log.InfoFormat("Added handler for {0}", fullUrl); handlers.Add(fullUrl, handler); } protected virtual void Listen() { while (httpListener.IsListening) CheckContext(); } protected virtual void CheckContext() { var context = httpListener.GetContext(); void close() { Log.Info("End handling request."); 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); if (string.IsNullOrWhiteSpace(path)) { Log.Info("Request has no path"); close(); return; } // Read input content Log.Debug("Read input content"); string? contentJson; if (context.Request.ContentLength64 > 0) { using StreamReader input = new(context.Request.InputStream); contentJson = input.ReadToEnd(); } 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!; // Handle message Log.Debug("Handle mssage"); if (HandleMessage(path, contentJson, authKey) is not PrivateApiResult result) { Log.Warn("Request couldn't be handled"); close(); return; } // Set response parameters Log.Debug("Set response parameters"); context.Response.StatusCode = (int)result.Original.StatusCode; // Write response content Log.Debug("Write response"); if (result.ResultJson is not null) { Log.Info("Write response"); context.Response.ContentType = "application/json"; using StreamWriter output = new(context.Response.OutputStream); output.Write(result.ResultJson); } Log.Debug("Finish response"); close(); } protected virtual PrivateApiResult? HandleMessage(string url, 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); else authKeyDecoded = null!; if (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); // Deserialize Log.Debug("Deserialize message"); ApiMessage? message; if (json != null && targetType != null) message = serializer.Deserialize(json, targetType); else message = null; // Invoke handler Log.Debug("Invoke handler"); var parameters = BuildParameters(handler, () => message, () => new(message, isAuthenticated, authKeyDecoded)); if (handler.DynamicInvoke(parameters) is not ApiResult result) return new(ApiResult.InternalServerError(), null); // Return result without message Log.Debug("Check message"); if (result.Message is null) return new(result, null); // Serializer Log.Debug("Serialize message"); if (serializer.Serialize(result.Message) is not string resultStr) return new(ApiResult.InternalServerError(), null); // Return result with message Log.Debug("Complete result"); return new(result, resultStr); } protected virtual object?[]? BuildParameters(Delegate handler, Func getMessage, Func getRequestInfo) { var infos = handler.Method.GetParameters(); var objs = new List(); foreach (var info in infos) { if (info.ParameterType.IsAssignableTo(typeof(ApiMessage))) objs.Add(getMessage()); else if (info.ParameterType.IsAssignableTo(typeof(ApiRequestInfo))) objs.Add(getRequestInfo()); else objs.Add(null); } return [.. objs]; } protected virtual IApiMessageSerializer 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 IApiMessageSerializer 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; } protected virtual string? DecodeAuthKey(string authKey) { return authKey; } }