Pārlūkot izejas kodu

Not Working IPCLClient is messy

Frank van den Bos 2 gadi atpakaļ
vecāks
revīzija
c196a6c20f

+ 3 - 2
InABox.Core/Serialization.cs

@@ -137,8 +137,9 @@ namespace InABox.Core
                 //else 
                 if (typeof(T).IsArray)
                 {
-                    object o = Array.CreateInstance(typeof(T).GetElementType(), 0);
-                    ret = (T)o;
+                    ret = JsonConvert.DeserializeObject<T>(json, settings);
+                    //object o = Array.CreateInstance(typeof(T).GetElementType(), 0);
+                    //ret = (T)o;
                 }
                 else
                 {

+ 11 - 2
InABox.Serialization.Json/JsonSerializer.cs

@@ -1,4 +1,7 @@
 using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
 using InABox.Core;
 using Newtonsoft.Json;
 
@@ -32,8 +35,14 @@ namespace InABox.Serialization
             {
                 if (typeof(TType).IsArray)
                 {
-                    object o = Array.CreateInstance(typeof(TType).GetElementType(), 0);
-                    ret = (TType)o;
+                    ret = JsonConvert.DeserializeObject<TType>(data, serializerSettings);
+                    // JsonConvert.DeserializeObject<JArray>(data,)
+                    // var listtype = typeof(List<>).MakeGenericType(typeof(TType).GetElementType());
+                    // var list = JsonConvert.DeserializeObject(data, listtype, serializerSettings) as IList;
+                    // new ArrayList
+                    // ret = (TType)(list.To
+                    //object o = Array.CreateInstance(typeof(TType).GetElementType(), 0);
+                    //ret = (TType)o;
                 }
                 else
                 {

+ 237 - 0
InABox.Server/IPC/IPCServer.cs

@@ -0,0 +1,237 @@
+using H.Pipes;
+using H.Pipes.AccessControl;
+using InABox.API;
+using InABox.Clients;
+using InABox.Core;
+using System.IO.Pipes;
+using System.Reflection;
+using System.Security.Principal;
+
+namespace InABox.IPC
+{
+    public class IPCServer : IDisposable
+    {
+        PipeServer<IPCRequest> Server;
+
+        public IPCServer(string name)
+        {
+            Server = new PipeServer<IPCRequest>(name);
+
+            #if WINDOWS
+            SetPipeSecurity();
+            #endif
+
+            Server.ClientConnected += Server_ClientConnected;
+            Server.ClientDisconnected += Server_ClientDisconnected;
+            Server.MessageReceived += Server_MessageReceived;
+            Server.ExceptionOccurred += Server_ExceptionOccurred;
+        }
+
+        private void SetPipeSecurity()
+        {
+            #pragma warning disable CA1416
+            
+            var pipeSecurity = new PipeSecurity();
+
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSid, null), PipeAccessRights.ReadWrite,
+                System.Security.AccessControl.AccessControlType.Allow));
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null), PipeAccessRights.ReadWrite,
+                System.Security.AccessControl.AccessControlType.Allow));
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.ReadWrite,
+                System.Security.AccessControl.AccessControlType.Allow));
+
+            Server.SetPipeSecurity(pipeSecurity);
+            
+            #pragma warning restore CA1416
+        }
+
+        private void Server_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
+        {
+            Logger.Send(LogType.Error, "", $"Exception Occurred: {e.Exception.Message}");
+        }
+
+        public void Start()
+        {
+            Server.StartAsync().Wait();
+        }
+
+        private static List<Type>? _persistentRemotable;
+
+        private static Type? GetEntity(string entityName)
+        {
+            _persistentRemotable ??= CoreUtils.TypeList(
+                e => e.IsSubclassOf(typeof(Entity)) &&
+                     e.GetInterfaces().Contains(typeof(IRemotable)) &&
+                     e.GetInterfaces().Contains(typeof(IPersistent))).ToList();
+            return _persistentRemotable.FirstOrDefault(x => x.Name == entityName);
+        }
+
+        private static Type? GetResponseType(Method method, string? entityName)
+        {
+            if(entityName != null)
+            {
+                var entityType = GetEntity(entityName);
+                if(entityType != null)
+                {
+                    var response = method switch
+                    {
+                        Method.Query => typeof(QueryResponse<>).MakeGenericType(entityType),
+                        Method.Delete => typeof(DeleteResponse<>).MakeGenericType(entityType),
+                        Method.MultiDelete => typeof(MultiDeleteResponse<>).MakeGenericType(entityType),
+                        Method.Save => typeof(SaveResponse<>).MakeGenericType(entityType),
+                        Method.MultiSave => typeof(MultiSaveResponse<>).MakeGenericType(entityType),
+                        _ => null
+                    };
+                    if (response != null) return response;
+                }
+            }
+            return method switch
+            {
+                Method.QueryMultiple => typeof(MultiQueryResponse),
+                Method.Validate => typeof(ValidateResponse),
+                Method.Check2FA => typeof(Check2FAResponse),
+                _ => null
+            };
+        }
+
+        private static IPCRequest QueryMultiple(IPCRequest request)
+        {
+            var response = RestService.QueryMultiple(request.GetRequest<MultiQueryRequest>(), true);
+            return request.Respond(response);
+        }
+
+        private static IPCRequest Validate(IPCRequest request)
+        {
+            var response = RestService.Validate(request.GetRequest<ValidateRequest>());
+            return request.Respond(response);
+        }
+
+        private static IPCRequest Ping(IPCRequest request) => request.Respond(new PingResponse().Status(StatusCode.OK));
+
+        private static IPCRequest Info(IPCRequest request)
+        {
+            var response = RestService.Info(request.GetRequest<InfoRequest>());
+            return request.Respond(response);
+        }
+        
+        private static IPCRequest Check2FA(IPCRequest request)
+        {
+            var response = RestService.Check2FA(request.GetRequest<Check2FARequest>());
+            return request.Respond(response);
+        }
+
+        private static IPCRequest Query<T>(IPCRequest request) where T : Entity, new()
+        {
+            var response = RestService<T>.List(request.GetRequest<QueryRequest<T>>());
+            return request.Respond(response);
+        }
+
+        private static IPCRequest Save<T>(IPCRequest request) where T : Entity, new()
+        {
+            var response = RestService<T>.Save(request.GetRequest<SaveRequest<T>>());
+            return request.Respond(response);
+        }
+        private static IPCRequest MultiSave<T>(IPCRequest request) where T : Entity, new()
+        {
+            var response = RestService<T>.MultiSave(request.GetRequest<MultiSaveRequest<T>>());
+            return request.Respond(response);
+        }
+
+        private static IPCRequest Delete<T>(IPCRequest request) where T : Entity, new()
+        {
+            var response = RestService<T>.Delete(request.GetRequest<DeleteRequest<T>>());
+            return request.Respond(response);
+        }
+        private static IPCRequest MultiDelete<T>(IPCRequest request) where T : Entity, new()
+        {
+            var response = RestService<T>.MultiDelete(request.GetRequest<MultiDeleteRequest<T>>());
+            return request.Respond(response);
+        }
+
+        private static MethodInfo QueryMethod = GetMethod(nameof(Query));
+        private static MethodInfo SaveMethod = GetMethod(nameof(Save));
+        private static MethodInfo MultiSaveMethod = GetMethod(nameof(MultiSave));
+        private static MethodInfo DeleteMethod = GetMethod(nameof(Delete));
+        private static MethodInfo MultiDeleteMethod = GetMethod(nameof(MultiDelete));
+        private static MethodInfo QueryMultipleMethod = GetMethod(nameof(QueryMultiple));
+        private static MethodInfo ValidateMethod = GetMethod(nameof(Validate));
+        private static MethodInfo Check2FAMethod = GetMethod(nameof(Check2FA));
+        private static MethodInfo PingMethod = GetMethod(nameof(Ping));
+        private static MethodInfo InfoMethod = GetMethod(nameof(Info));
+
+        private static MethodInfo GetMethod(string name) =>
+            typeof(IPCServer).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static)
+            ?? throw new Exception($"Invalid method '{name}'");
+
+        private void Server_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<IPCRequest?> e)
+        {
+            Task.Run(() =>
+            {
+                var start = DateTime.Now;
+                try
+                {
+                    if (e.Message == null) throw new Exception($"Invalid message");
+
+                    var method = e.Message.Method switch
+                    {
+                        Method.Query => QueryMethod,
+                        Method.QueryMultiple => QueryMultipleMethod,
+                        Method.Delete => DeleteMethod,
+                        Method.MultiDelete => MultiDeleteMethod,
+                        Method.Save => SaveMethod,
+                        Method.MultiSave => MultiSaveMethod,
+                        Method.Check2FA => Check2FAMethod,
+                        Method.Validate => ValidateMethod,
+                        Method.Ping => PingMethod,
+                        Method.Info => InfoMethod,
+                        Method.None or _ => throw new Exception($"Invalid method '{e.Message.Method}'")
+                    };
+
+                    if (e.Message.Type != null)
+                    {
+                        var entityType = GetEntity(e.Message.Type) ?? throw new Exception($"No entity '{e.Message.Type}'");
+                        method = method.MakeGenericMethod(entityType);
+                    }
+
+                    var response = method.Invoke(null, new object[] { e.Message }) as IPCRequest;
+                    e.Connection.WriteAsync(response);
+                }
+                catch (Exception err)
+                {
+                    Logger.Send(LogType.Error, "", err.Message);
+                    if (e.Message != null)
+                    {
+                        var responseType = GetResponseType(e.Message.Method, e.Message.Type);
+                        if (responseType != null)
+                        {
+                            var response = (Activator.CreateInstance(responseType) as Response)!;
+                            response.Status = StatusCode.Error;
+                            response.Messages.Add(err.Message);
+                            e.Connection.WriteAsync(e.Message.Respond(response));
+                        }
+                    }
+                }
+            });
+        }
+
+        private void Server_ClientDisconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCRequest> e)
+        {
+            Logger.Send(LogType.Information, "", "Client Disconnected");
+            e.Connection.DisposeAsync();
+        }
+
+        private void Server_ClientConnected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCRequest> e)
+        {
+            Logger.Send(LogType.Information, "", "Client Connected");
+        }
+
+        public void Dispose()
+        {
+            Server.DisposeAsync().AsTask().Wait();
+        }
+        ~IPCServer()
+        {
+            Dispose();
+        }
+    }
+}

+ 119 - 0
InABox.Server/IPC/RPCServerTransport.cs

@@ -0,0 +1,119 @@
+using System.IO.Pipes;
+using System.Security.Principal;
+using H.Pipes;
+using H.Pipes.AccessControl;
+using InABox.Core;
+
+namespace InABox.IPC
+{
+    public class RPCServerTransport : IDisposable
+    {
+        PipeServer<RPCMessage> Server;
+
+        private Dictionary<String, IRPCCommandHandler> _commands = new Dictionary<String, IRPCCommandHandler>();
+
+        public void AddCommandHandler<TSender, TCommand, TProperties, TResult>(RPCCommandHandler<TSender,TProperties,TResult> commandHandler) 
+            where TCommand : IRPCCommand<TProperties, TResult> 
+            where TSender : class
+        {
+            _commands[typeof(TCommand).Name] = commandHandler;
+        }
+        
+        public event LogFunction? OnLog;
+
+        private void SetPipeSecurity()
+        {
+
+            #pragma warning disable CA1416
+            
+            var pipeSecurity = new PipeSecurity();
+
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSid, null), PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null), PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
+            pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
+
+            Server.SetPipeSecurity(pipeSecurity);
+            
+            #pragma warning restore CA1416
+
+        }
+
+        public RPCServerTransport(string name)
+        {
+            Server = new PipeServer<RPCMessage>(name);
+
+            #if WINDOWS
+            SetPipeSecurity();
+            #endif
+            
+            Server.ClientConnected += Server_ClientConnected;
+            Server.ClientDisconnected += Server_ClientDisconnected;
+            Server.MessageReceived += Server_MessageReceived;
+            Server.ExceptionOccurred += Server_ExceptionOccurred;
+        }
+
+        private void Server_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
+        {
+            OnLog?.Invoke(LogType.Error, "", $"Exception Occurred: {e.Exception.Message}");
+        }
+
+        public void Start()
+        {
+            Server.StartAsync().Wait();
+        }
+        
+        private void Server_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<RPCMessage?> e)
+        {
+            Task.Run(() =>
+                {
+                    if (e.Message != null)
+                    {
+                        RPCMessage message = new RPCMessage(e.Message.RequestID, e.Message.Command, "");
+                        
+                        if (!_commands.TryGetValue(e.Message.Command, out var command))
+                            message.Error = RPCError.COMMANDNOTFOUND;
+                        else
+                        {
+                            try
+                            {
+                                message.Payload = command.Execute(sender!, e.Message.Payload) ?? "";
+                            }
+                            catch (Exception err)
+                            {
+                                OnLog?.Invoke(LogType.Error, "", err.Message);
+                                message.Error = RPCError.SERVERERROR;
+                            }
+                        }
+
+                        e.Connection.WriteAsync(message);
+
+                        
+                    }
+                    else
+                        OnLog?.Invoke(LogType.Error, "", "Null Message Received");
+                }
+            );
+        }
+
+        private void Server_ClientDisconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<RPCMessage> e)
+        {
+            OnLog?.Invoke(LogType.Information, "", "Client Disconnected");
+            e.Connection.DisposeAsync();
+        }
+
+        private void Server_ClientConnected(object? sender, H.Pipes.Args.ConnectionEventArgs<RPCMessage> e)
+        {
+            OnLog?.Invoke(LogType.Information, "", "Client Connected");
+        }
+
+        public void Dispose()
+        {
+            Server.DisposeAsync().AsTask().Wait();
+        }
+        
+        ~RPCServerTransport()
+        {
+            Dispose();
+        }
+    }
+}

+ 355 - 76
inabox.client.ipc/IPCClient.cs

@@ -1,24 +1,20 @@
-using H.Pipes;
+using InABox.Client.IPC;
+using InABox.Clients;
 using InABox.Core;
-using InABox.IPC.Shared;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace InABox.Client.IPC
+
+namespace InABox.IPC
 {
-    public class IPCClient : IDisposable
+    internal static class LocalCache
     {
-        private PipeClient<PipeRequest> Client;
+        public static string Password { get; set; }
 
-        private ConcurrentDictionary<Guid, ManualResetEventSlim> Events = new();
-        private ConcurrentDictionary<Guid, PipeRequest> Responses = new();
+    }
 
-        private const int DefaultRequestTimeout = 5 * 60 * 1000; // 5 minutes
+    public class IPCClient<TEntity> : BaseClient<TEntity> where TEntity : Entity, new()
+    {
+        private IPCClientTransport _clientTransport;
 
+<<<<<<< Updated upstream
         public delegate void ConnectEvent();
         public delegate void DisconnectEvent();
 
@@ -34,113 +30,396 @@ namespace InABox.Client.IPC
         public event PushEvent? OnPush;
 
         public IPCClient(string name)
+=======
+        public IPCClient(string pipeName)
+>>>>>>> Stashed changes
         {
-            Client = new PipeClient<PipeRequest>(name);
-            Client.Connected += Client_Connected;
-            Client.Disconnected += Client_Disconnected;
-            Client.MessageReceived += Client_MessageReceived;
-            Client.ExceptionOccurred += Client_ExceptionOccurred;
-
-            Client.ConnectAsync();
+            _clientTransport = IPCClientFactory.GetClient(pipeName);
+            Timeout = TimeSpan.FromSeconds(300);
         }
 
-        private void Client_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
+        private static string[]? _types;
+        public override string[] SupportedTypes()
         {
-            Logger.Send(LogType.Error, "", $"Exception occured: {e.Exception.Message}");
+            _types ??= CoreUtils.Entities
+                .Where(x => x.GetInterfaces().Contains(typeof(IPersistent)))
+                .Select(x => x.EntityName().Replace(".", "_"))
+                .ToArray();
+            return _types;
         }
 
-        public PipeRequest Send(PipeRequest request, int timeout = DefaultRequestTimeout)
+        public override DatabaseInfo Info()
+        {
+            try
+            {
+                var request = new InfoRequest();
+                PrepareRequest(request, false);
+                var response = Send(IPCRequest.Info(request)).GetResponse<InfoResponse>();
+                return response.Info;
+            }
+            catch (Exception)
+            {
+                return new DatabaseInfo();
+            }
+        }
+        
+        private void PrepareRequest(Request request, bool doCredentials = true)
         {
-            var start = DateTime.Now;
-            var ev = Queue(request.RequestID);
-            Client.WriteAsync(request);
-            var result = GetResult(request.RequestID, ev, timeout);
-            return result;
+            if(request is not ValidateRequest && _clientTransport.Disconnected)
+            {
+                ClientFactory.Validate(ClientFactory.UserID, LocalCache.Password);
+            }
+
+            if (doCredentials)
+            {
+                request.Credentials.Platform = ClientFactory.Platform;
+                request.Credentials.Version = ClientFactory.Version;
+                request.Credentials.Session = ClientFactory.SessionID;
+            }
+
+            Request.BeforeRequest?.Invoke(request);
         }
 
-        public ManualResetEventSlim Queue(Guid id)
+        private IPCRequest Send(IPCRequest request, int? timeout = null)
         {
-            var ev = new ManualResetEventSlim();
-            Events[id] = ev;
-            return ev;
+            return _clientTransport.Send(request, timeout ?? Convert.ToInt32(Timeout.TotalMilliseconds));
         }
 
-        public PipeRequest GetResult(Guid id, ManualResetEventSlim ev, int timeout)
+        protected override bool DoCheck2FA(string code, Guid? session)
         {
-            if (Responses.TryGetValue(id, out var result))
+            var request = new Check2FARequest { Code = code };
+
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.Check2FA(request)).GetResponse<Check2FAResponse>();
+            if (response != null)
             {
-                Responses.Remove(id, out result);
-                Events.Remove(id, out ev);
-                return result;
+                return response.Status switch
+                {
+                    StatusCode.OK => response.Valid,
+                    StatusCode.Unauthenticated => false,
+                    _ => throw new IPCException(response.Messages),
+                };
             }
 
+            return false;
+        }
+
+        protected override bool DoPing()
+        {
             try
             {
-                if (!ev.Wait(timeout))
+                var request = new PingRequest();
+
+                PrepareRequest(request);
+
+                var response = Send(IPCRequest.Ping(request), 10_000).GetResponse<PingResponse>();
+                if (response != null)
                 {
-                    return PipeRequest.Error(RequestError.TIMEOUT);
+                    return response.Status switch
+                    {
+                        StatusCode.Error or StatusCode.BadServer or StatusCode.Incomplete => throw new IPCException(response.Messages),
+                        _ => true
+                    };
                 }
             }
-            catch (Exception e)
+            catch (Exception) { }
+
+            return false;
+        }
+        
+        protected override void DoDelete(TEntity entity, string auditnote)
+        {
+            var request = new DeleteRequest<TEntity> { Item = entity };
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.Delete(request)).GetResponse<DeleteResponse<TEntity>>();
+            switch (response.Status)
             {
-                Console.WriteLine(e);
-                throw;
+                case StatusCode.OK:
+                    break;
+                case StatusCode.Unauthenticated:
+                    throw new IPCException("Client not authenticated");
+                default:
+                    throw new IPCException(response.Messages);
             }
-            
-            Responses.Remove(id, out result);
-            Events.Remove(id, out ev);
-            return result ?? PipeRequest.Error(RequestError.UNKNOWN);
         }
 
-        private void Client_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<PipeRequest?> e)
+        protected override void DoDelete(IList<TEntity> entities, string auditnote)
         {
-            if (Events.TryGetValue(e.Message.RequestID, out var ev))
+            var items = entities.ToArray();
+            var request = new MultiDeleteRequest<TEntity> { Items = items, AuditNote = auditnote };
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.MultiDelete(request)).GetResponse<MultiDeleteResponse<TEntity>>();
+            switch (response.Status)
             {
-                Responses[e.Message.RequestID] = e.Message;
-                ev.Set();
+                case StatusCode.OK:
+                    break;
+                case StatusCode.Unauthenticated:
+                    throw new IPCException("Client not authenticated");
+                default:
+                    throw new IPCException(response.Messages);
             }
-            else
+        }
+
+        protected override TEntity[] DoLoad(Filter<TEntity> filter = null, SortOrder<TEntity> sort = null)
+        {
+            var request = new QueryRequest<TEntity>
             {
-                Task.Run(() =>
+                Filter = filter,
+                Sort = sort
+            };
+            PrepareRequest(request);
+
+            var result = new List<TEntity>();
+            var response = Send(IPCRequest.Query(request)).GetResponse<QueryResponse<TEntity>>();
+            if (response.Items != null)
+                foreach (var row in response.Items.Rows)
+                    result.Add(row.ToObject<TEntity>());
+            return result.ToArray();
+        }
+
+        protected override CoreTable DoQuery(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort = null)
+        {
+            var request = new QueryRequest<TEntity>
+            {
+                Columns = columns,
+                Filter = filter,
+                Sort = sort
+            };
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.Query(request)).GetResponse<QueryResponse<TEntity>>();
+
+            if (response != null)
+            {
+                return response.Status switch
                 {
-                    OnPush?.Invoke(e.Message);
-                }).ContinueWith(task =>
+                    StatusCode.OK => response.Items,
+                    StatusCode.Unauthenticated => throw new IPCException("Client not authenticated", StatusCode.Unauthenticated),
+                    _ => throw new IPCException(response.Messages),
+                };
+            }
+
+            return null;
+        }
+
+        protected override Dictionary<string, CoreTable> DoQueryMultiple(Dictionary<string, IQueryDef> queries)
+        {
+            var request = new MultiQueryRequest
+            {
+                TableTypes = new(),
+                Filters = new(),
+                Columns = new(),
+                Sorts = new()
+            };
+            foreach (var item in queries)
+            {
+                request.TableTypes[item.Key] = item.Value.Type.EntityName();
+                request.Filters[item.Key] = Serialization.Serialize(item.Value.Filter);
+                request.Columns[item.Key] = Serialization.Serialize(item.Value.Columns);
+                request.Sorts[item.Key] = Serialization.Serialize(item.Value.SortOrder);
+            }
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.QueryMultiple(request)).GetResponse<MultiQueryResponse>();
+            if (response != null)
+            {
+                return response.Status switch
                 {
-                    if (task.Exception != null)
-                    {
-                        Logger.Send(LogType.Error, "", $"Error in IPC Client Push: {CoreUtils.FormatException(task.Exception)}");
-                    }
-                });
+                    StatusCode.OK => response.Tables,
+                    StatusCode.Unauthenticated => throw new IPCException("Client not authenticated"),
+                    _ => throw new IPCException(response.Messages),
+                };
             }
+
+            return null;
         }
 
-        private void Client_Connected(object? sender, H.Pipes.Args.ConnectionEventArgs<PipeRequest> e)
+        protected override void DoSave(TEntity entity, string auditnote)
         {
-            Logger.Send(LogType.Information, "", $"Connected to Pipe: {e.Connection.PipeName}");
-            Disconnected = false;
-            OnConnect?.Invoke();
+            var request = new SaveRequest<TEntity>
+            {
+                Item = entity,
+                AuditNote = auditnote,
+                ReturnOnlyChanged = true
+            };
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.Save(request)).GetResponse<SaveResponse<TEntity>>();
+            switch (response.Status)
+            {
+                case StatusCode.OK:
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    entity.SetObserving(false);
+                    foreach (var prop in props.Keys)
+                    {
+                        var value = CoreUtils.GetPropertyValue(response.Item, prop);
+                        CoreUtils.SetPropertyValue(entity, prop, value);
+                    }
+
+                    entity.CommitChanges();
+                    entity.SetObserving(true);*/
+
+                    entity.SetObserving(false);
+                    foreach (var (key, value) in response.ChangedValues)
+                    {
+                        if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                        {
+                            CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                        }
+                    }
+                    entity.CommitChanges();
+                    entity.SetObserving(true);
+
+                    break;
+                case StatusCode.Unauthenticated:
+                    throw new IPCException("Client not authenticated");
+                default:
+                    throw new IPCException(response.Messages);
+            }
         }
 
-        private void Client_Disconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<PipeRequest> e)
+        protected override void DoSave(IEnumerable<TEntity> entities, string auditnote)
         {
-            Logger.Send(LogType.Information, "", $"Disconnected from Pipe: {e.Connection.PipeName}");
-            foreach (var ev in Events)
+            var items = entities.ToArray();
+            var request = new MultiSaveRequest<TEntity>
             {
-                Responses.TryAdd(ev.Key, PipeRequest.Error(RequestError.DISCONNECTED));
-                ev.Value.Set();
+                Items = items,
+                AuditNote = auditnote,
+                ReturnOnlyChanged = true
+            };
+            PrepareRequest(request);
+
+            var response = Send(IPCRequest.MultiSave(request)).GetResponse<MultiSaveResponse<TEntity>>();
+            switch (response.Status)
+            {
+                case StatusCode.OK:
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    for (var i = 0; i < items.Length; i++)
+                    {
+                        items[i].SetObserving(false);
+                        foreach (var prop in props.Keys)
+                        {
+                            var value = CoreUtils.GetPropertyValue(response.Items[i], prop);
+                            CoreUtils.SetPropertyValue(items[i], prop, value);
+                        }
+
+                        //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
+                        items[i].CommitChanges();
+                        items[i].SetObserving(true);
+                    }*/
+                    for (int i = 0; i < items.Length; ++i)
+                    {
+                        var entity = items[i];
+                        var changedValues = response.ChangedValues[i];
+
+                        entity.SetObserving(false);
+                        foreach (var (key, value) in changedValues)
+                        {
+                            if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                            {
+                                CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                            }
+                        }
+                        entity.CommitChanges();
+                        entity.SetObserving(true);
+                    }
+                    break;
+                case StatusCode.Unauthenticated:
+                    throw new IPCException("Client not authenticated");
+                default:
+                    throw new IPCException(response.Messages);
             }
-            Disconnected = true;
-            OnDisconnect?.Invoke();
         }
 
-        public void Dispose()
+        protected override ValidationData DoValidate(Guid session)
         {
-            Client.DisposeAsync().AsTask().Wait();
+            return Validate(
+                null, null, false, session);
         }
 
-        ~IPCClient()
+        protected override ValidationData DoValidate(string pin, Guid session)
         {
-            Dispose();
+            return Validate(
+                null, pin, true, session);
+        }
+
+        protected override ValidationData DoValidate(string userid, string password, Guid session)
+        {
+            return Validate(
+                userid, password, false, session);
+        }
+
+        private ValidationData Validate(string? userid, string? password, bool usePin, Guid session = default)
+        {
+            var ticks = DateTime.Now.ToUniversalTime().Ticks.ToString();
+            var request = new ValidateRequest { UsePIN = usePin };
+
+            if (usePin)
+            {
+                request.UserID = Encryption.Encrypt(ticks, "wCq9rryEJEuHIifYrxRjxg", true);
+                request.Password = Encryption.Encrypt(ticks, "7mhvLnqMwkCAzN+zNGlyyg", true);
+                request.PIN = password;
+            }
+            else
+            {
+<<<<<<< Updated upstream
+                Task.Run(() =>
+                {
+                    OnPush?.Invoke(e.Message);
+                }).ContinueWith(task =>
+                {
+                    if (task.Exception != null)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error in IPC Client Push: {CoreUtils.FormatException(task.Exception)}");
+                    }
+                });
+=======
+                request.UserID = userid;
+                request.Password = password;
+>>>>>>> Stashed changes
+            }
+            request.Credentials.Platform = ClientFactory.Platform;
+            request.Credentials.Version = ClientFactory.Version;
+            PrepareRequest(request, false);
+            if(session != Guid.Empty)
+            {
+                request.Credentials.Session = session;
+            }
+
+            var response = Send(IPCRequest.Validate(request), 10000).GetResponse<ValidateResponse>();
+            if (response != null)
+                if (response.Status.Equals(StatusCode.OK))
+                {
+                    LocalCache.Password = password;
+                    return new ValidationData(
+                        response.ValidationResult,
+                        response.UserID,
+                        response.UserGuid,
+                        response.SecurityID,
+                        response.Session,
+                        response.Recipient2FA,
+                        response.PasswordExpiration
+
+                    );
+                }
+                else if (response.Status == StatusCode.BadServer)
+                {
+                    throw new IPCException(response.Messages);
+                }
+
+            return new ValidationData(
+                ValidationResult.INVALID,
+                "",
+                Guid.Empty,
+                Guid.Empty,
+                Guid.Empty,
+                null,
+                DateTime.MinValue
+                );
         }
+        
     }
 }

+ 3 - 3
inabox.client.ipc/IPCClientFactory.cs

@@ -11,13 +11,13 @@ namespace InABox.Client.IPC
 {
     public static class IPCClientFactory
     {
-        private static Dictionary<string, IPCClient> Clients = new();
+        private static Dictionary<string, IPCClientTransport> Clients = new();
 
-        public static IPCClient GetClient(string pipeName)
+        public static IPCClientTransport GetClient(string pipeName)
         {
             if (!Clients.TryGetValue(pipeName, out var client))
             {
-                client = new IPCClient(pipeName);
+                client = new IPCClientTransport(pipeName);
                 Clients[pipeName] = client;
                 client.OnPush += Client_OnPush;
             }

+ 126 - 0
inabox.client.ipc/IPCClientTransport.cs

@@ -0,0 +1,126 @@
+using H.Pipes;
+using InABox.Core;
+using InABox.IPC;
+using System.Collections.Concurrent;
+
+namespace InABox.Client.IPC
+{
+    public class IPCClientTransport : IDisposable
+    {
+        private PipeClient<IPCRequest> Client;
+
+        private ConcurrentDictionary<Guid, ManualResetEventSlim> Events = new();
+        private ConcurrentDictionary<Guid, IPCRequest> Responses = new();
+
+        private const int DefaultRequestTimeout = 5 * 60 * 1000; // 5 minutes
+
+        public delegate void ConnectEvent();
+        public delegate void DisconnectEvent();
+
+        public bool Disconnected { get; private set; }
+
+        public event ConnectEvent? OnConnect;
+        public event DisconnectEvent? OnDisconnect;
+
+        public IPCClientTransport(string name)
+        {
+            Client = new PipeClient<IPCRequest>(name);
+            Client.Connected += Client_Connected;
+            Client.Disconnected += Client_Disconnected;
+            Client.MessageReceived += Client_MessageReceived;
+            Client.ExceptionOccurred += Client_ExceptionOccurred;
+
+            Client.ConnectAsync();
+        }
+
+        private void Client_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
+        {
+            Logger.Send(LogType.Error, "", $"Exception occured: {e.Exception.Message}");
+        }
+
+        public IPCRequest Send(IPCRequest request, int timeout = DefaultRequestTimeout)
+        {
+            var start = DateTime.Now;
+            var ev = Queue(request.RequestID);
+            Client.WriteAsync(request);
+            var result = GetResult(request.RequestID, ev, timeout);
+            return result;
+        }
+
+        public ManualResetEventSlim Queue(Guid id)
+        {
+            var ev = new ManualResetEventSlim();
+            Events[id] = ev;
+            return ev;
+        }
+
+        public IPCRequest GetResult(Guid id, ManualResetEventSlim ev, int timeout)
+        {
+            if (Responses.TryGetValue(id, out var result))
+            {
+                Responses.Remove(id, out result);
+                Events.Remove(id, out ev);
+                return result;
+            }
+
+            try
+            {
+                if (!ev.Wait(timeout))
+                {
+                    return IPCRequest.Error(RequestError.TIMEOUT);
+                }
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+                throw;
+            }
+            
+            Responses.Remove(id, out result);
+            Events.Remove(id, out ev);
+            return result ?? IPCRequest.Error(RequestError.UNKNOWN);
+        }
+
+        private void Client_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<IPCRequest?> e)
+        {
+            if (Events.TryGetValue(e.Message.RequestID, out var ev))
+            {
+                Responses[e.Message.RequestID] = e.Message;
+                ev.Set();
+            }
+            else
+            {
+                Responses[e.Message.RequestID] = e.Message;
+            }
+        }
+
+        private void Client_Connected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCRequest> e)
+        {
+            Logger.Send(LogType.Information, "", $"Connected to Pipe: {e.Connection.PipeName}");
+            Disconnected = false;
+            OnConnect?.Invoke();
+        }
+
+        private void Client_Disconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<IPCRequest> e)
+        {
+            Logger.Send(LogType.Information, "", $"Disconnected from Pipe: {e.Connection.PipeName}");
+            foreach (var ev in Events)
+            {
+                Responses.TryAdd(ev.Key, IPCRequest.Error(RequestError.DISCONNECTED));
+                ev.Value.Set();
+            }
+            Disconnected = true;
+            OnDisconnect?.Invoke();
+        }
+
+        public void Dispose()
+        {
+            Client.DisposeAsync().AsTask().Wait();
+        }
+
+        ~IPCClientTransport()
+        {
+            Dispose();
+        }
+    }
+}

+ 141 - 0
inabox.client.ipc/RPCClientTransport.cs

@@ -0,0 +1,141 @@
+using System.Collections.Concurrent;
+using H.Pipes;
+using InABox.Core;
+
+namespace InABox.IPC
+{
+    public class RPCClientTransport : IDisposable
+    {
+        private PipeClient<RPCMessage> Client;
+
+        private ConcurrentDictionary<Guid, ManualResetEventSlim> Events = new();
+        private ConcurrentDictionary<Guid, RPCMessage> Responses = new();
+
+        private const int DefaultRequestTimeout = 5 * 60 * 1000; // 5 minutes
+
+        public delegate void ConnectEvent();
+        public delegate void DisconnectEvent();
+
+        public bool Disconnected { get; private set; }
+
+        public event ConnectEvent? OnConnect;
+        public event DisconnectEvent? OnDisconnect;
+
+        public RPCClientTransport(string name)
+        {
+            Client = new PipeClient<RPCMessage>(name);
+            Client.Connected += Client_Connected;
+            Client.Disconnected += Client_Disconnected;
+            Client.MessageReceived += Client_MessageReceived;
+            Client.ExceptionOccurred += Client_ExceptionOccurred;
+
+            Client.ConnectAsync();
+        }
+
+        private void Client_ExceptionOccurred(object? sender, H.Pipes.Args.ExceptionEventArgs e)
+        {
+            Logger.Send(LogType.Error, "", $"Exception occured: {e.Exception.Message}");
+        }
+
+        public TResult Send<TCommand, TParameters, TResult>(TParameters properties) where TCommand : IRPCCommand<TParameters,TResult>
+        {
+            var request = new RPCMessage(
+                new Guid(),
+                typeof(TCommand).Name,
+                Serialization.Serialize(properties)
+            );
+            var response = Send(request);
+            if (response.Error != RPCError.NONE)
+                throw new Exception($"Exception in {typeof(TCommand).Name}({request.RequestID}): {response.Error}");
+            var result = Serialization.Deserialize<TResult>(response.Payload);
+            if (result == null)
+                throw new Exception($"{typeof(TCommand).Name}({request.RequestID}) returned NULL");
+            return result;
+        }
+        
+        public RPCMessage Send(RPCMessage request, int timeout = DefaultRequestTimeout)
+        {
+            var start = DateTime.Now;
+            var ev = Queue(request.RequestID);
+            Client.WriteAsync(request);
+            var result = GetResult(request.RequestID, ev, timeout);
+            return result;
+        }
+
+        public ManualResetEventSlim Queue(Guid id)
+        {
+            var ev = new ManualResetEventSlim();
+            Events[id] = ev;
+            return ev;
+        }
+
+        public RPCMessage GetResult(Guid id, ManualResetEventSlim ev, int timeout)
+        {
+            if (Responses.TryGetValue(id, out var result))
+            {
+                Responses.Remove(id, out result);
+                Events.Remove(id, out ev);
+                return result;
+            }
+
+            try
+            {
+                if (!ev.Wait(timeout))
+                {
+                    return new RPCMessage(id,"","",RPCError.TIMEOUT);
+                }
+            }
+            catch (Exception e)
+            {
+                Logger.Send(LogType.Error, "", e.Message);
+                throw;
+            }
+            
+            Responses.Remove(id, out result);
+            Events.Remove(id, out ev);
+            return result ?? new RPCMessage(id,"","",RPCError.UNKNOWN);
+        }
+
+        private void Client_MessageReceived(object? sender, H.Pipes.Args.ConnectionMessageEventArgs<RPCMessage?> e)
+        {
+            if (Events.TryGetValue(e.Message.RequestID, out var ev))
+            {
+                Responses[e.Message.RequestID] = e.Message;
+                ev.Set();
+            }
+            else
+            {
+                Responses[e.Message.RequestID] = e.Message;
+            }
+        }
+
+        private void Client_Connected(object? sender, H.Pipes.Args.ConnectionEventArgs<RPCMessage> e)
+        {
+            Logger.Send(LogType.Information, "", $"Connected to Pipe: {e.Connection.PipeName}");
+            Disconnected = false;
+            OnConnect?.Invoke();
+        }
+
+        private void Client_Disconnected(object? sender, H.Pipes.Args.ConnectionEventArgs<RPCMessage> e)
+        {
+            Logger.Send(LogType.Information, "", $"Disconnected from Pipe: {e.Connection.PipeName}");
+            foreach (var ev in Events)
+            {
+                Responses.TryAdd(ev.Key, new RPCMessage(Guid.Empty,"","",RPCError.DISCONNECTED));
+                ev.Value.Set();
+            }
+            Disconnected = true;
+            OnDisconnect?.Invoke();
+        }
+
+        public void Dispose()
+        {
+            Client.DisposeAsync().AsTask().Wait();
+        }
+
+        ~RPCClientTransport()
+        {
+            Dispose();
+        }
+    }
+}

+ 28 - 30
inabox.database.sqlite/SQLiteProvider.cs

@@ -1430,11 +1430,10 @@ namespace InABox.Database.SQLite
                     else
                     {
                         var value = Encode(filter.Value, mexp.Type);
-                        if (IsNull(value))
+                        if (IsNull(value) && ((filter.Operator == Operator.IsEqualTo) || (filter.Operator == Operator.IsNotEqualTo)))
                         {
                             result = string.Format("({0} {1} NULL)", prop,
-                                filter.Operator == Operator.IsEqualTo || filter.Operator == Operator.IsLessThan ||
-                                filter.Operator == Operator.IsLessThanOrEqualTo
+                                filter.Operator == Operator.IsEqualTo 
                                     ? "IS"
                                     : "IS NOT");
                         }
@@ -1454,7 +1453,6 @@ namespace InABox.Database.SQLite
                                 {
                                     result = string.Format("(" + operators[filter.Operator] + ")", prop, sParam);
                                 }
-
                                 command.Parameters.AddWithValue(sParam, value);
                             }
                             else
@@ -1598,17 +1596,21 @@ namespace InABox.Database.SQLite
 
             if (attribute.Operator == FormulaOperator.Maximum)
             {
-                var result = string.Format(
-                    "CASE IFNULL({0},0.00) WHEN IFNULL({0},0.00) < IFNULL({1},0.00) THEN IFNULL({1},0.00) ELSE IFNULL({0},0.00) END",
-                    fieldmap[attribute.Value], fieldmap[attribute.Modifiers.First()]);
+                var parameters = attribute.Modifiers.Select(m => $"IFNULL({fieldmap[m]},0.00)");
+                var result = $"MAX({fieldmap[attribute.Value]}, {String.Join(", ", parameters)})";
+                // var result = string.Format(
+                //     "CASE IFNULL({0},0.00) WHEN IFNULL({0},0.00) < IFNULL({1},0.00) THEN IFNULL({1},0.00) ELSE IFNULL({0},0.00) END",
+                //     fieldmap[attribute.Value], fieldmap[attribute.Modifiers.First()]);
                 return result;
             }
 
             if (attribute.Operator == FormulaOperator.Minumum)
             {
-                var result = string.Format(
-                    "CASE IFNULL({0},0.00) WHEN IFNULL({0},0.00) > IFNULL({1},0.00) THEN IFNULL({1},0.00) ELSE IFNULL({0},0.00) END",
-                    fieldmap[attribute.Value], fieldmap[attribute.Modifiers.First()]);
+                var parameters = attribute.Modifiers.Select(m => $"IFNULL({fieldmap[m]},0.00)");
+                var result = $"MIN({fieldmap[attribute.Value]}, {String.Join(", ", parameters)})";
+                //var result = string.Format(
+                //    "CASE IFNULL({0},0.00) WHEN IFNULL({0},0.00) > IFNULL({1},0.00) THEN IFNULL({1},0.00) ELSE IFNULL({0},0.00) END",
+                //    fieldmap[attribute.Value], fieldmap[attribute.Modifiers.First()]);
                 return result;
             }
 
@@ -1827,28 +1829,24 @@ namespace InABox.Database.SQLite
 
                             if (attr is FormulaAttribute fnc)
                             {
-                                var functionmap = new Dictionary<string, string>();
-
-                                // LogStart();
-                                CheckColumn(columns, fnc.Value);
-                                // LogStop("Formula.CheckColumn");
-
-                                //// LogStart();
-                                LoadFieldsandTables(command, type, prefix, functionmap, tables, columns, fnc.Value, useparams);
-                                //// LogStop("Formula.LoadFieldsAndTables");
-
-                                foreach (var column in fnc.Modifiers)
+                                
+                                // var functionmap = new Dictionary<string, string>();
+                                // CheckColumn(columns, fnc.Value);
+                                // LoadFieldsandTables(command, type, prefix, functionmap, tables, columns, fnc.Value, useparams);
+                                // foreach (var column in fnc.Modifiers)
+                                // {
+                                //     CheckColumn(columns, column);
+                                //     LoadFieldsandTables(command, type, prefix, functionmap, tables, columns, column, useparams);
+                                // }
+                                // fieldmap[columnname] = GetFunction(fnc, functionmap, columnname);
+
+                                foreach (var field in fnc.Modifiers.Prepend(fnc.Value))
                                 {
-                                    // LogStart();
-                                    CheckColumn(columns, column);
-                                    // LogStop("Formula.Modifiers.CheckColumn");
-
-                                    //// LogStart();
-                                    LoadFieldsandTables(command, type, prefix, functionmap, tables, columns, column, useparams);
-                                    //// LogStop("Formula.Modifiers.LoadFieldsAndTables");
+                                    CheckColumn(columns, field);
+                                    LoadFieldsandTables(command, type, prefix, fieldmap, tables, columns, field, useparams);
                                 }
-
-                                fieldmap[columnname] = GetFunction(fnc, functionmap, columnname);
+                                fieldmap[columnname] = GetFunction(fnc, fieldmap, columnname);
+                                
                             }
                             else
                             {

+ 145 - 0
inabox.ipc.shared/IPCRequest.cs

@@ -0,0 +1,145 @@
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.NetworkInformation;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InABox.IPC
+{
+    public enum RequestError
+    {
+        NONE,
+        DISCONNECTED,
+        UNKNOWN,
+        TIMEOUT
+    }
+
+    public enum Method
+    {
+        None,
+        Query,
+        Save,
+        MultiSave,
+        Delete,
+        MultiDelete,
+        QueryMultiple,
+        Validate,
+        Check2FA,
+        Ping,
+        Info
+    }
+
+    [Serializable]
+    public class IPCRequest
+    {
+        public Guid RequestID;
+        public Method Method;
+        public string? Type;
+        public string Data;
+
+        [NonSerialized]
+        public RequestError ErrorCode;
+
+        private IPCRequest(Guid requestID, Method method, string? type, string data, RequestError error = RequestError.NONE)
+        {
+            RequestID = requestID;
+            Method = method;
+            Type = type;
+            Data = data;
+            ErrorCode = error;
+        }
+
+        public IPCRequest Respond<TResponse>(TResponse response) where TResponse : Response
+        {
+            return new IPCRequest(RequestID, Method.None, Type, Serialization.Serialize(response));
+        }
+
+        public TRequest GetRequest<TRequest>()
+        {
+            return Serialization.Deserialize<TRequest>(Data);
+        }
+
+        public TResponse GetResponse<TResponse>() where TResponse : Response, new()
+        {
+            var start = DateTime.Now;
+            var response = Serialization.Deserialize<TResponse>(Data);
+            if (response == null) response = new TResponse();
+            switch (ErrorCode)
+            {
+                case RequestError.NONE:
+                    break;
+                case RequestError.DISCONNECTED:
+                    response.Status = StatusCode.Error;
+                    response.Messages.Add("Server disconnected");
+                    break;
+                case RequestError.UNKNOWN:
+                    response.Status = StatusCode.Error;
+                    response.Messages.Add("Unknown Error");
+                    break;
+                case RequestError.TIMEOUT:
+                    response.Status = StatusCode.Error;
+                    response.Messages.Add("Timeout");
+                    break;
+            }
+            return response;
+        }
+
+        public static IPCRequest Query<T>(QueryRequest<T> request) where T : Entity, new()
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Query, typeof(T).Name, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest Save<T>(SaveRequest<T> request) where T : Entity, new()
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Save, typeof(T).Name, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest MultiSave<T>(MultiSaveRequest<T> request) where T : Entity, new()
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.MultiSave, typeof(T).Name, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest Delete<T>(DeleteRequest<T> request) where T : Entity, new()
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Delete, typeof(T).Name, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest MultiDelete<T>(MultiDeleteRequest<T> request) where T : Entity, new()
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.MultiDelete, typeof(T).Name, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest QueryMultiple(MultiQueryRequest request)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.QueryMultiple, null, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest Validate(ValidateRequest request)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Validate, null, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest Check2FA(Check2FARequest request)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Check2FA, null, Serialization.Serialize(request));
+        }
+
+        public static IPCRequest Error(RequestError error)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.None, null, "", error);
+        }
+
+        public static IPCRequest Ping(PingRequest request)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Ping, null, Serialization.Serialize(request));
+        }
+        
+        public static IPCRequest Info(InfoRequest request)
+        {
+            return new IPCRequest(Guid.NewGuid(), Method.Info, null, Serialization.Serialize(request));
+        }
+    }
+}

+ 32 - 0
inabox.ipc.shared/RPCCommand.cs

@@ -0,0 +1,32 @@
+using System;
+using InABox.Core;
+
+namespace InABox.IPC
+{
+    
+    public interface IRPCCommand<TProperties,TResult> { }
+    
+    public interface IRPCCommandHandler
+    {
+        String? Execute(object sender, String? parameters);
+    }
+    
+    public abstract class RPCCommandHandler<TSender, TParameters, TResult> : IRPCCommandHandler where TSender : class
+    {
+        private TSender _sender;
+
+        public RPCCommandHandler(TSender sender)
+        {
+            _sender = sender ?? throw new ArgumentNullException(nameof(sender));
+        }
+
+        public abstract TResult? Execute(TSender sender, TParameters? parameters);
+
+        public String? Execute(object sender, String? parameters)
+        {
+            var props = Serialization.Deserialize<TParameters>(parameters); 
+            var result = Execute(_sender, props);
+            return Serialization.Serialize(result);
+        }
+    }
+}

+ 12 - 0
inabox.ipc.shared/RPCError.cs

@@ -0,0 +1,12 @@
+namespace InABox.IPC
+{
+    public enum RPCError
+    {
+        NONE,
+        DISCONNECTED,
+        UNKNOWN,
+        TIMEOUT,
+        COMMANDNOTFOUND,
+        SERVERERROR
+    }
+}

+ 27 - 0
inabox.ipc.shared/RPCMessage.cs

@@ -0,0 +1,27 @@
+namespace InABox.IPC
+{
+    [Serializable]
+    public class RPCMessage
+    {
+        public Guid RequestID { get; set; }
+        public String Command { get; set; }
+        public string Payload { get; set; }
+        public RPCError Error { get; set; }
+
+        public RPCMessage()
+        {
+            Command = "";
+            Payload = "";
+            Error = RPCError.NONE;
+        }
+
+        public RPCMessage(Guid requestID, String command, String payload, RPCError error = RPCError.NONE)
+        {
+            RequestID = requestID;
+            Command = command;
+            Payload = payload;
+            Error = error;
+        }
+        
+    }
+}

+ 3 - 0
inabox.wpf/DynamicGrid/Columns/DynamicActionColumn.cs

@@ -27,6 +27,7 @@ namespace InABox.DynamicGrid
         public object Tag { get; set; }
 
         public string HeaderText { get; set; }
+        public bool VerticalHeader { get; set; }
 
         public int Width { get; set; }
         
@@ -87,6 +88,7 @@ namespace InABox.DynamicGrid
             Alignment = Alignment.MiddleCenter;
             Text = text;
             Action = action;
+            VerticalHeader = false;
         }
 
         public override object Data(CoreRow? row) => Text?.Invoke(row);
@@ -98,6 +100,7 @@ namespace InABox.DynamicGrid
         {
             Image = image;
             Action = action;
+            VerticalHeader = true;
         }
 
         public DynamicImageColumn(BitmapImage image, Func<CoreRow?, bool>? action = null)

+ 6 - 6
inabox.wpf/DynamicGrid/DynamicDataGrid.cs

@@ -47,7 +47,7 @@ namespace InABox.DynamicGrid
             }
         }
 
-        private Tuple<string, Filter<TEntity>>? Filter;
+        protected Tuple<string, Filter<TEntity>>? SelectedFilter;
 
         private Column<TEntity>[] FilterColumns;
 
@@ -209,8 +209,8 @@ namespace InABox.DynamicGrid
         {
             //if (sort == null)
             //    sort = new SortOrder<TEntity>(x => x.Sort);
-            if (Filter != null)
-                criteria.Add(Filter.Item2);
+            if (SelectedFilter != null)
+                criteria.Add(SelectedFilter.Item2);
 
             OnReload?.Invoke(this, criteria, columns, ref sort);
             new Client<TEntity>().Query(criteria.Combine(), columns, sort, action);
@@ -710,7 +710,7 @@ namespace InABox.DynamicGrid
             foreach(var filter in filters)
             {
                 var item = new MenuItem { Header = filter.Name, Tag = filter };
-                if(Filter?.Item1 == filter.Name)
+                if(SelectedFilter?.Item1 == filter.Name)
                 {
                     item.IsChecked = true;
                 }
@@ -744,12 +744,12 @@ namespace InABox.DynamicGrid
             Bitmap image;
             if(tag is DynamicGridFilter filter)
             {
-                Filter = new(filter.Name, Serialization.Deserialize<Filter<TEntity>>(filter.Filter));
+                SelectedFilter = new(filter.Name, Serialization.Deserialize<Filter<TEntity>>(filter.Filter));
                 image =Wpf.Resources.filter_set;
             }
             else
             {
-                Filter = null;
+                SelectedFilter = null;
                 image =Wpf.Resources.filter;
             }
             UpdateButton(FilterBtn, image.AsBitmapImage(), "");

+ 14 - 2
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -428,6 +428,8 @@ namespace InABox.DynamicGrid
         protected virtual FontStyle? GetCellFontStyle(CoreRow row, String columnname) => null;
         protected virtual FontWeight? GetCellFontWeight(CoreRow row, String columnname) => null;
         
+
+        
         public DynamicGrid() : base()
         {
             IsReady = false;
@@ -1507,8 +1509,9 @@ namespace InABox.DynamicGrid
                             //headstyle.Setters.Add(new Setter(LayoutTransformProperty, new RotateTransform(270.0F)));
                             headstyle.Setters.Add(new Setter(BorderThicknessProperty, new Thickness(0.0, 0.0, 0, 0)));
                             headstyle.Setters.Add(new Setter(MarginProperty, new Thickness(0, 0, 0.75, 0.75)));
-                            headstyle.Setters.Add(new Setter(TemplateProperty,
-                                Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
+                            if (imgcol.VerticalHeader)
+                                headstyle.Setters.Add(new Setter(TemplateProperty,
+                                    Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
                         }
                         else
                         {
@@ -1570,6 +1573,15 @@ namespace InABox.DynamicGrid
                         headstyle.Setters.Add(new Setter(BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
                         headstyle.Setters.Add(new Setter(ForegroundProperty, new SolidColorBrush(Colors.Black)));
                         headstyle.Setters.Add(new Setter(FontSizeProperty, 12D));
+                        headstyle.Setters.Add(new Setter(MarginProperty, new Thickness(0, -0.75, 0, 0.75)));
+                        headstyle.Setters.Add(new Setter(BorderThicknessProperty, new Thickness(0.75)));
+                        if (txtCol.VerticalHeader)
+                        {
+                            headstyle.Setters.Add(new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Left));
+                            headstyle.Setters.Add(new Setter(TemplateProperty,
+                                Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
+                        }
+
                         newcol.HeaderStyle = headstyle;
 
                         DataGrid.Columns.Add(newcol);

+ 128 - 0
inabox.wpf/Themes/Generic.xaml

@@ -360,6 +360,134 @@
         </Setter>
     </Style>
 
+               <ControlTemplate x:Key="VerticalColumnHeader"
+                     TargetType="{x:Type syncfusion:GridHeaderCellControl}">
+                <Grid>
+                    <Grid.LayoutTransform>
+                        <RotateTransform Angle="270" />
+                    </Grid.LayoutTransform>
+                    <VisualStateManager.VisualStateGroups>
+                        <VisualStateGroup x:Name="HiddenColumnsResizingStates">
+                            <VisualState x:Name="PreviousColumnHidden">
+                                <Storyboard>
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_HeaderCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="3,0,1,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                </Storyboard>
+                            </VisualState>
+                            <VisualState x:Name="HiddenState">
+                                <Storyboard>
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_HeaderCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="3,0,3,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                </Storyboard>
+                            </VisualState>
+                            <VisualState x:Name="NormalState" />
+                            <VisualState x:Name="LastColumnHidden">
+                                <Storyboard>
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_HeaderCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="0,0,3,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                </Storyboard>
+                            </VisualState>
+                        </VisualStateGroup>
+                        <VisualStateGroup x:Name="CommonStates">
+                            <VisualState x:Name="MouseOver" />
+                            <VisualState x:Name="Normal" />
+                        </VisualStateGroup>
+                        <VisualStateGroup x:Name="BorderStates">
+                            <VisualState x:Name="NormalCell" />
+                            <VisualState x:Name="FooterColumnCell">
+                                <Storyboard BeginTime="0">
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_FooterCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="1,0,1,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                </Storyboard>
+                            </VisualState>
+                            <VisualState x:Name="BeforeFooterColumnCell">
+                                <Storyboard BeginTime="0">
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_FooterCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="0,0,0,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                    <ThicknessAnimationUsingKeyFrames BeginTime="0" Duration="1.0:0:0"
+                                                                      Storyboard.TargetProperty="BorderThickness"
+                                                                      Storyboard.TargetName="PART_HeaderCellBorder">
+                                        <EasingThicknessKeyFrame KeyTime="0" Value="0,0,0,1" />
+                                    </ThicknessAnimationUsingKeyFrames>
+                                </Storyboard>
+                            </VisualState>
+                        </VisualStateGroup>
+                    </VisualStateManager.VisualStateGroups>
+                    <Border x:Name="PART_FooterCellBorder" BorderBrush="{TemplateBinding BorderBrush}"
+                            Background="{TemplateBinding Background}" />
+                    <Border x:Name="PART_HeaderCellBorder" BorderBrush="{TemplateBinding BorderBrush}"
+                            BorderThickness="{TemplateBinding BorderThickness}"
+                            Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
+                        <Grid Margin="{TemplateBinding Padding}" SnapsToDevicePixels="True">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="*" />
+                                <ColumnDefinition Width="Auto" />
+                                <ColumnDefinition Width="Auto" />
+                            </Grid.ColumnDefinitions>
+                            <ContentPresenter
+                                ContentTemplate="{TemplateBinding ContentTemplate}"
+                                ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False"
+                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+                                <ContentPresenter.Content>
+                                    <TextBlock Text="{Binding HeaderText}" TextWrapping="Wrap" />
+                                </ContentPresenter.Content>
+                            </ContentPresenter>
+                            <Grid x:Name="PART_SortButtonPresenter" Grid.Column="1" SnapsToDevicePixels="True">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="*">
+                                        <ColumnDefinition.MinWidth>
+                                            <Binding Mode="OneWay" Path="SortDirection"
+                                                     RelativeSource="{RelativeSource TemplatedParent}">
+                                                <Binding.Converter>
+                                                    <syncfusion:SortDirectionToWidthConverter />
+                                                </Binding.Converter>
+                                            </Binding>
+                                        </ColumnDefinition.MinWidth>
+                                    </ColumnDefinition>
+                                    <ColumnDefinition Width="*" />
+                                </Grid.ColumnDefinitions>
+                                <TextBlock Grid.Column="1" Foreground="{TemplateBinding Foreground}"
+                                           FontSize="10" Margin="0,-4,0,0" SnapsToDevicePixels="True"
+                                           Text="{TemplateBinding SortNumber}"
+                                           Visibility="{TemplateBinding SortNumberVisibility}"
+                                           VerticalAlignment="Bottom" />
+                            </Grid>
+                            <syncfusion:FilterToggleButton x:Name="PART_FilterToggleButton" Grid.Column="2"
+                                                           HorizontalAlignment="Stretch"
+                                                           SnapsToDevicePixels="True"
+                                                           Visibility="{TemplateBinding FilterIconVisiblity}"
+                                                           VerticalAlignment="Stretch">
+                                <syncfusion:FilterToggleButton.LayoutTransform>
+                                    <RotateTransform Angle="90" />
+                                </syncfusion:FilterToggleButton.LayoutTransform>
+                            </syncfusion:FilterToggleButton>
+                            <Border x:Name="PART_FilterPopUpPresenter">
+                                <Border.LayoutTransform>
+                                    <RotateTransform Angle="90" />
+                                </Border.LayoutTransform>
+                            </Border>
+                        </Grid>
+                    </Border>
+                </Grid>
+            </ControlTemplate>
+    
+    
     <Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="{x:Type local:DynamicTabItem}">
 
         <Setter Property="Template">