using Pilz.Extensions.Reflection; using System; using System.Net; using System.Reflection; using System.Reflection.PortableExecutable; namespace Pilz.Networking.Api; public class ApiServer(string apiUrl) : ApiClient, IApiServer { protected readonly Dictionary handlers = []; protected readonly Dictionary serializers = []; protected readonly HttpListener httpListener = new(); public string ApiUrl { get; } = apiUrl; public IMessageSerializer Serializer { get; set; } = new DefaultMessageSerializer(); public virtual void Start() { httpListener.Start(); Listen(); } 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(ApiMessage))) throw new NotSupportedException("The first parameter needs to be of type ApiMessage and must also return an ApiMessage object and the method must have the MessageHAndlerAttribute."); // Add handler handlers.Add(ApiUrl + attribute.Url, handler); } protected virtual void Listen() { while (httpListener.IsListening) { var context = httpListener.GetContext(); if (context.Request.HttpMethod != HttpMethod.Post.Method || context.Request.ContentType is not string contentType || contentType.Contains("application/json") || context.Request.AcceptTypes is null || context.Request.AcceptTypes.Contains("application/json")) { close(); continue; } // Parse url var path = context.Request.Url?.PathAndQuery.Replace(ApiUrl, string.Empty); if (string.IsNullOrWhiteSpace(path) || context.Request.ContentLength64 <= 0) { close(); continue; } // Read input content using StreamReader input = new(context.Request.InputStream); var contentJson = input.ReadToEnd(); // Handle message if (HandleMessage(path, contentJson) is not string resultJson) { close(); continue; } // Set response parameters context.Response.StatusCode = (int)args.ResponseStatusCode; context.Response.StatusDescription = args.ResponseStatusDescription; // Write response content context.Response.ContentType = ContentTypes.CONTENT_TYPE_JSON; using StreamWriter output = new(context.Response.OutputStream); output.Write(resultJson); close(); void close() => context.Response.OutputStream.Close(); } } protected virtual string? HandleMessage(string url, string json) { // Get handler if (!handlers.TryGetValue(url, out var handler) || handler.Method.GetCustomAttribute() is not MessageHandlerAttribute attribute) 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 ApiMessage result) return null; // Serializer if (serializer.Serialize(result) is not string resultStr) return null; return 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; } }