using System.Reflection; using InABox.Clients; using InABox.Core; using InABox.Database; using InABox.IPC; namespace InABox.API { public class RestService { public static bool CheckPasswordExpiration { get; set; } = true; public static bool IsHTTPS { get; set; } = false; protected static void GarbageCollection() { //DateTime now = DateTime.Now; //GC.Collect(); //GC.WaitForPendingFinalizers(); //GC.Collect(); //Logger.Send(LogType.Information, "", String.Format("Garbage Collection takes {0:F2}ms", (DateTime.Now - now).TotalMilliseconds)); } protected static Guid ValidateRequest(Request request, out string? userID) { var session = request.Credentials.Session; if (session != Guid.Empty) { var valid = CredentialsCache.Validate(session, out userID); if (valid != Guid.Empty) { CredentialsCache.RefreshSessionExpiry(session); } return valid; } else if (request.Credentials.UserID is not null && request.Credentials.Password is not null && CredentialsCache.IsBypassed(request.Credentials.UserID, request.Credentials.Password)) { userID = ""; return CoreUtils.FullGuid; } userID = null; return Guid.Empty; } public static MultiQueryResponse QueryMultiple(MultiQueryRequest request, bool isSecure) { var start = DateTime.Now; var response = new MultiQueryResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] QueryMultiple({2})", request.Credentials.Platform, request.Credentials.Version, request.Queries.Count)); try { var queries = new Dictionary(); foreach (var (key, query) in request.Queries) { var type = CoreUtils.GetEntity(query.Type); if (type.IsAssignableTo(typeof(ISecure)) && !isSecure) { Logger.Send(LogType.Error, userid, $"{query.Type} is a secure entity. Request failed"); } else { queries.Add(key, Activator.CreateInstance(typeof(QueryDef<>).MakeGenericType(type), query.Filter, query.Columns, query.Sort) as IQueryDef); } } response.Tables = DbFactory.QueryMultiple(queries, userguid, userid, request.Credentials.Platform, request.Credentials.Version); response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] QueryMultiple Complete", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] QueryMultiple Failed: {3} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, err.Message)); } GarbageCollection(); return response; } public static ValidateResponse Validate(ValidateRequest request) { var response = new ValidateResponse(); User? user = null; bool reLogin = false; if (request.Credentials.Session != Guid.Empty) { var session = request.Credentials.Session; user = CredentialsCache.Validate(session); if (user != null) { Logger.Send(LogType.Information, "", $"{session} re-logged in!"); CredentialsCache.RefreshSessionExpiry(session); reLogin = true; } } if (user == null) { if (request.UsePIN) { Logger.Send(LogType.Information, "", $"Login request with PIN {request.PIN}"); user = CredentialsCache.ValidateUser(request.PIN); } else { var userID = request.UserID ?? request.Credentials.UserID; var password = request.Password ?? request.Credentials.Password; if (userID == null || password == null) return response.Status(StatusCode.Error); user = CredentialsCache.ValidateUser(userID, password); if (user?.ID != CoreUtils.FullGuid) { Logger.Send(LogType.Information, userID, $"Login request for {userID}"); } } } response.Status = StatusCode.OK; if (user == null) { Logger.Send(LogType.Information, "", $"Login failed!"); response.ValidationStatus = ValidationStatus.INVALID; } else if (CheckPasswordExpiration && !request.UsePIN && user.PasswordExpiration > DateTime.MinValue && user.PasswordExpiration < DateTime.Now) { Logger.Send(LogType.Information, user.UserID, $"Password for ({user.UserID}) has expired!"); response.ValidationStatus = ValidationStatus.PASSWORD_EXPIRED; } else if (reLogin) { Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!"); response.ValidationStatus = ValidationStatus.VALID; response.UserGuid = user.ID; response.UserID = user.UserID; response.SecurityID = user.SecurityGroup.ID; response.Session = request.Credentials.Session; response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue; } else if (user.ID == CoreUtils.FullGuid || !user.Use2FA) { Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) success!"); response.ValidationStatus = ValidationStatus.VALID; response.UserGuid = user.ID; response.UserID = user.UserID; response.SecurityID = user.SecurityGroup.ID; response.Session = user.ID == CoreUtils.FullGuid ? CredentialsCache.NewSession(user, true, DateTime.MaxValue) : CredentialsCache.NewSession(user, true); response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue; } else { Logger.Send(LogType.Information, user.UserID, $"Login ({user.UserID}) requires 2FA. Sending code..."); var session = CredentialsCache.SendCode(user.ID, out var recipient); if (session != null) { response.ValidationStatus = ValidationStatus.REQUIRE_2FA; response.UserGuid = user.ID; response.UserID = user.UserID; response.SecurityID = user.SecurityGroup.ID; response.Session = session ?? Guid.Empty; response.Recipient2FA = recipient; response.PasswordExpiration = CheckPasswordExpiration ? user.PasswordExpiration : DateTime.MinValue; } else { response.Status = StatusCode.Error; response.Messages.Add("Code failed to send"); } } return response; } public static Check2FAResponse Check2FA(Check2FARequest request) { var response = new Check2FAResponse(); Logger.Send(LogType.Information, "", $"2FA check for session {request.Credentials.Session} and code {request.Code}"); response.Valid = CredentialsCache.ValidateCode(request.Credentials.Session, request.Code); response.Status = StatusCode.OK; return response; } public static InfoResponse Info(InfoRequest request) { var response = new InfoResponse(new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), IsHTTPS, DbFactory.RestPort, DbFactory.RPCPort)); response.Status = StatusCode.OK; return response; } } public class RestService : RestService where TEntity : Entity, new() { private static string SimpleName(Type t) { return t.Name.Split('.').Last(); } public static QueryResponse List(QueryRequest request) { var start = DateTime.Now; var response = new QueryResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] Query{2}: Filter=[{3}] Sort=[{4}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Filter != null ? request.Filter.AsOData() : "", request.Sort != null ? request.Sort.AsOData() : "")); try { var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); response.Items = store.Query(request.Filter, request.Columns, request.Sort); response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] Query{3} Complete: {4} records / {5} columns returned", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Rows.Count, response.Items.Columns.Count)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] Query{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message)); } GarbageCollection(); return response; } /*public static LoadResponse Load(LoadRequest request) { var start = DateTime.Now; var response = new LoadResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] Load{2}: Filter=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Filter != null ? request.Filter.AsOData() : "")); try { var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); response.Items = store.Load(request.Filter, request.Sort); response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] Load{3} Complete: {4} records returned", request.Credentials.Platform, request.Credentials.Version, (uint)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Count())); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] Load{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message)); } GarbageCollection(); return response; }*/ public static SaveResponse Save(SaveRequest request) { var start = DateTime.Now; var response = new SaveResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] Save{2}: Data=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Item != null ? request.Item.ToString() : request + " (null)")); try { var e = request.Item; var preSaveChanges = BaseObjectExtensions.GetOriginaValues(e, false); var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); store.Save(e, request.AuditNote); if (request.ReturnOnlyChanged) { var currentChanges = BaseObjectExtensions.GetOriginaValues(e, false); foreach (var (key, value) in currentChanges) { if (preSaveChanges.TryGetValue(key, out var oldValue)) { if (!Equals(value, oldValue)) { response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key)); } } else { response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key)); } } } else { response.Item = e; } response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Item)); CredentialsCache.Refresh(typeof(TEntity) == typeof(User)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] Save{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace)); } GarbageCollection(); return response; } public static MultiSaveResponse MultiSave(MultiSaveRequest request) { var start = DateTime.Now; var response = new MultiSaveResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] MultiSave{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Items.Length, request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)")); try { var originals = new Dictionary>(); var es = request.Items; foreach (var e in es) originals[e] = BaseObjectExtensions.GetOriginaValues(e, false); var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); store.Save(es, request.AuditNote); if (request.ReturnOnlyChanged) { for (int i = 0; i < es.Length; ++i) { var result = new Dictionary(); var e = es[i]; var currentChanges = BaseObjectExtensions.GetOriginaValues(e, false); var preSaveChanges = originals[e]; foreach (var (key, value) in currentChanges) { if (preSaveChanges.TryGetValue(key, out var oldValue)) { if (!Equals(value, oldValue)) { result[key] = CoreUtils.GetPropertyValue(e, key); } } else { result[key] = CoreUtils.GetPropertyValue(e, key); } } response.ChangedValues.Add(result); } } else { response.Items = es; } response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), es.Length)); CredentialsCache.Refresh(typeof(TEntity) == typeof(User)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace)); } GarbageCollection(); return response; } public static DeleteResponse Delete(DeleteRequest request) { var start = DateTime.Now; var response = new DeleteResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] Delete{2} Data=[{3}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Item)); try { var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); store.Delete(request.Item, request.AuditNote); response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] Delete{3} Complete", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)))); CredentialsCache.Refresh(typeof(TEntity) == typeof(User)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] Delete{3} Failed: {4} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message)); } GarbageCollection(); return response; } public static MultiDeleteResponse MultiDelete(MultiDeleteRequest request) { var start = DateTime.Now; var response = new MultiDeleteResponse(); var userguid = ValidateRequest(request, out var userid); if (userguid == Guid.Empty) return response.Status(StatusCode.Unauthenticated); Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] MultiDelete{2}({3}) Data=[{4}]", request.Credentials.Platform, request.Credentials.Version, SimpleName(typeof(TEntity)), request.Items.Length, request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)")); try { var es = request.Items; var store = DbFactory.FindStore(userguid, userid, request.Credentials.Platform, request.Credentials.Version); store.Delete(es, request.AuditNote); response.Status = StatusCode.OK; Logger.Send(LogType.Information, userid, string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), request.Items.Length)); CredentialsCache.Refresh(typeof(TEntity) == typeof(User)); } catch (Exception err) { response.Status = StatusCode.Error; response.Messages.Add(err.Message); Logger.Send(LogType.Error, userid, string.Format("[{0} {1}] [{2:D8}] MultiDelete{3} Failed: {4}\n\n{5} ", request.Credentials.Platform, request.Credentials.Version, (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), err.Message, err.StackTrace)); } GarbageCollection(); return response; } } }