using System; using System.IO; using System.Security; using System.Threading; using System.Windows.Forms; using mRemoteNG.App; using mRemoteNG.App.Info; using mRemoteNG.Config.Connections; using mRemoteNG.Config.Connections.Multiuser; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Config.Putty; using mRemoteNG.Connection.Protocol; using mRemoteNG.Messages; using mRemoteNG.Security; using mRemoteNG.Tools; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; using mRemoteNG.UI; using mRemoteNG.UI.TaskDialog; namespace mRemoteNG.Connection { public class ConnectionsService : IConnectionsService { private static readonly object SaveLock = new object(); private bool _showDialogWhenLoadingConnections; private readonly PuttySessionsManager _puttySessionsManager; private readonly Import _import; private readonly IWin32Window _dialogWindowParent; private bool _batchingSaves = false; private bool _saveRequested = false; private bool _saveAsyncRequested = false; public bool IsConnectionsFileLoaded { get; set; } public bool UsingDatabase { get; private set; } public string ConnectionFileName { get; private set; } public RemoteConnectionsSyncronizer RemoteConnectionsSyncronizer { get; set; } public DateTime LastSqlUpdate { get; set; } public SecureString EncryptionKey { get; set; } = new RootNodeInfo(RootNodeType.Connection).PasswordString.ConvertToSecureString(); // TODO - this is only a property to break up a circular dependency. move to ctor when able public DatabaseConnectorFactory DatabaseConnectorFactory { get; set; } public ConnectionTreeModel ConnectionTreeModel { get; private set; } public ConnectionsService(PuttySessionsManager puttySessionsManager, Import import, IWin32Window dialogWindowParent) { _puttySessionsManager = puttySessionsManager.ThrowIfNull(nameof(puttySessionsManager)); _import = import.ThrowIfNull(nameof(import)); _dialogWindowParent = dialogWindowParent.ThrowIfNull(nameof(dialogWindowParent)); } public void NewConnectionsFile(string filename) { try { filename.ThrowIfNullOrEmpty(nameof(filename)); var newConnectionsModel = new ConnectionTreeModel(); newConnectionsModel.AddRootNode(new RootNodeInfo(RootNodeType.Connection)); SaveConnections(newConnectionsModel, false, new SaveFilter(), filename, true); LoadConnections(false, false, filename); } catch (Exception ex) { Runtime.MessageCollector.AddExceptionMessage(Language.strCouldNotCreateNewConnectionsFile, ex); } } public ConnectionInfo CreateQuickConnect(string connectionString, ProtocolType protocol) { try { var uri = new Uri("dummyscheme" + Uri.SchemeDelimiter + connectionString); if (string.IsNullOrEmpty(uri.Host)) return null; var newConnectionInfo = new ConnectionInfo(); newConnectionInfo.CopyFrom(DefaultConnectionInfo.Instance); newConnectionInfo.Name = Settings.Default.IdentifyQuickConnectTabs ? string.Format(Language.strQuick, uri.Host) : uri.Host; newConnectionInfo.Protocol = protocol; newConnectionInfo.Hostname = uri.Host; if (uri.Port == -1) { newConnectionInfo.SetDefaultPort(); } else { newConnectionInfo.Port = uri.Port; } if (string.IsNullOrEmpty(newConnectionInfo.Panel)) newConnectionInfo.Panel = Language.strGeneral; newConnectionInfo.IsQuickConnect = true; return newConnectionInfo; } catch (Exception ex) { Runtime.MessageCollector.AddExceptionMessage(Language.strQuickConnectFailed, ex); return null; } } /// /// Load connections from a source. is ignored if /// is true. /// /// /// /// public void LoadConnections(bool useDatabase, bool import, string connectionFileName) { var oldConnectionTreeModel = ConnectionTreeModel; var oldIsUsingDatabaseValue = UsingDatabase; var newConnectionTreeModel = useDatabase ? new SqlConnectionsLoader(DatabaseConnectorFactory, this).Load() : new XmlConnectionsLoader(connectionFileName, this, _dialogWindowParent).Load(); if (newConnectionTreeModel == null) { DialogFactory.ShowLoadConnectionsFailedDialog(connectionFileName, "Decrypting connection file failed", IsConnectionsFileLoaded, this); return; } IsConnectionsFileLoaded = true; ConnectionFileName = connectionFileName; UsingDatabase = useDatabase; if (!import) { _puttySessionsManager.AddSessions(); newConnectionTreeModel.RootNodes.AddRange(_puttySessionsManager.RootPuttySessionsNodes); } ConnectionTreeModel = newConnectionTreeModel; UpdateCustomConsPathSetting(connectionFileName); RaiseConnectionsLoadedEvent(oldConnectionTreeModel, newConnectionTreeModel, oldIsUsingDatabaseValue, useDatabase, connectionFileName); } /// /// When turned on, calls to or /// will not immediately execute. /// Instead, they will be deferred until /// is called. /// public void BeginBatchingSaves() { _batchingSaves = true; } /// /// Immediately executes a single or /// if one has been requested /// since calling . /// public void EndBatchingSaves() { _batchingSaves = false; if (_saveAsyncRequested) SaveConnectionsAsync(); else if(_saveRequested) SaveConnections(); } /// /// Saves the currently loaded with /// no . /// public void SaveConnections() { SaveConnections(ConnectionTreeModel, UsingDatabase, new SaveFilter(), ConnectionFileName); } /// /// Saves the given . /// If is true, is ignored /// /// /// /// /// /// Bypasses safety checks that prevent saving if a connection file isn't loaded. public void SaveConnections(ConnectionTreeModel connectionTreeModel, bool useDatabase, SaveFilter saveFilter, string connectionFileName, bool forceSave = false) { if (connectionTreeModel == null) return; if (!forceSave && !IsConnectionsFileLoaded) return; if (_batchingSaves) { _saveRequested = true; return; } try { Runtime.MessageCollector.AddMessage(MessageClass.InformationMsg, "Saving connections..."); RemoteConnectionsSyncronizer?.Disable(); var previouslyUsingDatabase = UsingDatabase; if (useDatabase) new SqlConnectionsSaver(saveFilter, this, DatabaseConnectorFactory).Save(connectionTreeModel); else new XmlConnectionsSaver(connectionFileName, saveFilter).Save(connectionTreeModel); if (UsingDatabase) LastSqlUpdate = DateTime.Now; UsingDatabase = useDatabase; ConnectionFileName = connectionFileName; RaiseConnectionsSavedEvent(connectionTreeModel, previouslyUsingDatabase, UsingDatabase, connectionFileName); Runtime.MessageCollector.AddMessage(MessageClass.InformationMsg, "Successfully saved connections"); } catch (Exception ex) { Runtime.MessageCollector?.AddExceptionMessage(string.Format(Language.strConnectionsFileCouldNotSaveAs, connectionFileName), ex, logOnly:false); } finally { RemoteConnectionsSyncronizer?.Enable(); } } public void SaveConnectionsAsync() { if (_batchingSaves) { _saveAsyncRequested = true; return; } var t = new Thread(SaveConnectionsBGd); t.SetApartmentState(ApartmentState.STA); t.Start(); } private void SaveConnectionsBGd() { lock (SaveLock) { SaveConnections(); } } public void LoadConnectionsAsync() { _showDialogWhenLoadingConnections = false; var t = new Thread(LoadConnectionsBGd); t.SetApartmentState(ApartmentState.STA); t.Start(); } private void LoadConnectionsBGd() { LoadConnections(_showDialogWhenLoadingConnections); } public void LoadConnections(bool withDialog = false) { var connectionFileName = ""; try { // disable sql update checking while we are loading updates RemoteConnectionsSyncronizer?.Disable(); if (!Settings.Default.UseSQLServer) { if (withDialog) { var loadDialog = DialogFactory.BuildLoadConnectionsDialog(); if (loadDialog.ShowDialog() != DialogResult.OK) return; connectionFileName = loadDialog.FileName; } else { connectionFileName = GetStartupConnectionFileName(); } } LoadConnections(Settings.Default.UseSQLServer, false, connectionFileName); if (Settings.Default.UseSQLServer) { LastSqlUpdate = DateTime.Now; } else { if (connectionFileName == GetDefaultStartupConnectionFileName()) { Settings.Default.LoadConsFromCustomLocation = false; } else { Settings.Default.LoadConsFromCustomLocation = true; Settings.Default.CustomConsPath = connectionFileName; } } // re-enable sql update checking after updates are loaded RemoteConnectionsSyncronizer?.Enable(); } catch (Exception ex) { if (Settings.Default.UseSQLServer) { Runtime.MessageCollector.AddExceptionMessage(Language.strLoadFromSqlFailed, ex); var commandButtons = string.Join("|", Language.strCommandTryAgain, Language.strCommandOpenConnectionFile, string.Format(Language.strCommandExitProgram, Application.ProductName)); CTaskDialog.ShowCommandBox(Application.ProductName, Language.strLoadFromSqlFailed, Language.strLoadFromSqlFailedContent, MiscTools.GetExceptionMessageRecursive(ex), "", "", commandButtons, false, ESysIcons.Error, ESysIcons.Error); switch (CTaskDialog.CommandButtonResult) { case 0: LoadConnections(withDialog); return; case 1: Settings.Default.UseSQLServer = false; LoadConnections(true); return; default: Application.Exit(); return; } } if (ex is FileNotFoundException && !withDialog) { Runtime.MessageCollector.AddExceptionMessage(string.Format(Language.strConnectionsFileCouldNotBeLoadedNew, connectionFileName), ex, MessageClass.InformationMsg); string[] commandButtons = { Language.ConfigurationCreateNew, Language.ConfigurationCustomPath, Language.ConfigurationImportFile, Language.strMenuExit }; var answered = false; while (!answered) { try { CTaskDialog.ShowTaskDialogBox( GeneralAppInfo.ProductName, Language.ConnectionFileNotFound, "", "", "", "", "", string.Join(" | ", commandButtons), ETaskDialogButtons.None, ESysIcons.Question, ESysIcons.Question); switch (CTaskDialog.CommandButtonResult) { case 0: NewConnectionsFile(connectionFileName); answered = true; break; case 1: LoadConnections(true); answered = true; break; case 2: NewConnectionsFile(connectionFileName); _import.ImportFromFile(ConnectionTreeModel.RootNodes[0]); answered = true; break; case 3: Application.Exit(); answered = true; break; } } catch (Exception exc) { Runtime.MessageCollector.AddExceptionMessage(string.Format(Language.strConnectionsFileCouldNotBeLoadedNew, connectionFileName), exc, MessageClass.InformationMsg); } } return; } Runtime.MessageCollector.AddExceptionStackTrace(string.Format(Language.strConnectionsFileCouldNotBeLoaded, connectionFileName), ex); if (connectionFileName != GetStartupConnectionFileName()) { LoadConnections(withDialog); } else { MessageBox.Show(_dialogWindowParent, string.Format(Language.strErrorStartupConnectionFileLoad, Environment.NewLine, Application.ProductName, GetStartupConnectionFileName(), MiscTools.GetExceptionMessageRecursive(ex)), @"Could not load startup file.", MessageBoxButtons.OK, MessageBoxIcon.Error); Application.Exit(); } } } public string GetStartupConnectionFileName() { return Settings.Default.LoadConsFromCustomLocation == false ? GetDefaultStartupConnectionFileName() : Settings.Default.CustomConsPath; } public string GetDefaultStartupConnectionFileName() { return Runtime.IsPortableEdition ? GetDefaultStartupConnectionFileNamePortableEdition() : GetDefaultStartupConnectionFileNameNormalEdition(); } private void UpdateCustomConsPathSetting(string filename) { if (filename == GetDefaultStartupConnectionFileName()) { Settings.Default.LoadConsFromCustomLocation = false; } else { Settings.Default.LoadConsFromCustomLocation = true; Settings.Default.CustomConsPath = filename; } } private string GetDefaultStartupConnectionFileNameNormalEdition() { var appDataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Application.ProductName, ConnectionsFileInfo.DefaultConnectionsFile); return File.Exists(appDataPath) ? appDataPath : GetDefaultStartupConnectionFileNamePortableEdition(); } private string GetDefaultStartupConnectionFileNamePortableEdition() { return Path.Combine(ConnectionsFileInfo.DefaultConnectionsPath, ConnectionsFileInfo.DefaultConnectionsFile); } #region Events public event EventHandler ConnectionsLoaded; public event EventHandler ConnectionsSaved; private void RaiseConnectionsLoadedEvent(Optional previousTreeModel, ConnectionTreeModel newTreeModel, bool previousSourceWasDatabase, bool newSourceIsDatabase, string newSourcePath) { ConnectionsLoaded?.Invoke(this, new ConnectionsLoadedEventArgs( previousTreeModel, newTreeModel, previousSourceWasDatabase, newSourceIsDatabase, newSourcePath)); } private void RaiseConnectionsSavedEvent(ConnectionTreeModel modelThatWasSaved, bool previouslyUsingDatabase, bool usingDatabase, string connectionFileName) { ConnectionsSaved?.Invoke(this, new ConnectionsSavedEventArgs(modelThatWasSaved, previouslyUsingDatabase, usingDatabase, connectionFileName)); } #endregion } }