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.Rpc; using InABox.Server; using InABox.Wpf.Reports; using PRS.Shared; using Timer = System.Timers.Timer; namespace PRSServer { public class DatabaseEngine : Engine { private Timer? CertificateRefreshTimer; private Timer? CertificateHaltTimer; private string PipeName; //private IPCServer? PipeServer; private IRpcServer? _pipeserver; private IRpcServer? _socketserver; public override void Configure(Server server) { base.Configure(server); Logger.Send(LogType.Information, "", "Configuring..."); PipeName = DatabaseServerProperties.GetPipeName(server.Key); MoveUpdateFiles(); } public override PortStatus[] PortStatusList() { var result = new List(); if (Properties.Port != 0) { if (RestListener.Certificate != null) result.Add(new PortStatus(Properties.Port, PortType.Database, PortState.Secure)); else result.Add(new PortStatus(Properties.Port, PortType.Database, PortState.Available)); } if (Properties.WebSocketPort != 0) result.Add(new PortStatus(Properties.WebSocketPort, 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, "", string.Format("Initializing Exchange Mailer", Properties.Port)); CredentialsCache.AddSMSProvider(new ExchangeProvider( exchange.Host, exchange.Port, exchange.EmailAddress, exchange.Password )); break; case IMAPProviderProperties imap: Logger.Send(LogType.Information, "", string.Format("Initializing IMAP Mailer", Properties.Port)); CredentialsCache.AddSMSProvider(new IMAPProvider( imap.Host, imap.Port, imap.EmailAddress, imap.Password )); break; case ASPSMSProviderProperties asp: Logger.Send(LogType.Information, "", string.Format("Initializing ASPSMS", Properties.Port)); CredentialsCache.AddSMSProvider(new ASPSMSProvider( asp.Userkey, asp.APIPassword )); break; case TwilioProviderProperties tw: Logger.Send(LogType.Information, "", string.Format("Initializing Twilio", Properties.Port)); 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, ""); return store.Query( new Filter(x => x.Employee.UserLink.ID).IsEqualTo(user.ID) .And(x => x.Closed).IsEqualTo(DateTime.MinValue), new Columns( 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 ConfigurePusher() { PushManager.AddPollHandler(PollNotifications); } public override void Run() { Logger.Send(LogType.Information, "", "Starting.."); if (Properties.Port.Equals(0)) throw new Exception("Error: Port not Specified\n"); 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(); ReportUtils.RegisterClasses(); ConfigurationUtils.RegisterClasses(); DatabaseUpdateScripts.RegisterScripts(); Logger.Send(LogType.Information, "", "Starting Database: " + Properties.FileName); DbFactory.Stores = CoreUtils.TypeList( AppDomain.CurrentDomain.GetAssemblies(), myType => myType.IsClass && !myType.IsAbstract && !myType.IsGenericType && myType.GetInterfaces().Contains(typeof(IStore)) ).ToArray(); DbFactory.Provider = new SQLiteProvider(Properties.FileName); DbFactory.ColorScheme = Properties.ColorScheme; DbFactory.Logo = Properties.Logo; DbFactory.Start(); UserStore.PasswordExpirationTime = TimeSpan.FromDays(Properties.PasswordExpiryTime); RestService.CheckPasswordExpiration = Properties.PasswordExpiryTime > 0; var users = DbFactory.Provider.Load(); if (!users.Any()) { var user = new User { UserID = "ADMIN", Password = "admin" }; DbFactory.Provider.Save(user); var employee = DbFactory.Provider.Load(new Filter(x => x.Code).IsEqualTo("ADMIN")).FirstOrDefault(); if (employee == null) employee = new Employee { Code = "ADMIN", Name = "Administrator Account" }; employee.UserLink.ID = user.ID; DbFactory.Provider.Save(employee); } StoreUtils.GoogleAPIKey = Properties.GoogleAPIKey; PurchaseOrderStore.AutoIncrementPrefix = Properties.PurchaseOrderPrefix; JobStore.AutoIncrementPrefix = Properties.JobPrefix; ConfigureSMSProviders(); CredentialsCache.SetCacheFile(Path.Combine(AppDataFolder, "session_cache.json")); CredentialsCache.LoadSessionCache(); CredentialsCache.SetSessionExpiryTime(TimeSpan.FromMinutes(Properties.SessionExpiryTime)); // if (Properties.WebSocketPort != 0) // { // // Put a log saying to start the web socket listener. // Logger.Send(LogType.Information, "", string.Format("Starting Web Socket Listener: Port={0}", Properties.WebSocketPort)); // } // // This should be out of the if-statement, since the listener needs to be initialised, even if the web socket port is 0. // RestListener.Init(Properties.WebSocketPort); // InitialisePort(); // RestListener.Start(); Logger.Send(LogType.Information, "", string.Format("Starting Web Listener: Port={0}", Properties.Port)); var sockettransport = new RpcServerSocketTransport(Properties.Port, CertificateFileName()); _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, "", string.Format("Server started listening on port {0}", Properties.Port)); // PipeServer = new IPCServer(PipeName); // PipeServer.Start(); Logger.Send(LogType.Information, "", $"Starting Pipe Listener with pipe name {PipeName}"); var pipetransport = new RpcServerPipeTransport(PipeName); _pipeserver = new RpcServer(pipetransport); _pipeserver.OnLog += (type, userid, message, parameters) => Logger.Send(type, userid, $"[P] {message}", parameters); _pipeserver.Start(); PushManager.AddPusher(pipetransport); Logger.Send(LogType.Information, "", "Pipe Listener started"); ConfigurePusher(); } #region Certificate Management private string CertificateFileName() => !string.IsNullOrWhiteSpace(Properties.CertificateFile) ? Properties.CertificateFile : CertificateEngine.CertificateFile; private void InitialisePort() { var useHTTP = true; if (File.Exists(CertificateFileName())) { Logger.Send(LogType.Information, "", "Certificate found; verifying HTTPS Certificate"); try { var certificate = new X509Certificate2(CertificateFileName()); if (certificate.NotAfter > DateTime.Now) { RestListener.InitCertificate((ushort)Properties.Port, certificate); var names = CertificateEngine.GetDnsNames(certificate); Logger.Send(LogType.Information, "", $"Certificate valid for {string.Join(',', names)}"); useHTTP = false; } else { Logger.Send(LogType.Error, "", "HTTPS Certificate has expired, using HTTP instead"); } } catch (Exception) { Logger.Send(LogType.Error, "", "Error validating HTTPS Certificate, using HTTP instead"); } } if (useHTTP) { RestListener.InitPort((ushort)Properties.Port); } else { // 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(); } } private void SendCertificateExpiryNotification(DateTime expiry) { string message; if (expiry.Date == DateTime.Now.Date) { message = $"HTTPS Certificate for Database Engine will expire today at {expiry.TimeOfDay:hh\\:mm}"; } else { message = $"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.Provider.Query( new Filter(x => x.UserLink.UserID).IsEqualTo(Properties.CertificateExpirationSubscriber), new Columns(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, "").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..."); RestartListener(); } 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..."); RestartListener(); } #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 target = Path.Combine(CoreUtils.GetCommonAppData("PRSServer"), "update", 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 private void RestartListener() { RestListener.Clear(); RestListener.Init(Properties.WebSocketPort); InitialisePort(); RestListener.Start(); } public override void Stop() { Logger.Send(LogType.Information, "", "Stopping.."); _socketserver?.Stop(); _socketserver = null; _pipeserver?.Stop(); _pipeserver = null; //PipeServer?.Dispose(); RestListener.Stop(); CredentialsCache.SaveSessionCache(); CertificateRefreshTimer?.Stop(); CertificateHaltTimer?.Stop(); } } }