using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Timers; using Comal.Classes; using Comal.Stores; using InABox.API; using InABox.Configuration; using InABox.Core; using InABox.Database; using InABox.Database.SQLite; using InABox.IPC; using InABox.Mail; using InABox.Rpc; using InABox.Server; using InABox.Wpf.Reports; using PRS.Shared; using PRS.Shared.Events; using PRSServices; using Timer = System.Timers.Timer; namespace PRSServer; public class DatabaseEngine : Engine { private Timer? _certificateRefreshTimer; private Timer? _certificateHaltTimer; private Timer? _scheduleTimer; private string _ipcPipeName = ""; private IPCServer? _ipcServer; private string _rpcPipeName = ""; private IRpcServer? _pipeserver; private IRpcServer? _socketserver; public override void Configure(Server server) { base.Configure(server); Logger.Send(LogType.Information, "", "Configuring..."); _ipcPipeName = DatabaseServerProperties.GetPipeName(server.Key, false); _rpcPipeName = DatabaseServerProperties.GetPipeName(server.Key, true); MoveUpdateFiles(); } public override PortStatus[] PortStatusList() { var result = new List(); if (Properties.Port != 0) { result.Add(RestListener.Certificate != null ? new PortStatus(Properties.Port, PortType.Database, PortState.Secure) : new PortStatus(Properties.Port, PortType.Database, PortState.Available)); } if (Properties.RPCPort != 0) result.Add(new PortStatus(Properties.RPCPort, PortType.Session, PortState.Available)); return result.ToArray(); } private void ConfigureSmsProviders() { if (Properties.SMSProviderProperties == null) return; if (Properties.SMSProviderProperties.Count == 0) { Logger.Send(LogType.Information, "", "No SMS Providers to initialise"); } foreach (var (type, properties) in Properties.SMSProviderProperties) { var provider = SMSProviderProperties.ToProperties(type, properties); switch (provider) { case ExchangeProviderProperties exchange: Logger.Send(LogType.Information, "", "Initializing Exchange Mailer"); CredentialsCache.AddSMSProvider(new ExchangeProvider( exchange.Host, exchange.Port, exchange.EmailAddress, exchange.Password )); break; case IMAPProviderProperties imap: Logger.Send(LogType.Information, "", "Initializing IMAP Mailer"); CredentialsCache.AddSMSProvider(new IMAPProvider( imap.Host, imap.Port, imap.EmailAddress, imap.Password )); break; case ASPSMSProviderProperties asp: Logger.Send(LogType.Information, "", "Initializing ASPSMS"); CredentialsCache.AddSMSProvider(new ASPSMSProvider( asp.Userkey, asp.APIPassword )); break; case TwilioProviderProperties tw: Logger.Send(LogType.Information, "", "Initializing Twilio"); CredentialsCache.AddSMSProvider(new TwilioSMSProvider( tw.AccountSID, tw.AuthToken, tw.Number )); break; } } } private IEnumerable PollNotifications(Guid session) { var user = CredentialsCache.Validate(session); if (user == null) return Array.Empty(); var store = DbFactory.FindStore(user.ID, user.UserID, Platform.DatabaseEngine, "", Logger.New()); return store.Query( new Filter(x => x.Employee.UserLink.ID).IsEqualTo(user.ID) .And(x => x.Closed).IsEqualTo(DateTime.MinValue), Columns.None().Add( x => x.ID, x => x.Title, //x => x.Description, x => x.Created, x => x.Sender.ID, x => x.Sender.Name, x => x.Job.ID, x => x.Job.Deleted, x => x.Job.JobNumber, //x => x.Kanban.ID, //x => x.Setout.ID, //x => x.Requisition.ID, //x => x.Delivery.ID, x => x.Employee.ID, x => x.EntityType, x => x.EntityID, x => x.Closed )).Rows.Select(x => x.ToObject()); } private void ConfigureMailer() { if (!Properties.EmailProperties.IsNullOrWhiteSpace()) { switch (Properties.GetEmailProperties()) { case ServerEmailIMAPProperties imap: DbFactory.Mailer = new IMAPMailer { SMTPHost = imap.Host, SMTPDomain = imap.Domain, SMTPUserName = imap.UserName, SMTPPassword = imap.Password, SMTPPort = imap.Port }; break; case ServerEmailExchangeProperties exchange: DbFactory.Mailer = new ExchangeMailer { MailboxHost = exchange.Host, MailboxDomain = exchange.Domain, MailboxUserName = exchange.UserName, MailboxPassword = exchange.Password, MailboxPort = exchange.Port }; break; } } } #region Run/Stop Functionality public override void Run() { Logger.Send(LogType.Information, "", "Starting.."); if (string.IsNullOrEmpty(Properties.FileName)) throw new Exception("Error: Filename not Specified\n"); Logger.Send(LogType.Information, "", "Registering Classes: " + Properties.FileName); StoreUtils.RegisterClasses(); CoreUtils.RegisterClasses(); ComalUtils.RegisterClasses(); PRSSharedUtils.RegisterClasses(); ReportUtils.RegisterClasses(); ConfigurationUtils.RegisterClasses(); DatabaseUpdateScripts.RegisterScripts(); Logger.Send(LogType.Information, "", "Starting Database: " + Properties.FileName); DbFactory.Stores = CoreUtils.TypeList( AppDomain.CurrentDomain.GetAssemblies(), myType => myType is { IsClass: true, IsAbstract: false, IsGenericType: false } && myType.GetInterfaces().Contains(typeof(IStore)) ).ToArray(); DbFactory.DefaultStore = typeof(BaseStore<>); DbFactory.ProviderFactory = new SQLiteProviderFactory(Properties.FileName); DbFactory.ColorScheme = Properties.ColorScheme; DbFactory.Logo = Properties.Logo; // See notes on Request.DatabaseInfo Class // Once RPC listeners are stable, this should be removed. DbFactory.RestPort = Properties.Port; DbFactory.RPCPort = Properties.RPCPort; DbFactory.Start(); UserStore.PasswordExpirationTime = TimeSpan.FromDays(Properties.PasswordExpiryTime); RestService.CheckPasswordExpiration = Properties.PasswordExpiryTime > 0; if (DbFactory.IsReadOnly) { Logger.Send(LogType.Error,"","Unable to create ADMIN user at this time."); } else { var users = DbFactory.NewProvider(Logger.Main).Load(); if (!users.Any()) { var user = new User { UserID = "ADMIN", Password = "admin" }; DbFactory.NewProvider(Logger.Main).Save(user); var employee = DbFactory.NewProvider(Logger.Main).Load(new Filter(x => x.Code).IsEqualTo("ADMIN")) .FirstOrDefault() ?? new Employee { Code = "ADMIN", Name = "Administrator Account" }; employee.UserLink.ID = user.ID; DbFactory.NewProvider(Logger.Main).Save(employee); } } CoreUtils.GoogleAPIKey = Properties.GoogleAPIKey; PurchaseOrder.PONumberPrefix = Properties.PurchaseOrderPrefix; Job.JobNumberPrefix = Properties.JobPrefix; ConfigureMailer(); ConfigureSmsProviders(); CredentialsCache.SetCacheFile(Path.Combine(AppDataFolder, "session_cache.json")); CredentialsCache.LoadSessionCache(); CredentialsCache.SetSessionExpiryTime(TimeSpan.FromMinutes(Properties.SessionExpiryTime)); Start(); } private void Start() { var certificate = LoadCertificate(CertificateFileName()); if (certificate != null) { // Once every day, check certificate expiry if (_certificateRefreshTimer == null) { _certificateRefreshTimer = new Timer(1000 * 60 * 60 * 24); _certificateRefreshTimer.Elapsed += CertificateTimer_Elapsed; _certificateRefreshTimer.AutoReset = true; } _certificateRefreshTimer.Start(); } // Older Style Rest-Listener if (Properties.Port != 0) { RestListener.Init((ushort)Properties.Port, certificate); RestListener.Start(); Logger.Send(LogType.Information, "", $"- Rest Listener Started: Port={Properties.Port}"); } // New Style Socket Listener if (Properties.RPCPort != 0) { var sockettransport = new RpcServerSocketTransport(Properties.RPCPort); //, certificate); _socketserver = new RpcServer(sockettransport); _socketserver.OnLog += (type, userid, message, parameters) => Logger.Send(type, userid, $"[S] {message}", parameters); _socketserver.Start(); PushManager.AddPusher(sockettransport); Logger.Send(LogType.Information, "", $"- RPC Listener Started: Port={Properties.RPCPort}"); } // Older-Style Pipe (IPC Server) _ipcServer = new IPCServer(_ipcPipeName); _ipcServer.Start(); Logger.Send(LogType.Information, "", $"- IPC Pipe Listener started: Name=[{_ipcPipeName}]"); // New Style Pipe (RPC) Listener var pipetransport = new RpcServerPipeTransport(_rpcPipeName); PushManager.AddPusher(pipetransport); _pipeserver = new RpcServer(pipetransport); _pipeserver.OnLog += (type, userid, message, parameters) => Logger.Send(type, userid, $"[P] {message}", parameters); _pipeserver.Start(); Logger.Send(LogType.Information, "", $"- RPC Pipe Listener started: Name=[{_rpcPipeName}]"); PushManager.AddPollHandler(PollNotifications); Logger.Send(LogType.Information, "", $"- Push Notifications Configured"); if(_scheduleTimer is null) { _scheduleTimer = new Timer(TimeSpan.FromMinutes(1)); _scheduleTimer.Elapsed += _scheduleTimer_Elapsed; _scheduleTimer.AutoReset = true; } _scheduleTimer.Start(); Logger.Send(LogType.Information, "", "Schedule timer started"); } private void _scheduleTimer_Elapsed(object? sender, ElapsedEventArgs e) { EventUtils.CheckScheduledEvents(); } public override void Stop() { Logger.Send(LogType.Information, "", "Stopping.."); _socketserver?.Stop(); _socketserver = null; _pipeserver?.Stop(); _pipeserver = null; _scheduleTimer?.Stop(); _scheduleTimer = null; _ipcServer?.Dispose(); RestListener.Stop(); CredentialsCache.SaveSessionCache(); _certificateRefreshTimer?.Stop(); _certificateRefreshTimer = null; _certificateHaltTimer?.Stop(); _certificateHaltTimer = null; } #endregion #region Certificate Management private string CertificateFileName() => Properties.CertificateFile.NotWhiteSpaceOr(CertificateEngine.CertificateFile); private void SendCertificateExpiryNotification(DateTime expiry) { var message = expiry.Date == DateTime.Now.Date ? $"HTTPS Certificate for Database Engine will expire today at {expiry.TimeOfDay:hh\\:mm}" : $"HTTPS Certificate for Database Engine will expire in {(expiry - DateTime.Now).Days} at {expiry:dd/MM/yyyy hh:mm}"; Logger.Send(LogType.Information, "DATABASE", message); if (!string.IsNullOrWhiteSpace(Properties.CertificateExpirationSubscriber)) { var employee = DbFactory.NewProvider(Logger.Main).Query( new Filter(x => x.UserLink.UserID).IsEqualTo(Properties.CertificateExpirationSubscriber), Columns.None().Add(x => x.ID, x => x.UserLink.ID, x => x.UserLink.UserID)).Rows.FirstOrDefault()?.ToObject(); if (employee != null) { var notification = new Notification(); notification.Employee.ID = employee.ID; notification.Title = "HTTPS Certificate expires soon"; notification.Description = message; DbFactory.FindStore(employee.UserLink.ID, employee.UserLink.UserID, Platform.DatabaseEngine, "", Logger.New()) .Save(notification, ""); } else { Logger.Send(LogType.Information, "DATABASE", $"Certificate expiration subscriber {Properties.CertificateExpirationSubscriber} employee doesn't exist"); } } } private void CertificateTimer_Elapsed(object? sender, ElapsedEventArgs e) { if (RestListener.Certificate != null) { X509Certificate2? cert = null; if (File.Exists(CertificateFileName())) { cert = new X509Certificate2(CertificateFileName()); } if (cert != null && cert.NotAfter > RestListener.Certificate.NotAfter && cert.NotAfter > DateTime.Now) { Logger.Send(LogType.Information, "DATABASE", "HTTPS Certificate with greater expiry date found; restarting HTTPS listener..."); Stop(); Start(); } var expiry = RestListener.Certificate.NotAfter; var untilExpiry = expiry - DateTime.Now; if (untilExpiry.TotalDays <= 7) { SendCertificateExpiryNotification(expiry); if (untilExpiry.TotalDays <= 1) { _certificateRefreshTimer?.Stop(); _certificateHaltTimer = new Timer(untilExpiry.TotalMilliseconds); _certificateHaltTimer.Elapsed += HTTPS_Halt_Elapsed; _certificateHaltTimer.AutoReset = false; _certificateHaltTimer.Start(); } } } } /// /// Restarts listener in HTTP mode /// /// /// private void HTTPS_Halt_Elapsed(object? sender, ElapsedEventArgs e) { _certificateHaltTimer?.Dispose(); _certificateHaltTimer = null; Logger.Send(LogType.Information, "", "Expiry of certificate reached; restarting HTTPS listener..."); Stop(); Start(); } #endregion #region Desktop Installer Files private static bool CheckNewer(string filename) { var source = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update", filename); var target = Path.Combine(CoreUtils.GetCommonAppData("PRSServer"), "update", filename); if (!File.Exists(target)) return true; if (!File.Exists(source)) return false; return File.GetLastWriteTimeUtc(source) > File.GetLastWriteTimeUtc(target); } private static void CopyFile(string filename) { var source = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update", filename); var targetdir = Path.Combine(CoreUtils.GetCommonAppData("PRSServer"), "update"); if (!Directory.Exists(targetdir)) Directory.CreateDirectory(targetdir); var target = Path.Combine(targetdir, filename); File.Copy(source, target, true); } public static void MoveUpdateFiles() { try { if (CheckNewer("version.txt") || CheckNewer("Release Notes.txt") || CheckNewer("PRSDesktopSetup.exe")) { CopyFile("version.txt"); CopyFile("Release Notes.txt"); CopyFile("PRSDesktopSetup.exe"); } } catch (Exception e) { Logger.Send(LogType.Error, "", $"Could not copy desktop update files: {e.Message}"); } } #endregion }