diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index ebc2917c..1090b83d 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -11,6 +11,21 @@ Features/Enhancements: #928: Add context menu items to 'Close all but this' and 'Close all tabs to the right' +1.76.12 (2018-11-08): + +Features/Enhancements: +---------------------- +#1180: Allow saving certain connection properties locally when using database + +Fixes: +------ +#1181: Connections sometimes dont immediately load when switching to sql feature +#1173: Fixed memory leak when loading connections multiple times +#1168: Autohide Connection and Config tab won't open when ssh connection active +#1134: Fixed issue where opening a connection opens same connection on other clients when using database feature +#449: Encrypt passwords saved to database + + 1.76.11 (2018-10-18): Fixes: diff --git a/mRemoteNGTests/Config/Serializers/DataTableDeserializerTests.cs b/mRemoteNGTests/Config/Serializers/DataTableDeserializerTests.cs index edba5616..8d7cc77f 100644 --- a/mRemoteNGTests/Config/Serializers/DataTableDeserializerTests.cs +++ b/mRemoteNGTests/Config/Serializers/DataTableDeserializerTests.cs @@ -1,10 +1,12 @@ using System.Data; -using System.Linq; -using mRemoteNG.Config.Serializers; +using System.Security; +using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Connection; using mRemoteNG.Security; +using mRemoteNG.Security.SymmetricEncryption; using mRemoteNG.Tree; using mRemoteNGTests.TestHelpers; +using NSubstitute; using NUnit.Framework; namespace mRemoteNGTests.Config.Serializers @@ -12,30 +14,37 @@ namespace mRemoteNGTests.Config.Serializers public class DataTableDeserializerTests { private DataTableDeserializer _deserializer; + private ICryptographyProvider _cryptographyProvider; + + [SetUp] + public void Setup() + { + _cryptographyProvider = new LegacyRijndaelCryptographyProvider(); + } [Test] public void WeCanDeserializeATree() { var model = CreateConnectionTreeModel(); var dataTable = CreateDataTable(model.RootNodes[0]); - _deserializer = new DataTableDeserializer(); + _deserializer = new DataTableDeserializer(_cryptographyProvider, new SecureString()); var output = _deserializer.Deserialize(dataTable); - Assert.That(output.GetRecursiveChildList().Count(), Is.EqualTo(model.GetRecursiveChildList().Count())); + Assert.That(output.GetRecursiveChildList().Count, Is.EqualTo(model.GetRecursiveChildList().Count)); } [Test] public void WeCanDeserializeASingleEntry() { var dataTable = CreateDataTable(new ConnectionInfo()); - _deserializer = new DataTableDeserializer(); + _deserializer = new DataTableDeserializer(_cryptographyProvider, new SecureString()); var output = _deserializer.Deserialize(dataTable); - Assert.That(output.GetRecursiveChildList().Count(), Is.EqualTo(1)); + Assert.That(output.GetRecursiveChildList().Count, Is.EqualTo(1)); } private DataTable CreateDataTable(ConnectionInfo tableContent) { - var serializer = new DataTableSerializer(new SaveFilter()); + var serializer = new DataTableSerializer(new SaveFilter(), _cryptographyProvider, new SecureString()); return serializer.Serialize(tableContent); } diff --git a/mRemoteNGTests/Config/Serializers/DataTableSerializerTests.cs b/mRemoteNGTests/Config/Serializers/DataTableSerializerTests.cs index 5176fa1a..6292419a 100644 --- a/mRemoteNGTests/Config/Serializers/DataTableSerializerTests.cs +++ b/mRemoteNGTests/Config/Serializers/DataTableSerializerTests.cs @@ -1,11 +1,15 @@ using System.Linq; +using System.Security; using mRemoteNG.Config.Serializers; +using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Connection; using mRemoteNG.Container; using mRemoteNG.Security; +using mRemoteNG.Security.SymmetricEncryption; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; using mRemoteNGTests.TestHelpers; +using NSubstitute; using NUnit.Framework; namespace mRemoteNGTests.Config.Serializers @@ -19,7 +23,10 @@ namespace mRemoteNGTests.Config.Serializers public void Setup() { _saveFilter = new SaveFilter(); - _dataTableSerializer = new DataTableSerializer(_saveFilter); + _dataTableSerializer = new DataTableSerializer( + _saveFilter, + new LegacyRijndaelCryptographyProvider(), + new SecureString()); } [Test] diff --git a/mRemoteV1/App/Info/SettingsFileInfo.cs b/mRemoteV1/App/Info/SettingsFileInfo.cs index 812259a7..a91d9497 100644 --- a/mRemoteV1/App/Info/SettingsFileInfo.cs +++ b/mRemoteV1/App/Info/SettingsFileInfo.cs @@ -2,14 +2,15 @@ using System.IO; using System.Reflection; using System.Windows.Forms; +using mRemoteNG.Connection; namespace mRemoteNG.App.Info { public static class SettingsFileInfo { - private static readonly string ExePath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + private static readonly string ExePath = Path.GetDirectoryName(Assembly.GetAssembly(typeof(ConnectionInfo))?.Location); - public static string SettingsPath { get; } = Runtime.IsPortableEdition ? ExePath : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\" + Application.ProductName; + public static string SettingsPath => Runtime.IsPortableEdition ? ExePath : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\" + Application.ProductName; public static string LayoutFileName { get; } = "pnlLayout.xml"; public static string ExtAppsFilesName { get; } = "extApps.xml"; public static string ThemesFileName { get; } = "Themes.xml"; diff --git a/mRemoteV1/App/Runtime.cs b/mRemoteV1/App/Runtime.cs index b1921a8c..f637a001 100644 --- a/mRemoteV1/App/Runtime.cs +++ b/mRemoteV1/App/Runtime.cs @@ -1,8 +1,3 @@ -using System; -using System.IO; -using System.Security; -using System.Threading; -using System.Windows.Forms; using mRemoteNG.App.Info; using mRemoteNG.Config.Putty; using mRemoteNG.Connection; @@ -15,6 +10,11 @@ using mRemoteNG.Tree.Root; using mRemoteNG.UI; using mRemoteNG.UI.Forms; using mRemoteNG.UI.TaskDialog; +using System; +using System.IO; +using System.Security; +using System.Threading; +using System.Windows.Forms; namespace mRemoteNG.App { @@ -43,19 +43,23 @@ namespace mRemoteNG.App #region Connections Loading/Saving public static void LoadConnectionsAsync() { - _withDialog = false; - var t = new Thread(LoadConnectionsBGd); t.SetApartmentState(ApartmentState.STA); t.Start(); } - private static bool _withDialog; private static void LoadConnectionsBGd() { - LoadConnections(_withDialog); + LoadConnections(); } + /// + /// + /// + /// + /// Should we show the file selection dialog to allow the user to select + /// a connection file + /// public static void LoadConnections(bool withDialog = false) { var connectionFileName = ""; @@ -65,18 +69,19 @@ namespace mRemoteNG.App // disable sql update checking while we are loading updates ConnectionsService.RemoteConnectionsSyncronizer?.Disable(); - if (!Settings.Default.UseSQLServer) + if (withDialog) { - if (withDialog) - { - var loadDialog = DialogFactory.BuildLoadConnectionsDialog(); - if (loadDialog.ShowDialog() != DialogResult.OK) return; - connectionFileName = loadDialog.FileName; - } - else - { - connectionFileName = ConnectionsService.GetStartupConnectionFileName(); - } + var loadDialog = DialogFactory.BuildLoadConnectionsDialog(); + if (loadDialog.ShowDialog() != DialogResult.OK) + return; + + connectionFileName = loadDialog.FileName; + Settings.Default.UseSQLServer = false; + Settings.Default.Save(); + } + else if (!Settings.Default.UseSQLServer) + { + connectionFileName = ConnectionsService.GetStartupConnectionFileName(); } ConnectionsService.LoadConnections(Settings.Default.UseSQLServer, false, connectionFileName); diff --git a/mRemoteV1/Config/Connections/CsvConnectionsSaver.cs b/mRemoteV1/Config/Connections/CsvConnectionsSaver.cs index 28e7b79e..598cea19 100644 --- a/mRemoteV1/Config/Connections/CsvConnectionsSaver.cs +++ b/mRemoteV1/Config/Connections/CsvConnectionsSaver.cs @@ -24,7 +24,7 @@ namespace mRemoteNG.Config.Connections _saveFilter = saveFilter; } - public void Save(ConnectionTreeModel connectionTreeModel) + public void Save(ConnectionTreeModel connectionTreeModel, string propertyNameTrigger = "") { var csvConnectionsSerializer = new CsvConnectionsSerializerMremotengFormat(_saveFilter, Runtime.CredentialProviderCatalog); var dataProvider = new FileDataProvider(_connectionFileName); diff --git a/mRemoteV1/Config/Connections/IConnectionsLoader.cs b/mRemoteV1/Config/Connections/IConnectionsLoader.cs new file mode 100644 index 00000000..a4669757 --- /dev/null +++ b/mRemoteV1/Config/Connections/IConnectionsLoader.cs @@ -0,0 +1,9 @@ +using mRemoteNG.Tree; + +namespace mRemoteNG.Config.Connections +{ + public interface IConnectionsLoader + { + ConnectionTreeModel Load(); + } +} \ No newline at end of file diff --git a/mRemoteV1/Config/Connections/Multiuser/RemoteConnectionsSyncronizer.cs b/mRemoteV1/Config/Connections/Multiuser/RemoteConnectionsSyncronizer.cs index a52ffea3..1a4c2075 100644 --- a/mRemoteV1/Config/Connections/Multiuser/RemoteConnectionsSyncronizer.cs +++ b/mRemoteV1/Config/Connections/Multiuser/RemoteConnectionsSyncronizer.cs @@ -1,6 +1,7 @@ -using System; +using mRemoteNG.App; +using System; using System.Timers; -using mRemoteNG.App; + // ReSharper disable ArrangeAccessorOwnerBody namespace mRemoteNG.Config.Connections.Multiuser diff --git a/mRemoteV1/Config/Connections/Multiuser/SqlConnectionsUpdateChecker.cs b/mRemoteV1/Config/Connections/Multiuser/SqlConnectionsUpdateChecker.cs index fdd8dc1b..af3ab56f 100644 --- a/mRemoteV1/Config/Connections/Multiuser/SqlConnectionsUpdateChecker.cs +++ b/mRemoteV1/Config/Connections/Multiuser/SqlConnectionsUpdateChecker.cs @@ -1,11 +1,11 @@ using mRemoteNG.App; +using mRemoteNG.Config.Connections.Multiuser; +using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Messages; using System; using System.Data; using System.Data.SqlClient; using System.Threading; -using mRemoteNG.Config.Connections.Multiuser; -using mRemoteNG.Config.DatabaseConnectors; namespace mRemoteNG.Config.Connections { @@ -13,7 +13,7 @@ namespace mRemoteNG.Config.Connections { private readonly SqlDatabaseConnector _sqlConnector; private readonly SqlCommand _sqlQuery; - private DateTime _lastUpdateTime; + private DateTime LastUpdateTime => Runtime.ConnectionsService.LastSqlUpdate; private DateTime _lastDatabaseUpdateTime; @@ -21,7 +21,6 @@ namespace mRemoteNG.Config.Connections { _sqlConnector = DatabaseConnectorFactory.SqlDatabaseConnectorFromSettings(); _sqlQuery = new SqlCommand("SELECT * FROM tblUpdate", _sqlConnector.SqlConnection); - _lastUpdateTime = default(DateTime); _lastDatabaseUpdateTime = default(DateTime); } @@ -58,14 +57,14 @@ namespace mRemoteNG.Config.Connections private bool DatabaseIsMoreUpToDateThanUs() { var lastUpdateInDb = GetLastUpdateTimeFromDbResponse(); - var IAmTheLastoneUpdated = CheckIfIAmTheLastOneUpdated(lastUpdateInDb); - return (lastUpdateInDb > _lastUpdateTime && !IAmTheLastoneUpdated); + var amTheLastoneUpdated = CheckIfIAmTheLastOneUpdated(lastUpdateInDb); + return (lastUpdateInDb > LastUpdateTime && !amTheLastoneUpdated); } private bool CheckIfIAmTheLastOneUpdated(DateTime lastUpdateInDb) { - DateTime LastSqlUpdateWithoutMilliseconds = new DateTime(Runtime.ConnectionsService.LastSqlUpdate.Ticks - (Runtime.ConnectionsService.LastSqlUpdate.Ticks % TimeSpan.TicksPerSecond), Runtime.ConnectionsService.LastSqlUpdate.Kind); - return lastUpdateInDb == LastSqlUpdateWithoutMilliseconds; + DateTime lastSqlUpdateWithoutMilliseconds = new DateTime(LastUpdateTime.Ticks - (LastUpdateTime.Ticks % TimeSpan.TicksPerSecond), LastUpdateTime.Kind); + return lastUpdateInDb == lastSqlUpdateWithoutMilliseconds; } private DateTime GetLastUpdateTimeFromDbResponse() @@ -104,10 +103,9 @@ namespace mRemoteNG.Config.Connections public event ConnectionsUpdateAvailableEventHandler ConnectionsUpdateAvailable; private void RaiseConnectionsUpdateAvailableEvent() { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, "Remote connection update is available"); var args = new ConnectionsUpdateAvailableEventArgs(_sqlConnector, _lastDatabaseUpdateTime); ConnectionsUpdateAvailable?.Invoke(this, args); - if(args.Handled) - _lastUpdateTime = _lastDatabaseUpdateTime; } public void Dispose() diff --git a/mRemoteV1/Config/Connections/SaveConnectionsOnEdit.cs b/mRemoteV1/Config/Connections/SaveConnectionsOnEdit.cs index 038b7fff..d36648af 100644 --- a/mRemoteV1/Config/Connections/SaveConnectionsOnEdit.cs +++ b/mRemoteV1/Config/Connections/SaveConnectionsOnEdit.cs @@ -33,7 +33,7 @@ namespace mRemoteNG.Config.Connections private void ConnectionTreeModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) { - SaveConnectionOnEdit(); + SaveConnectionOnEdit(propertyChangedEventArgs.PropertyName); } private void ConnectionTreeModelOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) @@ -41,14 +41,14 @@ namespace mRemoteNG.Config.Connections SaveConnectionOnEdit(); } - private void SaveConnectionOnEdit() + private void SaveConnectionOnEdit(string propertyName = "") { if (!mRemoteNG.Settings.Default.SaveConnectionsAfterEveryEdit) return; if (FrmMain.Default.IsClosing) return; - _connectionsService.SaveConnectionsAsync(); + _connectionsService.SaveConnectionsAsync(propertyName); } } } diff --git a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs index eb2107d8..ed566424 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs @@ -1,22 +1,89 @@ -using mRemoteNG.Config.DatabaseConnectors; +using System; +using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Serializers; +using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Config.Serializers.Versioning; +using mRemoteNG.Container; +using mRemoteNG.Tools; using mRemoteNG.Tree; +using mRemoteNG.Tree.Root; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using mRemoteNG.Security; +using mRemoteNG.Security.Authentication; +using mRemoteNG.Security.SymmetricEncryption; namespace mRemoteNG.Config.Connections { - public class SqlConnectionsLoader + public class SqlConnectionsLoader : IConnectionsLoader { + private readonly IDeserializer> _localConnectionPropertiesDeserializer; + private readonly IDataProvider _dataProvider; + + public Func> AuthenticationRequestor { get; set; } = + () => MiscTools.PasswordDialog("", false); + + public SqlConnectionsLoader( + IDeserializer> localConnectionPropertiesDeserializer, + IDataProvider dataProvider) + { + _localConnectionPropertiesDeserializer = localConnectionPropertiesDeserializer.ThrowIfNull(nameof(localConnectionPropertiesDeserializer)); + _dataProvider = dataProvider.ThrowIfNull(nameof(dataProvider)); + } + public ConnectionTreeModel Load() { var connector = DatabaseConnectorFactory.SqlDatabaseConnectorFromSettings(); var dataProvider = new SqlDataProvider(connector); + var metaDataRetriever = new SqlDatabaseMetaDataRetriever(); var databaseVersionVerifier = new SqlDatabaseVersionVerifier(connector); - databaseVersionVerifier.VerifyDatabaseVersion(); + var cryptoProvider = new LegacyRijndaelCryptographyProvider(); + + var metaData = metaDataRetriever.GetDatabaseMetaData(connector); + var decryptionKey = GetDecryptionKey(metaData); + + if (!decryptionKey.Any()) + throw new Exception("Could not load SQL connections"); + + databaseVersionVerifier.VerifyDatabaseVersion(metaData.ConfVersion); var dataTable = dataProvider.Load(); - var deserializer = new DataTableDeserializer(); - return deserializer.Deserialize(dataTable); + var deserializer = new DataTableDeserializer(cryptoProvider, decryptionKey.First()); + var connectionTree = deserializer.Deserialize(dataTable); + ApplyLocalConnectionProperties(connectionTree.RootNodes.First(i => i is RootNodeInfo)); + return connectionTree; + } + + private Optional GetDecryptionKey(SqlConnectionListMetaData metaData) + { + var cryptographyProvider = new LegacyRijndaelCryptographyProvider(); + var cipherText = metaData.Protected; + var authenticator = new PasswordAuthenticator(cryptographyProvider, cipherText, AuthenticationRequestor); + var authenticated = authenticator.Authenticate(new RootNodeInfo(RootNodeType.Connection).DefaultPassword.ConvertToSecureString()); + + if (authenticated) + return authenticator.LastAuthenticatedPassword; + return Optional.Empty; + } + + private void ApplyLocalConnectionProperties(ContainerInfo rootNode) + { + var localPropertiesXml = _dataProvider.Load(); + var localConnectionProperties = _localConnectionPropertiesDeserializer.Deserialize(localPropertiesXml); + + rootNode + .GetRecursiveChildList() + .Join(localConnectionProperties, + con => con.ConstantID, + locals => locals.ConnectionId, + (con, locals) => new {Connection = con, LocalProperties = locals}) + .ForEach(x => + { + x.Connection.PleaseConnect = x.LocalProperties.Connected; + if (x.Connection is ContainerInfo container) + container.IsExpanded = x.LocalProperties.Expanded; + }); } } } \ No newline at end of file diff --git a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs index 93a5868b..922bee10 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs @@ -1,13 +1,9 @@ -using System; -using System.Data.SqlClient; -using System.Globalization; -using System.Linq; -using System.Security; -using mRemoteNG.App; +using mRemoteNG.App; using mRemoteNG.App.Info; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Serializers; +using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Config.Serializers.Versioning; using mRemoteNG.Container; using mRemoteNG.Messages; @@ -16,6 +12,13 @@ using mRemoteNG.Security.SymmetricEncryption; using mRemoteNG.Tools; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Globalization; +using System.Linq; +using System.Security; +using mRemoteNG.Connection; namespace mRemoteNG.Config.Connections { @@ -23,40 +26,87 @@ namespace mRemoteNG.Config.Connections { private SecureString _password = Runtime.EncryptionKey; private readonly SaveFilter _saveFilter; + private readonly ISerializer, string> _localPropertiesSerializer; + private readonly IDataProvider _dataProvider; - public SqlConnectionsSaver(SaveFilter saveFilter) + public SqlConnectionsSaver( + SaveFilter saveFilter, + ISerializer, string> localPropertieSerializer, + IDataProvider localPropertiesDataProvider) { if (saveFilter == null) throw new ArgumentNullException(nameof(saveFilter)); _saveFilter = saveFilter; + _localPropertiesSerializer = localPropertieSerializer.ThrowIfNull(nameof(localPropertieSerializer)); + _dataProvider = localPropertiesDataProvider.ThrowIfNull(nameof(localPropertiesDataProvider)); } - public void Save(ConnectionTreeModel connectionTreeModel) + public void Save(ConnectionTreeModel connectionTreeModel, string propertyNameTrigger = "") { + var rootTreeNode = connectionTreeModel.RootNodes.OfType().First(); + + UpdateLocalConnectionProperties(rootTreeNode); + + if (PropertyIsLocalOnly(propertyNameTrigger)) + { + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, + $"Property {propertyNameTrigger} is local only. Not saving to database."); + return; + } + if (SqlUserIsReadOnly()) { Runtime.MessageCollector.AddMessage(MessageClass.InformationMsg, "Trying to save connection tree but the SQL read only checkbox is checked, aborting!"); return; } - using (var sqlConnector = DatabaseConnectorFactory.SqlDatabaseConnectorFromSettings()) { sqlConnector.Connect(); var databaseVersionVerifier = new SqlDatabaseVersionVerifier(sqlConnector); + var metaDataRetriever = new SqlDatabaseMetaDataRetriever(); + var metaData = metaDataRetriever.GetDatabaseMetaData(sqlConnector); - if (!databaseVersionVerifier.VerifyDatabaseVersion()) + if (!databaseVersionVerifier.VerifyDatabaseVersion(metaData.ConfVersion)) { Runtime.MessageCollector.AddMessage(MessageClass.ErrorMsg, Language.strErrorConnectionListSaveFailed); return; } - var rootTreeNode = connectionTreeModel.RootNodes.OfType().First(); - UpdateRootNodeTable(rootTreeNode, sqlConnector); UpdateConnectionsTable(rootTreeNode, sqlConnector); UpdateUpdatesTable(sqlConnector); } + + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, "Saved connections to database"); + } + + /// + /// Determines if a given property name should be only saved + /// locally. + /// + /// + /// The name of the property that triggered the save event + /// + /// + private bool PropertyIsLocalOnly(string property) + { + return property == nameof(ConnectionInfo.OpenConnections) || + property == nameof(ContainerInfo.IsExpanded); + } + + private void UpdateLocalConnectionProperties(ContainerInfo rootNode) + { + var a = rootNode.GetRecursiveChildList().Select(info => new LocalConnectionPropertiesModel + { + ConnectionId = info.ConstantID, + Connected = info.OpenConnections.Count > 0, + Expanded = info is ContainerInfo c && c.IsExpanded + }); + + var serializedProperties = _localPropertiesSerializer.Serialize(a); + _dataProvider.Save(serializedProperties); + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, "Saved local connection properties"); } private void UpdateRootNodeTable(RootNodeInfo rootTreeNode, SqlDatabaseConnector sqlDatabaseConnector) @@ -99,13 +149,15 @@ namespace mRemoteNG.Config.Connections } } - private void UpdateConnectionsTable(ContainerInfo rootTreeNode, SqlDatabaseConnector sqlDatabaseConnector) + private void UpdateConnectionsTable(RootNodeInfo rootTreeNode, SqlDatabaseConnector sqlDatabaseConnector) { - var sqlQuery = new SqlCommand("DELETE FROM tblCons", sqlDatabaseConnector.SqlConnection); - sqlQuery.ExecuteNonQuery(); - var serializer = new DataTableSerializer(_saveFilter); + var cryptoProvider = new LegacyRijndaelCryptographyProvider(); + var serializer = new DataTableSerializer(_saveFilter, cryptoProvider, rootTreeNode.PasswordString.ConvertToSecureString()); var dataTable = serializer.Serialize(rootTreeNode); var dataProvider = new SqlDataProvider(sqlDatabaseConnector); + + var sqlQuery = new SqlCommand("DELETE FROM tblCons", sqlDatabaseConnector.SqlConnection); + sqlQuery.ExecuteNonQuery(); dataProvider.Save(dataTable); } diff --git a/mRemoteV1/Config/Connections/XmlConnectionsLoader.cs b/mRemoteV1/Config/Connections/XmlConnectionsLoader.cs index c79e13d8..71a02ebe 100644 --- a/mRemoteV1/Config/Connections/XmlConnectionsLoader.cs +++ b/mRemoteV1/Config/Connections/XmlConnectionsLoader.cs @@ -1,14 +1,14 @@ -using System; -using System.IO; -using System.Security; -using mRemoteNG.Config.DataProviders; +using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Serializers.Xml; using mRemoteNG.Tools; using mRemoteNG.Tree; +using System; +using System.IO; +using System.Security; namespace mRemoteNG.Config.Connections { - public class XmlConnectionsLoader + public class XmlConnectionsLoader : IConnectionsLoader { private readonly string _connectionFilePath; diff --git a/mRemoteV1/Config/Connections/XmlConnectionsSaver.cs b/mRemoteV1/Config/Connections/XmlConnectionsSaver.cs index 060c2c2b..6dad366d 100644 --- a/mRemoteV1/Config/Connections/XmlConnectionsSaver.cs +++ b/mRemoteV1/Config/Connections/XmlConnectionsSaver.cs @@ -27,7 +27,7 @@ namespace mRemoteNG.Config.Connections _saveFilter = saveFilter; } - public void Save(ConnectionTreeModel connectionTreeModel) + public void Save(ConnectionTreeModel connectionTreeModel, string propertyNameTrigger = "") { try { diff --git a/mRemoteV1/Config/CredentialRepositoryListSaver.cs b/mRemoteV1/Config/CredentialRepositoryListSaver.cs index c5000024..6200eb72 100644 --- a/mRemoteV1/Config/CredentialRepositoryListSaver.cs +++ b/mRemoteV1/Config/CredentialRepositoryListSaver.cs @@ -18,7 +18,7 @@ namespace mRemoteNG.Config _dataProvider = dataProvider; } - public void Save(IEnumerable repositories) + public void Save(IEnumerable repositories, string propertyNameTrigger = "") { var serializer = new CredentialRepositoryListSerializer(); var data = serializer.Serialize(repositories); diff --git a/mRemoteV1/Config/ISaver.cs b/mRemoteV1/Config/ISaver.cs index cf4caea8..1f9700be 100644 --- a/mRemoteV1/Config/ISaver.cs +++ b/mRemoteV1/Config/ISaver.cs @@ -2,6 +2,6 @@ { public interface ISaver { - void Save(T model); + void Save(T model, string propertyNameTrigger = ""); } } \ No newline at end of file diff --git a/mRemoteV1/Config/Serializers/DataTableDeserializer.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableDeserializer.cs similarity index 89% rename from mRemoteV1/Config/Serializers/DataTableDeserializer.cs rename to mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableDeserializer.cs index 52f35877..ad01d1a2 100644 --- a/mRemoteV1/Config/Serializers/DataTableDeserializer.cs +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableDeserializer.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using mRemoteNG.App; +using mRemoteNG.App; using mRemoteNG.Connection; using mRemoteNG.Connection.Protocol; using mRemoteNG.Connection.Protocol.Http; @@ -12,11 +8,28 @@ using mRemoteNG.Connection.Protocol.VNC; using mRemoteNG.Container; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Security; +using mRemoteNG.Security; +using mRemoteNG.Security.SymmetricEncryption; +using mRemoteNG.Tools; -namespace mRemoteNG.Config.Serializers +namespace mRemoteNG.Config.Serializers.MsSql { - public class DataTableDeserializer : IDeserializer + public class DataTableDeserializer : IDeserializer { + private readonly ICryptographyProvider _cryptographyProvider; + private readonly SecureString _decryptionKey; + + public DataTableDeserializer(ICryptographyProvider cryptographyProvider, SecureString decryptionKey) + { + _cryptographyProvider = cryptographyProvider.ThrowIfNull(nameof(cryptographyProvider)); + _decryptionKey = decryptionKey.ThrowIfNull(nameof(decryptionKey)); + } + public ConnectionTreeModel Deserialize(DataTable table) { var connectionList = CreateNodesFromTable(table); @@ -34,10 +47,10 @@ namespace mRemoteNG.Config.Serializers switch ((string)row["Type"]) { case "Connection": - nodeList.Add(DeserializeConnectionInfo(row)); + nodeList.Add(DeserializeConnectionInfo(row)); break; case "Container": - nodeList.Add(DeserializeContainerInfo(row)); + nodeList.Add(DeserializeContainerInfo(row)); break; } } @@ -68,16 +81,12 @@ namespace mRemoteNG.Config.Serializers // The Parent object is linked properly later in CreateNodeHierarchy() //connectionInfo.Parent.ConstantID = (string)dataRow["ParentID"]; - var info = connectionInfo as ContainerInfo; - if(info != null) - info.IsExpanded = (bool)dataRow["Expanded"]; - connectionInfo.Description = (string)dataRow["Description"]; connectionInfo.Icon = (string)dataRow["Icon"]; connectionInfo.Panel = (string)dataRow["Panel"]; connectionInfo.Username = (string)dataRow["Username"]; connectionInfo.Domain = (string)dataRow["DomainName"]; - connectionInfo.Password = (string)dataRow["Password"]; + connectionInfo.Password = DecryptValue((string)dataRow["Password"]); connectionInfo.Hostname = (string)dataRow["Hostname"]; connectionInfo.Protocol = (ProtocolType)Enum.Parse(typeof(ProtocolType), (string)dataRow["Protocol"]); connectionInfo.PuttySession = (string)dataRow["PuttySession"]; @@ -106,7 +115,6 @@ namespace mRemoteNG.Config.Serializers connectionInfo.RedirectSound = (RdpProtocol.RDPSounds)Enum.Parse(typeof(RdpProtocol.RDPSounds), (string)dataRow["RedirectSound"]); connectionInfo.SoundQuality = (RdpProtocol.RDPSoundQuality)Enum.Parse(typeof(RdpProtocol.RDPSoundQuality), (string)dataRow["SoundQuality"]); connectionInfo.RedirectKeys = (bool)dataRow["RedirectKeys"]; - connectionInfo.PleaseConnect = (bool)dataRow["Connected"]; connectionInfo.PreExtApp = (string)dataRow["PreExtApp"]; connectionInfo.PostExtApp = (string)dataRow["PostExtApp"]; connectionInfo.MacAddress = (string)dataRow["MacAddress"]; @@ -119,7 +127,7 @@ namespace mRemoteNG.Config.Serializers connectionInfo.VNCProxyIP = (string)dataRow["VNCProxyIP"]; connectionInfo.VNCProxyPort = (int)dataRow["VNCProxyPort"]; connectionInfo.VNCProxyUsername = (string)dataRow["VNCProxyUsername"]; - connectionInfo.VNCProxyPassword = (string)dataRow["VNCProxyPassword"]; + connectionInfo.VNCProxyPassword = DecryptValue((string)dataRow["VNCProxyPassword"]); connectionInfo.VNCColors = (ProtocolVNC.Colors)Enum.Parse(typeof(ProtocolVNC.Colors), (string)dataRow["VNCColors"]); connectionInfo.VNCSmartSizeMode = (ProtocolVNC.SmartSizeMode)Enum.Parse(typeof(ProtocolVNC.SmartSizeMode), (string)dataRow["VNCSmartSizeMode"]); connectionInfo.VNCViewOnly = (bool)dataRow["VNCViewOnly"]; @@ -127,7 +135,7 @@ namespace mRemoteNG.Config.Serializers connectionInfo.RDGatewayHostname = (string)dataRow["RDGatewayHostname"]; connectionInfo.RDGatewayUseConnectionCredentials = (RdpProtocol.RDGatewayUseConnectionCredentials)Enum.Parse(typeof(RdpProtocol.RDGatewayUseConnectionCredentials), (string)dataRow["RDGatewayUseConnectionCredentials"]); connectionInfo.RDGatewayUsername = (string)dataRow["RDGatewayUsername"]; - connectionInfo.RDGatewayPassword = (string)dataRow["RDGatewayPassword"]; + connectionInfo.RDGatewayPassword = DecryptValue((string)dataRow["RDGatewayPassword"]); connectionInfo.RDGatewayDomain = (string)dataRow["RDGatewayDomain"]; connectionInfo.Inheritance.CacheBitmaps = (bool)dataRow["InheritCacheBitmaps"]; @@ -187,10 +195,26 @@ namespace mRemoteNG.Config.Serializers connectionInfo.Inheritance.RDGatewayDomain = (bool)dataRow["InheritRDGatewayDomain"]; } + private string DecryptValue(string cipherText) + { + try + { + return _cryptographyProvider.Decrypt(cipherText, _decryptionKey); + } + catch (EncryptionException) + { + // value may not be encrypted + return cipherText; + } + } + private ConnectionTreeModel CreateNodeHierarchy(List connectionList, DataTable dataTable) { var connectionTreeModel = new ConnectionTreeModel(); - var rootNode = new RootNodeInfo(RootNodeType.Connection, "0"); + var rootNode = new RootNodeInfo(RootNodeType.Connection, "0") + { + PasswordString = _decryptionKey.ConvertToUnsecureString() + }; connectionTreeModel.AddRootNode(rootNode); foreach (DataRow row in dataTable.Rows) diff --git a/mRemoteV1/Config/Serializers/DataTableSerializer.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableSerializer.cs similarity index 94% rename from mRemoteV1/Config/Serializers/DataTableSerializer.cs rename to mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableSerializer.cs index 05fd2ba8..ca65f093 100644 --- a/mRemoteV1/Config/Serializers/DataTableSerializer.cs +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/DataTableSerializer.cs @@ -1,25 +1,31 @@ -using System; -using System.Data; -using System.Data.SqlTypes; -using System.Linq; -using mRemoteNG.Connection; +using mRemoteNG.Connection; using mRemoteNG.Container; using mRemoteNG.Security; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; +using System; +using System.Data; +using System.Data.SqlTypes; +using System.Linq; +using System.Security; +using mRemoteNG.Tools; -namespace mRemoteNG.Config.Serializers +namespace mRemoteNG.Config.Serializers.MsSql { public class DataTableSerializer : ISerializer { + private readonly ICryptographyProvider _cryptographyProvider; + private readonly SecureString _encryptionKey; private DataTable _dataTable; private const string TableName = "tblCons"; private readonly SaveFilter _saveFilter; private int _currentNodeIndex; - public DataTableSerializer(SaveFilter saveFilter) + public DataTableSerializer(SaveFilter saveFilter, ICryptographyProvider cryptographyProvider, SecureString encryptionKey) { - _saveFilter = saveFilter; + _saveFilter = saveFilter.ThrowIfNull(nameof(saveFilter)); + _cryptographyProvider = cryptographyProvider.ThrowIfNull(nameof(cryptographyProvider)); + _encryptionKey = encryptionKey.ThrowIfNull(nameof(encryptionKey)); } @@ -206,14 +212,15 @@ namespace mRemoteNG.Config.Serializers dataRow["ParentID"] = connectionInfo.Parent?.ConstantID ?? ""; dataRow["PositionID"] = _currentNodeIndex; dataRow["LastChange"] = (SqlDateTime)DateTime.Now; - var info = connectionInfo as ContainerInfo; - dataRow["Expanded"] = info != null && info.IsExpanded; + dataRow["Expanded"] = false; // TODO: this column can eventually be removed. we now save this property locally dataRow["Description"] = connectionInfo.Description; dataRow["Icon"] = connectionInfo.Icon; dataRow["Panel"] = connectionInfo.Panel; dataRow["Username"] = _saveFilter.SaveUsername ? connectionInfo.Username : ""; dataRow["DomainName"] = _saveFilter.SaveDomain ? connectionInfo.Domain : ""; - dataRow["Password"] = _saveFilter.SavePassword ? connectionInfo.Password : ""; + dataRow["Password"] = _saveFilter.SavePassword + ? _cryptographyProvider.Encrypt(connectionInfo.Password, _encryptionKey) + : ""; dataRow["Hostname"] = connectionInfo.Hostname; dataRow["Protocol"] = connectionInfo.Protocol; dataRow["PuttySession"] = connectionInfo.PuttySession; @@ -242,7 +249,7 @@ namespace mRemoteNG.Config.Serializers dataRow["RedirectSound"] = connectionInfo.RedirectSound; dataRow["SoundQuality"] = connectionInfo.SoundQuality; dataRow["RedirectKeys"] = connectionInfo.RedirectKeys; - dataRow["Connected"] = connectionInfo.OpenConnections.Count > 0; + dataRow["Connected"] = false; // TODO: this column can eventually be removed. we now save this property locally dataRow["PreExtApp"] = connectionInfo.PreExtApp; dataRow["PostExtApp"] = connectionInfo.PostExtApp; dataRow["MacAddress"] = connectionInfo.MacAddress; @@ -255,14 +262,14 @@ namespace mRemoteNG.Config.Serializers dataRow["VNCProxyIP"] = connectionInfo.VNCProxyIP; dataRow["VNCProxyPort"] = connectionInfo.VNCProxyPort; dataRow["VNCProxyUsername"] = connectionInfo.VNCProxyUsername; - dataRow["VNCProxyPassword"] = connectionInfo.VNCProxyPassword; + dataRow["VNCProxyPassword"] = _cryptographyProvider.Encrypt(connectionInfo.VNCProxyPassword, _encryptionKey); dataRow["VNCColors"] = connectionInfo.VNCColors; dataRow["VNCSmartSizeMode"] = connectionInfo.VNCSmartSizeMode; dataRow["VNCViewOnly"] = connectionInfo.VNCViewOnly; dataRow["RDGatewayUsageMethod"] = connectionInfo.RDGatewayUsageMethod; dataRow["RDGatewayHostname"] = connectionInfo.RDGatewayHostname; dataRow["RDGatewayUseConnectionCredentials"] = connectionInfo.RDGatewayUseConnectionCredentials; - dataRow["RDGatewayUsername"] = connectionInfo.RDGatewayUsername; + dataRow["RDGatewayUsername"] = _cryptographyProvider.Encrypt(connectionInfo.RDGatewayUsername, _encryptionKey); dataRow["RDGatewayPassword"] = connectionInfo.RDGatewayPassword; dataRow["RDGatewayDomain"] = connectionInfo.RDGatewayDomain; if (_saveFilter.SaveInheritance) diff --git a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesModel.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesModel.cs new file mode 100644 index 00000000..f8cdb35f --- /dev/null +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesModel.cs @@ -0,0 +1,20 @@ +namespace mRemoteNG.Config.Serializers.MsSql +{ + public class LocalConnectionPropertiesModel + { + /// + /// The unique Id of this tree node + /// + public string ConnectionId { get; set; } + + /// + /// Indicates whether this connection is connected + /// + public bool Connected { get; set; } + + /// + /// Indicates whether this container is expanded in the tree + /// + public bool Expanded { get; set; } + } +} diff --git a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesXmlSerializer.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesXmlSerializer.cs new file mode 100644 index 00000000..78a82371 --- /dev/null +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/LocalConnectionPropertiesXmlSerializer.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace mRemoteNG.Config.Serializers.MsSql +{ + public class LocalConnectionPropertiesXmlSerializer : + ISerializer, string>, + IDeserializer> + { + public string Serialize(IEnumerable models) + { + var localConnections = models + .Select(m => new XElement("Node", + new XAttribute("ConnectionId", m.ConnectionId), + new XAttribute("Connected", m.Connected), + new XAttribute("Expanded", m.Expanded))); + + var root = new XElement("LocalConnections", localConnections); + var xdoc = new XDocument(new XDeclaration("1.0", "utf-8", null), root); + return WriteXmlToString(xdoc); + } + + public IEnumerable Deserialize(string serializedData) + { + if (string.IsNullOrWhiteSpace(serializedData)) + return Enumerable.Empty(); + + var xdoc = XDocument.Parse(serializedData); + return xdoc + .Descendants("Node") + .Where(e => e.Attribute("ConnectionId") != null) + .Select(e => new LocalConnectionPropertiesModel + { + ConnectionId = e.Attribute("ConnectionId")?.Value, + Connected = bool.Parse(e.Attribute("Connected")?.Value ?? "False"), + Expanded = bool.Parse(e.Attribute("Expanded")?.Value ?? "False") + }); + } + + private static string WriteXmlToString(XNode xmlDocument) + { + string xmlString; + var xmlWriterSettings = new XmlWriterSettings { Indent = true, IndentChars = " ", Encoding = Encoding.UTF8 }; + var memoryStream = new MemoryStream(); + using (var xmlTextWriter = XmlWriter.Create(memoryStream, xmlWriterSettings)) + { + xmlDocument.WriteTo(xmlTextWriter); + xmlTextWriter.Flush(); + var streamReader = new StreamReader(memoryStream, Encoding.UTF8, true); + memoryStream.Seek(0, SeekOrigin.Begin); + xmlString = streamReader.ReadToEnd(); + } + return xmlString; + } + } +} diff --git a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlConnectionListMetaData.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlConnectionListMetaData.cs new file mode 100644 index 00000000..12e8ce86 --- /dev/null +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlConnectionListMetaData.cs @@ -0,0 +1,13 @@ +using System; +using System.Security; + +namespace mRemoteNG.Config.Serializers.MsSql +{ + public class SqlConnectionListMetaData + { + public string Name { get; set; } + public string Protected { get; set; } + public bool Export { get; set; } + public Version ConfVersion { get; set; } + } +} diff --git a/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionRetriever.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs similarity index 57% rename from mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionRetriever.cs rename to mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs index 61c1a778..8c97292d 100644 --- a/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionRetriever.cs +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs @@ -5,13 +5,13 @@ using mRemoteNG.App; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Messages; -namespace mRemoteNG.Config.Serializers.Versioning +namespace mRemoteNG.Config.Serializers.MsSql { - public class SqlDatabaseVersionRetriever + public class SqlDatabaseMetaDataRetriever { - public Version GetDatabaseVersion(SqlDatabaseConnector sqlDatabaseConnector) + public SqlConnectionListMetaData GetDatabaseMetaData(SqlDatabaseConnector sqlDatabaseConnector) { - Version databaseVersion; + SqlConnectionListMetaData metaData; SqlDataReader sqlDataReader = null; try { @@ -20,10 +20,17 @@ namespace mRemoteNG.Config.Serializers.Versioning sqlDatabaseConnector.Connect(); sqlDataReader = sqlCommand.ExecuteReader(); if (!sqlDataReader.HasRows) - return new Version(); // assume new empty database + return null; // assume new empty database else sqlDataReader.Read(); - databaseVersion = new Version(Convert.ToString(sqlDataReader["confVersion"], CultureInfo.InvariantCulture)); + + metaData = new SqlConnectionListMetaData + { + Name = sqlDataReader["Name"] as string ?? "", + Protected = sqlDataReader["Protected"] as string ?? "", + Export = (bool)sqlDataReader["Export"], + ConfVersion = new Version(Convert.ToString(sqlDataReader["confVersion"], CultureInfo.InvariantCulture)) + }; } catch (Exception ex) { @@ -35,7 +42,7 @@ namespace mRemoteNG.Config.Serializers.Versioning if (sqlDataReader != null && !sqlDataReader.IsClosed) sqlDataReader.Close(); } - return databaseVersion; + return metaData; } } } \ No newline at end of file diff --git a/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionVerifier.cs b/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionVerifier.cs index a2ede1bb..b269f27e 100644 --- a/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionVerifier.cs +++ b/mRemoteV1/Config/Serializers/Versioning/SqlDatabaseVersionVerifier.cs @@ -9,7 +9,6 @@ namespace mRemoteNG.Config.Serializers.Versioning public class SqlDatabaseVersionVerifier { private readonly SqlDatabaseConnector _sqlDatabaseConnector; - private readonly SqlDatabaseVersionRetriever _versionRetriever; public SqlDatabaseVersionVerifier(SqlDatabaseConnector sqlDatabaseConnector) { @@ -17,15 +16,14 @@ namespace mRemoteNG.Config.Serializers.Versioning throw new ArgumentNullException(nameof(sqlDatabaseConnector)); _sqlDatabaseConnector = sqlDatabaseConnector; - _versionRetriever = new SqlDatabaseVersionRetriever(); } - public bool VerifyDatabaseVersion() + public bool VerifyDatabaseVersion(Version dbVersion) { var isVerified = false; try { - var databaseVersion = _versionRetriever.GetDatabaseVersion(_sqlDatabaseConnector); + var databaseVersion = dbVersion; if (databaseVersion.Equals(new Version())) { diff --git a/mRemoteV1/Connection/AbstractConnectionRecord.cs b/mRemoteV1/Connection/AbstractConnectionRecord.cs index 835bd9a6..a7f8c6fb 100644 --- a/mRemoteV1/Connection/AbstractConnectionRecord.cs +++ b/mRemoteV1/Connection/AbstractConnectionRecord.cs @@ -685,7 +685,7 @@ namespace mRemoteNG.Connection PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(args.PropertyName)); } - private void SetField(ref T field, T value, string propertyName = null) + protected void SetField(ref T field, T value, string propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return; field = value; diff --git a/mRemoteV1/Connection/ConnectionInitiator.cs b/mRemoteV1/Connection/ConnectionInitiator.cs index 19a8813b..14f6fa77 100644 --- a/mRemoteV1/Connection/ConnectionInitiator.cs +++ b/mRemoteV1/Connection/ConnectionInitiator.cs @@ -1,6 +1,4 @@ -using System; -using System.Windows.Forms; -using mRemoteNG.App; +using mRemoteNG.App; using mRemoteNG.Connection.Protocol; using mRemoteNG.Connection.Protocol.RDP; using mRemoteNG.Container; @@ -8,14 +6,23 @@ using mRemoteNG.Messages; using mRemoteNG.UI.Forms; using mRemoteNG.UI.Panels; using mRemoteNG.UI.Window; +using System; +using System.Collections.Generic; +using System.Windows.Forms; using TabPage = Crownwood.Magic.Controls.TabPage; namespace mRemoteNG.Connection { - public class ConnectionInitiator : IConnectionInitiator + public class ConnectionInitiator : IConnectionInitiator { private readonly PanelAdder _panelAdder = new PanelAdder(); + private readonly List _activeConnections = new List(); + + /// + /// List of unique IDs of the currently active connections + /// + public IEnumerable ActiveConnections => _activeConnections; public void OpenConnection(ContainerInfo containerInfo, ConnectionInfo.Force force = ConnectionInfo.Force.None) { @@ -118,6 +125,7 @@ namespace mRemoteNG.Connection } connectionInfo.OpenConnections.Add(newProtocol); + _activeConnections.Add(connectionInfo.ConstantID); FrmMain.Default.SelectedConnection = connectionInfo; } catch (Exception ex) @@ -210,7 +218,7 @@ namespace mRemoteNG.Connection newProtocol.Closed += ((ConnectionWindow)connectionForm).Prot_Event_Closed; } - private static void SetConnectionEventHandlers(ProtocolBase newProtocol) + private void SetConnectionEventHandlers(ProtocolBase newProtocol) { newProtocol.Disconnected += Prot_Event_Disconnected; newProtocol.Connected += Prot_Event_Connected; @@ -251,7 +259,7 @@ namespace mRemoteNG.Connection } } - private static void Prot_Event_Closed(object sender) + private void Prot_Event_Closed(object sender) { try { @@ -267,6 +275,8 @@ namespace mRemoteNG.Connection Runtime.MessageCollector.AddMessage(MessageClass.InformationMsg, string.Format(Language.strConnenctionClosedByUser, connDetail, prot.InterfaceControl.Info.Protocol, Environment.UserName)); prot.InterfaceControl.Info.OpenConnections.Remove(prot); + if (_activeConnections.Contains(prot.InterfaceControl.Info.ConstantID)) + _activeConnections.Remove(prot.InterfaceControl.Info.ConstantID); if (prot.InterfaceControl.Info.PostExtApp == "") return; var extA = Runtime.ExternalToolsService.GetExtAppByName(prot.InterfaceControl.Info.PostExtApp); diff --git a/mRemoteV1/Connection/ConnectionsService.cs b/mRemoteV1/Connection/ConnectionsService.cs index 0fcbdeae..5fe7c855 100644 --- a/mRemoteV1/Connection/ConnectionsService.cs +++ b/mRemoteV1/Connection/ConnectionsService.cs @@ -1,12 +1,10 @@ -using System; -using System.IO; -using System.Threading; -using System.Windows.Forms; -using mRemoteNG.App; +using mRemoteNG.App; using mRemoteNG.App.Info; using mRemoteNG.Config.Connections; using mRemoteNG.Config.Connections.Multiuser; +using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Putty; +using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Connection.Protocol; using mRemoteNG.Messages; using mRemoteNG.Security; @@ -14,6 +12,11 @@ using mRemoteNG.Tools; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; using mRemoteNG.UI; +using System; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using mRemoteNG.Config; namespace mRemoteNG.Connection { @@ -21,6 +24,8 @@ namespace mRemoteNG.Connection { private static readonly object SaveLock = new object(); private readonly PuttySessionsManager _puttySessionsManager; + private readonly IDataProvider _localConnectionPropertiesDataProvider; + private readonly LocalConnectionPropertiesXmlSerializer _localConnectionPropertiesSerializer; private bool _batchingSaves = false; private bool _saveRequested = false; private bool _saveAsyncRequested = false; @@ -39,6 +44,9 @@ namespace mRemoteNG.Connection throw new ArgumentNullException(nameof(puttySessionsManager)); _puttySessionsManager = puttySessionsManager; + var path = SettingsFileInfo.SettingsPath; + _localConnectionPropertiesDataProvider = new FileDataProvider(Path.Combine(path, "LocalConnectionProperties.xml")); + _localConnectionPropertiesSerializer = new LocalConnectionPropertiesXmlSerializer(); } public void NewConnectionsFile(string filename) @@ -107,9 +115,14 @@ namespace mRemoteNG.Connection var oldConnectionTreeModel = ConnectionTreeModel; var oldIsUsingDatabaseValue = UsingDatabase; - var newConnectionTreeModel = useDatabase - ? new SqlConnectionsLoader().Load() - : new XmlConnectionsLoader(connectionFileName).Load(); + var connectionLoader = useDatabase + ? (IConnectionsLoader)new SqlConnectionsLoader(_localConnectionPropertiesSerializer, _localConnectionPropertiesDataProvider) + : new XmlConnectionsLoader(connectionFileName); + + var newConnectionTreeModel = connectionLoader.Load(); + + if (useDatabase) + LastSqlUpdate = DateTime.Now; if (newConnectionTreeModel == null) { @@ -130,6 +143,7 @@ namespace mRemoteNG.Connection ConnectionTreeModel = newConnectionTreeModel; UpdateCustomConsPathSetting(connectionFileName); RaiseConnectionsLoadedEvent(oldConnectionTreeModel, newConnectionTreeModel, oldIsUsingDatabaseValue, useDatabase, connectionFileName); + Runtime.MessageCollector.AddMessage(MessageClass.DebugMsg, $"Connections loaded using {connectionLoader.GetType().Name}"); } /// @@ -176,7 +190,17 @@ namespace mRemoteNG.Connection /// /// /// 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) + /// + /// Optional. The name of the property that triggered + /// this save. + /// + public void SaveConnections( + ConnectionTreeModel connectionTreeModel, + bool useDatabase, + SaveFilter saveFilter, + string connectionFileName, + bool forceSave = false, + string propertyNameTrigger = "") { if (connectionTreeModel == null) return; @@ -196,10 +220,13 @@ namespace mRemoteNG.Connection RemoteConnectionsSyncronizer?.Disable(); var previouslyUsingDatabase = UsingDatabase; - if (useDatabase) - new SqlConnectionsSaver(saveFilter).Save(connectionTreeModel); - else - new XmlConnectionsSaver(connectionFileName, saveFilter).Save(connectionTreeModel); + + var saver = useDatabase + ? (ISaver)new SqlConnectionsSaver(saveFilter, _localConnectionPropertiesSerializer, + _localConnectionPropertiesDataProvider) + : new XmlConnectionsSaver(connectionFileName, saveFilter); + + saver.Save(connectionTreeModel, propertyNameTrigger); if (UsingDatabase) LastSqlUpdate = DateTime.Now; @@ -219,7 +246,14 @@ namespace mRemoteNG.Connection } } - public void SaveConnectionsAsync() + /// + /// Save the currently loaded connections asynchronously + /// + /// + /// Optional. The name of the property that triggered + /// this save. + /// + public void SaveConnectionsAsync(string propertyNameTrigger = "") { if (_batchingSaves) { @@ -227,19 +261,22 @@ namespace mRemoteNG.Connection return; } - var t = new Thread(SaveConnectionsBGd); + var t = new Thread(() => + { + lock (SaveLock) + { + SaveConnections( + ConnectionTreeModel, + UsingDatabase, + new SaveFilter(), + ConnectionFileName, + propertyNameTrigger: propertyNameTrigger); + } + }); t.SetApartmentState(ApartmentState.STA); t.Start(); } - private void SaveConnectionsBGd() - { - lock (SaveLock) - { - SaveConnections(); - } - } - public string GetStartupConnectionFileName() { return Settings.Default.LoadConsFromCustomLocation == false diff --git a/mRemoteV1/Connection/IConnectionInitiator.cs b/mRemoteV1/Connection/IConnectionInitiator.cs index 5a0ff1d3..485a8940 100644 --- a/mRemoteV1/Connection/IConnectionInitiator.cs +++ b/mRemoteV1/Connection/IConnectionInitiator.cs @@ -1,9 +1,12 @@ using mRemoteNG.Container; +using System.Collections.Generic; namespace mRemoteNG.Connection { public interface IConnectionInitiator { + IEnumerable ActiveConnections { get; } + void OpenConnection(ConnectionInfo connectionInfo); void OpenConnection(ContainerInfo containerInfo, ConnectionInfo.Force force = ConnectionInfo.Force.None); diff --git a/mRemoteV1/Container/ContainerInfo.cs b/mRemoteV1/Container/ContainerInfo.cs index c6c6b4c8..989fc911 100644 --- a/mRemoteV1/Container/ContainerInfo.cs +++ b/mRemoteV1/Container/ContainerInfo.cs @@ -12,13 +12,19 @@ namespace mRemoteNG.Container [DefaultProperty("Name")] public class ContainerInfo : ConnectionInfo, INotifyCollectionChanged { - [Browsable(false)] + private bool _isExpanded; + + [Browsable(false)] public List Children { get; } = new List(); - [Category(""), Browsable(false), ReadOnly(false), Bindable(false), DefaultValue(""), DesignOnly(false)] - public bool IsExpanded { get; set; } + [Category(""), Browsable(false), ReadOnly(false), Bindable(false), DefaultValue(""), DesignOnly(false)] + public bool IsExpanded + { + get => _isExpanded; + set => SetField(ref _isExpanded, value, "IsExpanded"); + } - [Browsable(false)] + [Browsable(false)] public override bool IsContainer { get { return true; } set {} } public ContainerInfo(string uniqueId) diff --git a/mRemoteV1/Tools/Extensions.cs b/mRemoteV1/Tools/Extensions.cs index 76bcba77..ad4eb66b 100644 --- a/mRemoteV1/Tools/Extensions.cs +++ b/mRemoteV1/Tools/Extensions.cs @@ -1,9 +1,11 @@  using System; +using System.Collections.Generic; +using System.Linq; namespace mRemoteNG.Tools { - public static class Extensions + public static class Extensions { public static Optional Maybe(this T value) { @@ -50,5 +52,23 @@ namespace mRemoteNG.Tools throw new ArgumentException("Value cannot be null or empty", argName); return value; } + + /// + /// Perform an action for each item in the given collection. The item + /// is the pass along the processing chain. + /// + /// + /// + /// + /// + public static IEnumerable ForEach(this IEnumerable collection, Action action) + { + collection = collection.ToList(); + + foreach (var item in collection) + action(item); + + return collection; + } } } diff --git a/mRemoteV1/Tree/PreviousSessionOpener.cs b/mRemoteV1/Tree/PreviousSessionOpener.cs index 5df20c8a..ff664b98 100644 --- a/mRemoteV1/Tree/PreviousSessionOpener.cs +++ b/mRemoteV1/Tree/PreviousSessionOpener.cs @@ -1,8 +1,8 @@ -using System; -using System.Linq; -using mRemoteNG.Connection; +using mRemoteNG.Connection; using mRemoteNG.Container; using mRemoteNG.UI.Controls; +using System; +using System.Linq; namespace mRemoteNG.Tree @@ -21,7 +21,12 @@ namespace mRemoteNG.Tree public void Execute(IConnectionTree connectionTree) { var connectionInfoList = connectionTree.GetRootConnectionNode().GetRecursiveChildList().Where(node => !(node is ContainerInfo)); - var previouslyOpenedConnections = connectionInfoList.Where(item => item.PleaseConnect); + var previouslyOpenedConnections = connectionInfoList + .Where(item => + item.PleaseConnect && + // ignore items that have already connected + !_connectionInitiator.ActiveConnections.Contains(item.ConstantID)); + foreach (var connectionInfo in previouslyOpenedConnections) { _connectionInitiator.OpenConnection(connectionInfo); diff --git a/mRemoteV1/UI/Controls/ConnectionTree/ConnectionTree.cs b/mRemoteV1/UI/Controls/ConnectionTree/ConnectionTree.cs index 1919b6c7..14615af5 100644 --- a/mRemoteV1/UI/Controls/ConnectionTree/ConnectionTree.cs +++ b/mRemoteV1/UI/Controls/ConnectionTree/ConnectionTree.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using System.Windows.Forms; -using BrightIdeasSoftware; +using BrightIdeasSoftware; using mRemoteNG.App; using mRemoteNG.Config.Putty; using mRemoteNG.Connection; using mRemoteNG.Container; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Windows.Forms; // ReSharper disable ArrangeAccessorOwnerBody namespace mRemoteNG.UI.Controls @@ -43,8 +43,12 @@ namespace mRemoteNG.UI.Controls get { return _connectionTreeModel; } set { + if (_connectionTreeModel == value) + return; + + UnregisterModelUpdateHandlers(_connectionTreeModel); _connectionTreeModel = value; - PopulateTreeView(); + PopulateTreeView(value); } } @@ -156,28 +160,31 @@ namespace mRemoteNG.UI.Controls padding; } - private void PopulateTreeView() + private void PopulateTreeView(ConnectionTreeModel newModel) { - UnregisterModelUpdateHandlers(); - SetObjects(ConnectionTreeModel.RootNodes); - RegisterModelUpdateHandlers(); - NodeSearcher = new NodeSearcher(ConnectionTreeModel); + SetObjects(newModel.RootNodes); + RegisterModelUpdateHandlers(newModel); + NodeSearcher = new NodeSearcher(newModel); ExecutePostSetupActions(); AutoResizeColumn(Columns[0]); } - private void RegisterModelUpdateHandlers() + private void RegisterModelUpdateHandlers(ConnectionTreeModel newModel) { _puttySessionsManager.PuttySessionsCollectionChanged += OnPuttySessionsCollectionChanged; - ConnectionTreeModel.CollectionChanged += HandleCollectionChanged; - ConnectionTreeModel.PropertyChanged += HandleCollectionPropertyChanged; + newModel.CollectionChanged += HandleCollectionChanged; + newModel.PropertyChanged += HandleCollectionPropertyChanged; } - private void UnregisterModelUpdateHandlers() + private void UnregisterModelUpdateHandlers(ConnectionTreeModel oldConnectionTreeModel) { _puttySessionsManager.PuttySessionsCollectionChanged -= OnPuttySessionsCollectionChanged; - ConnectionTreeModel.CollectionChanged -= HandleCollectionChanged; - ConnectionTreeModel.PropertyChanged -= HandleCollectionPropertyChanged; + + if (oldConnectionTreeModel == null) + return; + + oldConnectionTreeModel.CollectionChanged -= HandleCollectionChanged; + oldConnectionTreeModel.PropertyChanged -= HandleCollectionPropertyChanged; } private void OnPuttySessionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) diff --git a/mRemoteV1/UI/Forms/OptionsPages/SqlServerPage.cs b/mRemoteV1/UI/Forms/OptionsPages/SqlServerPage.cs index ad59e290..71022a18 100644 --- a/mRemoteV1/UI/Forms/OptionsPages/SqlServerPage.cs +++ b/mRemoteV1/UI/Forms/OptionsPages/SqlServerPage.cs @@ -79,7 +79,7 @@ namespace mRemoteNG.UI.Forms.OptionsPages { Runtime.ConnectionsService.RemoteConnectionsSyncronizer?.Dispose(); Runtime.ConnectionsService.RemoteConnectionsSyncronizer = new RemoteConnectionsSyncronizer(new SqlConnectionsUpdateChecker()); - Runtime.ConnectionsService.RemoteConnectionsSyncronizer.Enable(); + Runtime.ConnectionsService.LoadConnections(true, false, ""); } private void DisableSql() diff --git a/mRemoteV1/UI/Forms/frmMain.cs b/mRemoteV1/UI/Forms/frmMain.cs index 4be80b35..bb213821 100644 --- a/mRemoteV1/UI/Forms/frmMain.cs +++ b/mRemoteV1/UI/Forms/frmMain.cs @@ -429,7 +429,8 @@ namespace mRemoteNG.UI.Forms // Only handle this msg if it was triggered by a click if (NativeMethods.LOWORD(m.WParam) == NativeMethods.WA_CLICKACTIVE) { - var controlThatWasClicked = FromChildHandle(NativeMethods.WindowFromPoint(MousePosition)); + var controlThatWasClicked = FromChildHandle(NativeMethods.WindowFromPoint(MousePosition)) + ?? GetChildAtPoint(MousePosition); if (controlThatWasClicked != null) { if (controlThatWasClicked is TreeView || @@ -444,9 +445,14 @@ namespace mRemoteNG.UI.Forms controlThatWasClicked is Crownwood.Magic.Controls.TabControl || controlThatWasClicked is Crownwood.Magic.Controls.InertButton) { - // Simulate a mouse event since one wasn't generated by Windows - SimulateClick(controlThatWasClicked); - controlThatWasClicked.Focus(); + // Simulate a mouse event since one wasn't generated by Windows + SimulateClick(controlThatWasClicked); + controlThatWasClicked.Focus(); + } + else if (controlThatWasClicked is AutoHideStripBase) + { + // only focus the autohide toolstrip + controlThatWasClicked.Focus(); } else { diff --git a/mRemoteV1/UI/Window/ConnectionTreeWindow.cs b/mRemoteV1/UI/Window/ConnectionTreeWindow.cs index cb42408a..e5976d13 100644 --- a/mRemoteV1/UI/Window/ConnectionTreeWindow.cs +++ b/mRemoteV1/UI/Window/ConnectionTreeWindow.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; using mRemoteNG.App; using mRemoteNG.Config.Connections; using mRemoteNG.Connection; @@ -11,6 +5,12 @@ using mRemoteNG.Themes; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; using mRemoteNG.UI.Controls; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; // ReSharper disable ArrangeAccessorOwnerBody @@ -50,8 +50,6 @@ namespace mRemoteNG.UI.Window ConnectionTree.UseFiltering = Settings.Default.UseFilterSearch; ApplyFiltering(); } - - SetConnectionTreeEventHandlers(); } diff --git a/mRemoteV1/mRemoteV1.csproj b/mRemoteV1/mRemoteV1.csproj index a9021b2d..e97f404c 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -136,6 +136,7 @@ + @@ -160,6 +161,9 @@ + + + @@ -170,14 +174,14 @@ - - + + - +