From 92416b466136f927ec493687877849f880e450f3 Mon Sep 17 00:00:00 2001 From: Sean Kaim Date: Sat, 22 Dec 2018 19:19:11 -0500 Subject: [PATCH 01/11] test build for running with FIPS enabled Reference #222 --- CHANGELOG.TXT | 7 +++++++ mRemoteV1/App/CompatibilityChecker.cs | 10 +++++++--- mRemoteV1/Properties/AssemblyInfo.cs | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index 6e1db368..223c56dd 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -1,3 +1,10 @@ +1.76.13 (2018-12-22): + +Changes: +-------- +#222: Pre-Release Test build for running on systems with FIPS Enabled + + 1.76.12 (2018-11-08): Features/Enhancements: diff --git a/mRemoteV1/App/CompatibilityChecker.cs b/mRemoteV1/App/CompatibilityChecker.cs index c26af6cd..b6c7f02d 100644 --- a/mRemoteV1/App/CompatibilityChecker.cs +++ b/mRemoteV1/App/CompatibilityChecker.cs @@ -21,10 +21,14 @@ namespace mRemoteNG.App { messageCollector.AddMessage(MessageClass.InformationMsg, "Checking FIPS Policy...", true); if (!FipsPolicyEnabledForServer2003() && !FipsPolicyEnabledForServer2008AndNewer()) return; - var errorText = string.Format(Language.strErrorFipsPolicyIncompatible, GeneralAppInfo.ProductName, - GeneralAppInfo.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); + var errorText = string.Format(Language.strErrorFipsPolicyIncompatible, GeneralAppInfo.ProductName, GeneralAppInfo.ProductName); messageCollector.AddMessage(MessageClass.ErrorMsg, errorText, true); - MessageBox.Show(FrmMain.Default, errorText); + MessageBox.Show(FrmMain.Default, errorText, GeneralAppInfo.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); + + var CrashOverride = MessageBox.Show(FrmMain.Default, "TEST BUILD -- OK to test mRemoteNG with FIPS Enabled.\nCancel to Exit.", GeneralAppInfo.ProductName, MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation); + if (CrashOverride == DialogResult.OK) + return; + Environment.Exit(1); } diff --git a/mRemoteV1/Properties/AssemblyInfo.cs b/mRemoteV1/Properties/AssemblyInfo.cs index 04ae9d81..19a8f3ae 100644 --- a/mRemoteV1/Properties/AssemblyInfo.cs +++ b/mRemoteV1/Properties/AssemblyInfo.cs @@ -33,5 +33,5 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: // -[assembly: AssemblyVersion("1.76.12.*")] +[assembly: AssemblyVersion("1.76.13.*")] [assembly: NeutralResourcesLanguage("en")] \ No newline at end of file From 08201b0f004b26ae1ceacd55e2ea8c1730e14558 Mon Sep 17 00:00:00 2001 From: Sean Kaim Date: Fri, 8 Feb 2019 16:51:58 -0500 Subject: [PATCH 02/11] Port FIPS override back to 1.76 Fixes #222 --- mRemoteV1/App/CompatibilityChecker.cs | 22 ++++++++++++++----- mRemoteV1/Properties/AssemblyInfo.cs | 4 ++-- mRemoteV1/Properties/Settings.Designer.cs | 14 +++++++++++- mRemoteV1/Properties/Settings.settings | 3 +++ .../Resources/Language/Language.Designer.cs | 4 ++-- mRemoteV1/Resources/Language/Language.resx | 4 ++-- mRemoteV1/app.config | 3 +++ 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/mRemoteV1/App/CompatibilityChecker.cs b/mRemoteV1/App/CompatibilityChecker.cs index b6c7f02d..09d62958 100644 --- a/mRemoteV1/App/CompatibilityChecker.cs +++ b/mRemoteV1/App/CompatibilityChecker.cs @@ -19,17 +19,29 @@ namespace mRemoteNG.App private static void CheckFipsPolicy(MessageCollector messageCollector) { + if (Settings.Default.OverrideFIPSCheck) + { + messageCollector.AddMessage(MessageClass.InformationMsg, "OverrideFIPSCheck is set. Will skip check...", true); + return; + } + messageCollector.AddMessage(MessageClass.InformationMsg, "Checking FIPS Policy...", true); if (!FipsPolicyEnabledForServer2003() && !FipsPolicyEnabledForServer2008AndNewer()) return; - var errorText = string.Format(Language.strErrorFipsPolicyIncompatible, GeneralAppInfo.ProductName, GeneralAppInfo.ProductName); + + var errorText = string.Format(Language.strErrorFipsPolicyIncompatible, GeneralAppInfo.ProductName); messageCollector.AddMessage(MessageClass.ErrorMsg, errorText, true); - MessageBox.Show(FrmMain.Default, errorText, GeneralAppInfo.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); - var CrashOverride = MessageBox.Show(FrmMain.Default, "TEST BUILD -- OK to test mRemoteNG with FIPS Enabled.\nCancel to Exit.", GeneralAppInfo.ProductName, MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation); - if (CrashOverride == DialogResult.OK) + var ShouldIStayOrShouldIGo = CTaskDialog.MessageBox(Application.ProductName, Language.strCompatibilityProblemDetected, errorText, "", "", Language.strCheckboxDoNotShowThisMessageAgain, ETaskDialogButtons.OkCancel, ESysIcons.Warning, ESysIcons.Warning); + if (CTaskDialog.VerificationChecked && ShouldIStayOrShouldIGo == DialogResult.OK) + { + messageCollector.AddMessage(MessageClass.ErrorMsg, "User requests that FIPS check be overridden", true); + Settings.Default.OverrideFIPSCheck = true; + Settings.Default.Save(); return; + } - Environment.Exit(1); + if (ShouldIStayOrShouldIGo == DialogResult.Cancel) + Environment.Exit(1); } private static bool FipsPolicyEnabledForServer2003() diff --git a/mRemoteV1/Properties/AssemblyInfo.cs b/mRemoteV1/Properties/AssemblyInfo.cs index 19a8f3ae..70ff1d15 100644 --- a/mRemoteV1/Properties/AssemblyInfo.cs +++ b/mRemoteV1/Properties/AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("Multi-protocol remote connections manager")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("mRemoteNG")] -[assembly: AssemblyCopyright("Copyright © 2018 mRemoteNG Dev Team; 2010-2013 Riley McArdle; 2007-2009 Felix Deimel")] +[assembly: AssemblyCopyright("Copyright © 2019 mRemoteNG Dev Team; 2010-2013 Riley McArdle; 2007-2009 Felix Deimel")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] @@ -33,5 +33,5 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: // -[assembly: AssemblyVersion("1.76.13.*")] +[assembly: AssemblyVersion("1.76.14.*")] [assembly: NeutralResourcesLanguage("en")] \ No newline at end of file diff --git a/mRemoteV1/Properties/Settings.Designer.cs b/mRemoteV1/Properties/Settings.Designer.cs index 885f33ec..e6dcfb9a 100644 --- a/mRemoteV1/Properties/Settings.Designer.cs +++ b/mRemoteV1/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace mRemoteNG { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -2722,5 +2722,17 @@ namespace mRemoteNG { this["StartUpPanelName"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool OverrideFIPSCheck { + get { + return ((bool)(this["OverrideFIPSCheck"])); + } + set { + this["OverrideFIPSCheck"] = value; + } + } } } diff --git a/mRemoteV1/Properties/Settings.settings b/mRemoteV1/Properties/Settings.settings index 4f2f8cc6..3d680b55 100644 --- a/mRemoteV1/Properties/Settings.settings +++ b/mRemoteV1/Properties/Settings.settings @@ -677,5 +677,8 @@ General + + False + \ No newline at end of file diff --git a/mRemoteV1/Resources/Language/Language.Designer.cs b/mRemoteV1/Resources/Language/Language.Designer.cs index 423a874e..f641180d 100644 --- a/mRemoteV1/Resources/Language/Language.Designer.cs +++ b/mRemoteV1/Resources/Language/Language.Designer.cs @@ -2000,11 +2000,11 @@ namespace mRemoteNG { } /// - /// Looks up a localized string similar to The Windows security setting, "System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing", is enabled. This setting is not compatible with {0}. + /// Looks up a localized string similar to The Windows security setting, "System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing", is enabled. /// ///See the Microsoft Support article at http://support.microsoft.com/kb/811833 for more information. /// - ///{0} will now close.. + ///{0} is not fully FIPS compliant. Click OK to proceed at your own discretion, or Cancel to Exit.. /// internal static string strErrorFipsPolicyIncompatible { get { diff --git a/mRemoteV1/Resources/Language/Language.resx b/mRemoteV1/Resources/Language/Language.resx index 8492d817..56cff077 100644 --- a/mRemoteV1/Resources/Language/Language.resx +++ b/mRemoteV1/Resources/Language/Language.resx @@ -655,11 +655,11 @@ Starting with new connections file. Encryption failed. {0} - The Windows security setting, "System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing", is enabled. This setting is not compatible with {0}. + The Windows security setting, "System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing", is enabled. See the Microsoft Support article at http://support.microsoft.com/kb/811833 for more information. -{0} will now close. +{0} is not fully FIPS compliant. Click OK to proceed at your own discretion, or Cancel to Exit. Errors diff --git a/mRemoteV1/app.config b/mRemoteV1/app.config index 8b26045a..e0a91c64 100644 --- a/mRemoteV1/app.config +++ b/mRemoteV1/app.config @@ -698,6 +698,9 @@ General + + False + From 07a20ed5adfa7b7edcc617c9d7b06f688d934b0d Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 08:41:27 -0600 Subject: [PATCH 03/11] batch saves when importing connection files This reduces saves/backup overwrites when importing multiple files --- mRemoteV1/App/Import.cs | 31 +++++++++---------- mRemoteV1/Connection/ConnectionsService.cs | 27 ++++++++++++----- mRemoteV1/Tools/DisposableAction.cs | 35 ++++++++++++++++++++++ mRemoteV1/mRemoteV1.csproj | 1 + 4 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 mRemoteV1/Tools/DisposableAction.cs diff --git a/mRemoteV1/App/Import.cs b/mRemoteV1/App/Import.cs index 119b9826..99a07f69 100644 --- a/mRemoteV1/App/Import.cs +++ b/mRemoteV1/App/Import.cs @@ -9,7 +9,7 @@ using mRemoteNG.Tools; namespace mRemoteNG.App { - public static class Import + public static class Import { public static void ImportFromFile(ContainerInfo importDestinationContainer) { @@ -35,22 +35,23 @@ namespace mRemoteNG.App if (openFileDialog.ShowDialog() != DialogResult.OK) return; - foreach (var fileName in openFileDialog.FileNames) + using (Runtime.ConnectionsService.BatchedSavingContext()) { - try - { - var importer = BuildConnectionImporterFromFileExtension(fileName); - importer.Import(fileName, importDestinationContainer); - } - catch (Exception ex) - { - MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), Language.strImportFileFailedMainInstruction, - MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); - Runtime.MessageCollector.AddExceptionMessage("Unable to import file.", ex); - } + foreach (var fileName in openFileDialog.FileNames) + { + try + { + var importer = BuildConnectionImporterFromFileExtension(fileName); + importer.Import(fileName, importDestinationContainer); + } + catch (Exception ex) + { + MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), Language.strImportFileFailedMainInstruction, + MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); + Runtime.MessageCollector.AddExceptionMessage("Unable to import file.", ex); + } + } } - - Runtime.ConnectionsService.SaveConnectionsAsync(); } } catch (Exception ex) diff --git a/mRemoteV1/Connection/ConnectionsService.cs b/mRemoteV1/Connection/ConnectionsService.cs index 41221a02..7a4b6a28 100644 --- a/mRemoteV1/Connection/ConnectionsService.cs +++ b/mRemoteV1/Connection/ConnectionsService.cs @@ -1,5 +1,10 @@ -using mRemoteNG.App; +using System; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using mRemoteNG.App; using mRemoteNG.App.Info; +using mRemoteNG.Config; using mRemoteNG.Config.Connections; using mRemoteNG.Config.Connections.Multiuser; using mRemoteNG.Config.DataProviders; @@ -12,15 +17,10 @@ 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 { - public class ConnectionsService + public class ConnectionsService { private static readonly object SaveLock = new object(); private readonly PuttySessionsManager _puttySessionsManager; @@ -171,6 +171,19 @@ namespace mRemoteNG.Connection SaveConnections(); } + /// + /// All calls to or + /// will be deferred until the returned is disposed. + /// Once disposed, this will immediately executes a single + /// or if one has been requested. + /// Place this call in a 'using' block to represent a batched saving context. + /// + /// + public DisposableAction BatchedSavingContext() + { + return new DisposableAction(BeginBatchingSaves, EndBatchingSaves); + } + /// /// Saves the currently loaded with /// no . diff --git a/mRemoteV1/Tools/DisposableAction.cs b/mRemoteV1/Tools/DisposableAction.cs new file mode 100644 index 00000000..cc5fd8ee --- /dev/null +++ b/mRemoteV1/Tools/DisposableAction.cs @@ -0,0 +1,35 @@ +using System; + +namespace mRemoteNG.Tools +{ + /// + /// Represents an action that will be executed when the + /// method is called. Useful for creating Using blocks around logical start/end + /// actions. + /// + public class DisposableAction : IDisposable + { + private readonly Action _disposeAction; + + /// + /// + /// + /// + /// An that should be performed immediately + /// when this object is initialized. It should return quickly. + /// + /// + /// An to be executed when this object is disposed. + /// + public DisposableAction(Action initializeAction, Action disposeAction) + { + initializeAction(); + _disposeAction = disposeAction; + } + + public void Dispose() + { + _disposeAction(); + } + } +} diff --git a/mRemoteV1/mRemoteV1.csproj b/mRemoteV1/mRemoteV1.csproj index 896383b6..8e4f0336 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -318,6 +318,7 @@ + From ec42fe7d7d170b03f18c1c6e1ebfbaa13ee1f322 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 11:17:19 -0600 Subject: [PATCH 04/11] improved testability of the import class --- mRemoteNGTests/App/ImportTests.cs | 47 +++++++++++++++++++++ mRemoteNGTests/mRemoteNGTests.csproj | 1 + mRemoteV1/App/Import.cs | 62 ++++++++++++++++++---------- 3 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 mRemoteNGTests/App/ImportTests.cs diff --git a/mRemoteNGTests/App/ImportTests.cs b/mRemoteNGTests/App/ImportTests.cs new file mode 100644 index 00000000..22628db3 --- /dev/null +++ b/mRemoteNGTests/App/ImportTests.cs @@ -0,0 +1,47 @@ +using System.IO; +using mRemoteNG.App; +using mRemoteNG.Config.Putty; +using mRemoteNG.Connection; +using mRemoteNG.Container; +using mRemoteNGTests.Properties; +using mRemoteNGTests.TestHelpers; +using NUnit.Framework; + +namespace mRemoteNGTests.App +{ + public class ImportTests + { + [Test] + public void ErrorHandlerCalledWhenUnsupportedFileExtensionFound() + { + using (FileTestHelpers.DisposableTempFile(out var file, ".blah")) + { + var conService = new ConnectionsService(PuttySessionsManager.Instance); + var container = new ContainerInfo(); + var exceptionOccurred = false; + + Import.HeadlessFileImport(new []{file}, container, conService, s => exceptionOccurred = true); + + Assert.That(exceptionOccurred); + } + } + + [Test] + public void AnErrorInOneFileDoNotPreventOtherFilesFromProcessing() + { + using (FileTestHelpers.DisposableTempFile(out var badFile, ".blah")) + using (FileTestHelpers.DisposableTempFile(out var rdpFile, ".rdp")) + { + File.AppendAllText(rdpFile, Resources.test_remotedesktopconnection_rdp); + var conService = new ConnectionsService(PuttySessionsManager.Instance); + var container = new ContainerInfo(); + var exceptionCount = 0; + + Import.HeadlessFileImport(new[] { badFile, rdpFile }, container, conService, s => exceptionCount++); + + Assert.That(exceptionCount, Is.EqualTo(1)); + Assert.That(container.Children, Has.One.Items); + } + } + } +} diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index 207c3007..fd012ad6 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -108,6 +108,7 @@ + diff --git a/mRemoteV1/App/Import.cs b/mRemoteV1/App/Import.cs index 99a07f69..48ad47ab 100644 --- a/mRemoteV1/App/Import.cs +++ b/mRemoteV1/App/Import.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; using mRemoteNG.Config.Import; +using mRemoteNG.Connection; using mRemoteNG.Connection.Protocol; using mRemoteNG.Container; using mRemoteNG.Tools; @@ -35,23 +36,12 @@ namespace mRemoteNG.App if (openFileDialog.ShowDialog() != DialogResult.OK) return; - using (Runtime.ConnectionsService.BatchedSavingContext()) - { - foreach (var fileName in openFileDialog.FileNames) - { - try - { - var importer = BuildConnectionImporterFromFileExtension(fileName); - importer.Import(fileName, importDestinationContainer); - } - catch (Exception ex) - { - MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), Language.strImportFileFailedMainInstruction, - MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1); - Runtime.MessageCollector.AddExceptionMessage("Unable to import file.", ex); - } - } - } + HeadlessFileImport( + openFileDialog.FileNames, + importDestinationContainer, + Runtime.ConnectionsService, + fileName => MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), Language.strImportFileFailedMainInstruction, + MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1)); } } catch (Exception ex) @@ -60,12 +50,38 @@ namespace mRemoteNG.App } } + public static void HeadlessFileImport( + IEnumerable filePaths, + ContainerInfo importDestinationContainer, + ConnectionsService connectionsService, + Action exceptionAction = null) + { + using (connectionsService.BatchedSavingContext()) + { + foreach (var fileName in filePaths) + { + try + { + var importer = BuildConnectionImporterFromFileExtension(fileName); + importer.Import(fileName, importDestinationContainer); + } + catch (Exception ex) + { + exceptionAction?.Invoke(fileName); + Runtime.MessageCollector.AddExceptionMessage($"Error occurred while importing file '{fileName}'.", ex); + } + } + } + } + public static void ImportFromActiveDirectory(string ldapPath, ContainerInfo importDestinationContainer, bool importSubOu) { try { - ActiveDirectoryImporter.Import(ldapPath, importDestinationContainer, importSubOu); - Runtime.ConnectionsService.SaveConnectionsAsync(); + using (Runtime.ConnectionsService.BatchedSavingContext()) + { + ActiveDirectoryImporter.Import(ldapPath, importDestinationContainer, importSubOu); + } } catch (Exception ex) { @@ -77,9 +93,11 @@ namespace mRemoteNG.App { try { - var importer = new PortScanImporter(protocol); - importer.Import(hosts, importDestinationContainer); - Runtime.ConnectionsService.SaveConnectionsAsync(); + using (Runtime.ConnectionsService.BatchedSavingContext()) + { + var importer = new PortScanImporter(protocol); + importer.Import(hosts, importDestinationContainer); + } } catch (Exception ex) { From 4847ce054b4aa27b0d3cf743ca5405a46021097b Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 11:17:45 -0600 Subject: [PATCH 05/11] resolved several parsing bugs in the rdcman deserializer --- ...topConnectionManager27DeserializerTests.cs | 321 +++++------------- .../Properties/Resources.Designer.cs | 26 ++ mRemoteNGTests/Properties/Resources.resx | 3 + .../test_rdcman_v2_7_schema3_null_values.rdg | 95 ++++++ mRemoteNGTests/TestHelpers/FileTestHelpers.cs | 21 +- mRemoteNGTests/mRemoteNGTests.csproj | 1 + ...oteDesktopConnectionManagerDeserializer.cs | 63 ++-- 7 files changed, 273 insertions(+), 257 deletions(-) create mode 100644 mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg diff --git a/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs b/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs index 20185a5f..2157fadf 100644 --- a/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs +++ b/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs @@ -1,20 +1,21 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using mRemoteNG.Config.Serializers; +using mRemoteNG.Connection; using mRemoteNG.Connection.Protocol; using mRemoteNG.Connection.Protocol.RDP; using mRemoteNG.Container; -using mRemoteNG.Tree; using mRemoteNGTests.Properties; using NUnit.Framework; namespace mRemoteNGTests.Config.Serializers.MiscSerializers { - public class RemoteDesktopConnectionManager27DeserializerTests + public class RemoteDesktopConnectionManager27DeserializerTests { private string _connectionFileContents; private RemoteDesktopConnectionManagerDeserializer _deserializer; - private ConnectionTreeModel _connectionTreeModel; private const string ExpectedName = "server1_displayname"; private const string ExpectedHostname = "server1"; private const string ExpectedDescription = "Comment text here"; @@ -44,262 +45,62 @@ namespace mRemoteNGTests.Config.Serializers.MiscSerializers { _connectionFileContents = Resources.test_rdcman_v2_7_schema3; _deserializer = new RemoteDesktopConnectionManagerDeserializer(); - _connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); } [Test] public void ConnectionTreeModelHasARootNode() { - var numberOfRootNodes = _connectionTreeModel.RootNodes.Count; + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var numberOfRootNodes = connectionTreeModel.RootNodes.Count; Assert.That(numberOfRootNodes, Is.GreaterThan(0)); } [Test] public void RootNodeHasContents() { - var rootNodeContents = _connectionTreeModel.RootNodes.First().Children; + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var rootNodeContents = connectionTreeModel.RootNodes.First().Children; Assert.That(rootNodeContents, Is.Not.Empty); } [Test] public void AllSubRootFoldersImported() { - var rootNode = _connectionTreeModel.RootNodes.First(); + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var rootNode = connectionTreeModel.RootNodes.First(); var importedRdcmanRootNode = rootNode.Children.OfType().First(); var rootNodeContents = importedRdcmanRootNode.Children.Count(node => node.Name == "Group1" || node.Name == "Group2"); Assert.That(rootNodeContents, Is.EqualTo(2)); } - [Test] - public void ConnectionDisplayNameImported() + [TestCaseSource(nameof(ExpectedPropertyValues))] + public void PropertiesWithValuesAreCorrectlyImported(Func propSelector, object expectedValue) { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Name, Is.EqualTo(ExpectedName)); + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + + var connection = connectionTreeModel + .GetRecursiveChildList() + .OfType() + .First(node => node.Name == "Group1") + .Children + .First(); + + Assert.That(propSelector(connection), Is.EqualTo(expectedValue)); } - [Test] - public void ConnectionHostnameImported() + [TestCaseSource(nameof(NullPropertyValues))] + public void PropertiesWithoutValuesAreIgnored(Func propSelector) { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Hostname, Is.EqualTo(ExpectedHostname)); - } + var connectionTreeModel = _deserializer.Deserialize(Resources.test_rdcman_v2_7_schema3_null_values); - [Test] - public void ConnectionDescriptionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Description, Is.EqualTo(ExpectedDescription)); - } + var importedConnection = connectionTreeModel + .GetRecursiveChildList() + .OfType() + .First(node => node.Name == "Group1") + .Children + .First(); - [Test] - public void ConnectionUsernameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Username, Is.EqualTo(ExpectedUsername)); - } - - [Test] - public void ConnectionDomainImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Domain, Is.EqualTo(ExpectedDomain)); - } - - // Since password is encrypted with a machine key, cant test decryption on another machine - //[Test] - //public void ConnectionPasswordImported() - //{ - // var rootNode = _connectionTreeModel.RootNodes.First(); - // var importedRdcmanRootNode = rootNode.Children.OfType().First(); - // var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - // var connection = group1.Children.First(); - // Assert.That(connection.Password, Is.EqualTo(ExpectedPassword)); - //} - - [Test] - public void ConnectionProtocolSetToRdp() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Protocol, Is.EqualTo(ProtocolType.RDP)); - } - - [Test] - public void ConnectionUseConsoleSessionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.UseConsoleSession, Is.EqualTo(ExpectedUseConsoleSession)); - } - - [Test] - public void ConnectionPortImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Port, Is.EqualTo(ExpectedPort)); - } - - [Test] - public void ConnectionGatewayUsageMethodImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayUsageMethod, Is.EqualTo(ExpectedGatewayUsageMethod)); - } - - [Test] - public void ConnectionGatewayHostnameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayHostname, Is.EqualTo(ExpectedGatewayHostname)); - } - - [Test] - public void ConnectionGatewayUsernameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayUsername, Is.EqualTo(ExpectedGatewayUsername)); - } - - // Since password is encrypted with a machine key, cant test decryption on another machine - //[Test] - //public void ConnectionGatewayPasswordImported() - //{ - // var rootNode = _connectionTreeModel.RootNodes.First(); - // var importedRdcmanRootNode = rootNode.Children.OfType().First(); - // var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - // var connection = group1.Children.First(); - // Assert.That(connection.RDGatewayPassword, Is.EqualTo(ExpectedGatewayPassword)); - //} - - [Test] - public void ConnectionGatewayDomainImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayDomain, Is.EqualTo(ExpectedGatewayDomain)); - } - - [Test] - public void ConnectionResolutionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Resolution, Is.EqualTo(ExpectedRdpResolution)); - } - - [Test] - public void ConnectionColorDepthImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Colors, Is.EqualTo(ExpectedRdpColorDepth)); - } - - [Test] - public void ConnectionAudioRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectSound, Is.EqualTo(ExpectedAudioRedirection)); - } - - [Test] - public void ConnectionKeyRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectKeys, Is.EqualTo(ExpectedKeyRedirection)); - } - - [Test] - public void ConnectionDriveRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectDiskDrives, Is.EqualTo(ExpectedDriveRedirection)); - } - - [Test] - public void ConnectionPortRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectPorts, Is.EqualTo(ExpectedPortRedirection)); - } - - [Test] - public void ConnectionPrinterRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectPrinters, Is.EqualTo(ExpectedPrinterRedirection)); - } - - [Test] - public void ConnectionSmartcardRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectSmartCards, Is.EqualTo(ExpectedSmartcardRedirection)); - } - - [Test] - public void ConnectionauthenticationLevelImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDPAuthenticationLevel, Is.EqualTo(ExpectedAuthLevel)); + Assert.That(propSelector(importedConnection), Is.EqualTo(propSelector(new ConnectionInfo()))); } [Test] @@ -322,5 +123,61 @@ namespace mRemoteNGTests.Config.Serializers.MiscSerializers var badFileContents = Resources.test_rdcman_noversion; Assert.That(() => _deserializer.Deserialize(badFileContents), Throws.TypeOf()); } + + private static IEnumerable ExpectedPropertyValues() + { + return new[] + { + new TestCaseData((Func)(con => con.Name), ExpectedName).SetName(nameof(ConnectionInfo.Name)), + new TestCaseData((Func)(con => con.Hostname), ExpectedHostname).SetName(nameof(ConnectionInfo.Hostname)), + new TestCaseData((Func)(con => con.Description), ExpectedDescription).SetName(nameof(ConnectionInfo.Description)), + new TestCaseData((Func)(con => con.Username), ExpectedUsername).SetName(nameof(ConnectionInfo.Username)), + new TestCaseData((Func)(con => con.Domain), ExpectedDomain).SetName(nameof(ConnectionInfo.Domain)), + new TestCaseData((Func)(con => con.Protocol), ProtocolType.RDP).SetName(nameof(ConnectionInfo.Protocol)), + new TestCaseData((Func)(con => con.UseConsoleSession), ExpectedUseConsoleSession).SetName(nameof(ConnectionInfo.UseConsoleSession)), + new TestCaseData((Func)(con => con.Port), ExpectedPort).SetName(nameof(ConnectionInfo.Port)), + new TestCaseData((Func)(con => con.RDGatewayUsageMethod), ExpectedGatewayUsageMethod).SetName(nameof(ConnectionInfo.RDGatewayUsageMethod)), + new TestCaseData((Func)(con => con.RDGatewayHostname), ExpectedGatewayHostname).SetName(nameof(ConnectionInfo.RDGatewayHostname)), + new TestCaseData((Func)(con => con.RDGatewayUsername), ExpectedGatewayUsername).SetName(nameof(ConnectionInfo.RDGatewayUsername)), + new TestCaseData((Func)(con => con.RDGatewayDomain), ExpectedGatewayDomain).SetName(nameof(ConnectionInfo.RDGatewayDomain)), + new TestCaseData((Func)(con => con.Resolution), ExpectedRdpResolution).SetName(nameof(ConnectionInfo.Resolution)), + new TestCaseData((Func)(con => con.Colors), ExpectedRdpColorDepth).SetName(nameof(ConnectionInfo.Colors)), + new TestCaseData((Func)(con => con.RedirectSound), ExpectedAudioRedirection).SetName(nameof(ConnectionInfo.RedirectSound)), + new TestCaseData((Func)(con => con.RedirectKeys), ExpectedKeyRedirection).SetName(nameof(ConnectionInfo.RedirectKeys)), + new TestCaseData((Func)(con => con.RDPAuthenticationLevel), ExpectedAuthLevel).SetName(nameof(ConnectionInfo.RDPAuthenticationLevel)), + new TestCaseData((Func)(con => con.RedirectSmartCards), ExpectedSmartcardRedirection).SetName(nameof(ConnectionInfo.RedirectSmartCards)), + new TestCaseData((Func)(con => con.RedirectPrinters), ExpectedPrinterRedirection).SetName(nameof(ConnectionInfo.RedirectPrinters)), + new TestCaseData((Func)(con => con.RedirectPorts), ExpectedPortRedirection).SetName(nameof(ConnectionInfo.RedirectPorts)), + new TestCaseData((Func)(con => con.RedirectDiskDrives), ExpectedDriveRedirection).SetName(nameof(ConnectionInfo.RedirectDiskDrives)), + }; + } + + private static IEnumerable NullPropertyValues() + { + return new[] + { + new TestCaseData((Func)(con => con.Name)).SetName(nameof(ConnectionInfo.Name)), + new TestCaseData((Func)(con => con.Hostname)).SetName(nameof(ConnectionInfo.Hostname)), + new TestCaseData((Func)(con => con.Description)).SetName(nameof(ConnectionInfo.Description)), + new TestCaseData((Func)(con => con.Username)).SetName(nameof(ConnectionInfo.Username)), + new TestCaseData((Func)(con => con.Domain)).SetName(nameof(ConnectionInfo.Domain)), + new TestCaseData((Func)(con => con.Protocol)).SetName(nameof(ConnectionInfo.Protocol)), + new TestCaseData((Func)(con => con.UseConsoleSession)).SetName(nameof(ConnectionInfo.UseConsoleSession)), + new TestCaseData((Func)(con => con.Port)).SetName(nameof(ConnectionInfo.Port)), + new TestCaseData((Func)(con => con.RDGatewayUsageMethod)).SetName(nameof(ConnectionInfo.RDGatewayUsageMethod)), + new TestCaseData((Func)(con => con.RDGatewayHostname)).SetName(nameof(ConnectionInfo.RDGatewayHostname)), + new TestCaseData((Func)(con => con.RDGatewayUsername)).SetName(nameof(ConnectionInfo.RDGatewayUsername)), + new TestCaseData((Func)(con => con.RDGatewayDomain)).SetName(nameof(ConnectionInfo.RDGatewayDomain)), + new TestCaseData((Func)(con => con.Resolution)).SetName(nameof(ConnectionInfo.Resolution)), + new TestCaseData((Func)(con => con.Colors)).SetName(nameof(ConnectionInfo.Colors)), + new TestCaseData((Func)(con => con.RedirectSound)).SetName(nameof(ConnectionInfo.RedirectSound)), + new TestCaseData((Func)(con => con.RedirectKeys)).SetName(nameof(ConnectionInfo.RedirectKeys)), + new TestCaseData((Func)(con => con.RDPAuthenticationLevel)).SetName(nameof(ConnectionInfo.RDPAuthenticationLevel)), + new TestCaseData((Func)(con => con.RedirectSmartCards)).SetName(nameof(ConnectionInfo.RedirectSmartCards)), + new TestCaseData((Func)(con => con.RedirectPrinters)).SetName(nameof(ConnectionInfo.RedirectPrinters)), + new TestCaseData((Func)(con => con.RedirectPorts)).SetName(nameof(ConnectionInfo.RedirectPorts)), + new TestCaseData((Func)(con => con.RedirectDiskDrives)).SetName(nameof(ConnectionInfo.RedirectDiskDrives)), + }; + } } } \ No newline at end of file diff --git a/mRemoteNGTests/Properties/Resources.Designer.cs b/mRemoteNGTests/Properties/Resources.Designer.cs index 2f759968..f41785c5 100644 --- a/mRemoteNGTests/Properties/Resources.Designer.cs +++ b/mRemoteNGTests/Properties/Resources.Designer.cs @@ -298,6 +298,32 @@ namespace mRemoteNGTests.Properties { } } + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> + ///<RDCMan programVersion="2.7" schemaVersion="3"> + /// <file> + /// <credentialsProfiles /> + /// <properties> + /// <expanded>True</expanded> + /// <name>test_RDCMan_connections</name> + /// </properties> + /// <smartGroup> + /// <properties> + /// <expanded>False</expanded> + /// <name>AllServers</name> + /// </properties> + /// <ruleGroup operator="All"> + /// <rule> + /// <property>DisplayName</property> + /// <operator>Matches</operator> + /// [rest of string was truncated]";. + /// + internal static string test_rdcman_v2_7_schema3_null_values { + get { + return ResourceManager.GetString("test_rdcman_v2_7_schema3_null_values", resourceCulture); + } + } + /// /// Looks up a localized string similar to screen mode id:i:1 ///use multimon:i:0 diff --git a/mRemoteNGTests/Properties/Resources.resx b/mRemoteNGTests/Properties/Resources.resx index 902712d2..245e2ab3 100644 --- a/mRemoteNGTests/Properties/Resources.resx +++ b/mRemoteNGTests/Properties/Resources.resx @@ -172,6 +172,9 @@ ..\Resources\test_RDCMan_v2_7_schema3.rdg;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Resources\test_rdcman_v2_7_schema3_null_values.rdg;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + ..\Resources\test_remotedesktopconnection.rdp;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 diff --git a/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg b/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg new file mode 100644 index 00000000..2a8c7ba3 --- /dev/null +++ b/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg @@ -0,0 +1,95 @@ + + + + + + True + test_RDCMan_connections + + + + False + AllServers + + + + DisplayName + Matches + server + + + + + + True + Group1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mRemoteNGTests/TestHelpers/FileTestHelpers.cs b/mRemoteNGTests/TestHelpers/FileTestHelpers.cs index d1b9c7eb..9e34a5ea 100644 --- a/mRemoteNGTests/TestHelpers/FileTestHelpers.cs +++ b/mRemoteNGTests/TestHelpers/FileTestHelpers.cs @@ -1,4 +1,5 @@ using System.IO; +using mRemoteNG.Tools; namespace mRemoteNGTests.TestHelpers { @@ -18,9 +19,17 @@ namespace mRemoteNGTests.TestHelpers File.Delete(file); } - public static string NewTempFilePath() + public static void DeleteDirectory(string directory) + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + } + + public static string NewTempFilePath(string extension = "") { var newPath = Path.Combine(GetTestSpecificTempDirectory(), Path.GetRandomFileName()); + if (!string.IsNullOrWhiteSpace(extension)) + newPath = newPath + extension; var folderPath = Path.GetDirectoryName(newPath); if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); @@ -35,5 +44,15 @@ namespace mRemoteNGTests.TestHelpers { return Path.Combine(Path.GetTempPath(), "mRemoteNGTests", Path.GetRandomFileName()); } + + public static DisposableAction DisposableTempFile(out string filePath, string extension = "") + { + var file = NewTempFilePath(extension); + filePath = file; + File.AppendAllText(file, ""); + return new DisposableAction( + () => {}, + () => DeleteDirectory(Path.GetDirectoryName(file))); + } } } \ No newline at end of file diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index fd012ad6..44703c54 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -266,6 +266,7 @@ + diff --git a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs index d8b77abd..7cd5aa45 100644 --- a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs +++ b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs @@ -13,7 +13,7 @@ using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Serializers { - public class RemoteDesktopConnectionManagerDeserializer : IDeserializer + public class RemoteDesktopConnectionManagerDeserializer : IDeserializer { private static int _schemaVersion; /* 1 = RDCMan v2.2 3 = RDCMan v2.7 */ @@ -129,11 +129,19 @@ namespace mRemoteNG.Config.Serializers { var connectionInfo = new ConnectionInfo {Protocol = ProtocolType.RDP}; - var propertiesNode = xmlNode.SelectSingleNode("./properties"); - if (_schemaVersion == 1) propertiesNode = xmlNode; // Version 2.2 defines the container name at the root instead + if (_schemaVersion == 1) + propertiesNode = xmlNode; // Version 2.2 defines the container name at the root instead + connectionInfo.Hostname = propertiesNode?.SelectSingleNode("./name")?.InnerText ?? ""; - connectionInfo.Name = propertiesNode?.SelectSingleNode("./displayName")?.InnerText ?? connectionInfo.Hostname; + + var connectionDisplayName = propertiesNode?.SelectSingleNode("./displayName")?.InnerText; + connectionInfo.Name = !string.IsNullOrWhiteSpace(connectionDisplayName) + ? connectionDisplayName + : string.IsNullOrWhiteSpace(connectionInfo.Hostname) + ? connectionInfo.Name + : connectionInfo.Hostname; + connectionInfo.Description = propertiesNode?.SelectSingleNode("./comment")?.InnerText ?? string.Empty; var logonCredentialsNode = xmlNode.SelectSingleNode("./logonCredentials"); @@ -165,10 +173,12 @@ namespace mRemoteNG.Config.Serializers var connectionSettingsNode = xmlNode.SelectSingleNode("./connectionSettings"); if (connectionSettingsNode?.Attributes?["inherit"]?.Value == "None") { - connectionInfo.UseConsoleSession = bool.Parse(connectionSettingsNode.SelectSingleNode("./connectToConsole")?.InnerText ?? "false"); + if (bool.TryParse(connectionSettingsNode.SelectSingleNode("./connectToConsole")?.InnerText, out var useConsole)) + connectionInfo.UseConsoleSession = useConsole; // ./startProgram // ./workingDir - connectionInfo.Port = Convert.ToInt32(connectionSettingsNode.SelectSingleNode("./port")?.InnerText); + if (int.TryParse(connectionSettingsNode.SelectSingleNode("./port")?.InnerText, out var port)) + connectionInfo.Port = port; } else { @@ -179,12 +189,16 @@ namespace mRemoteNG.Config.Serializers var gatewaySettingsNode = xmlNode.SelectSingleNode("./gatewaySettings"); if (gatewaySettingsNode?.Attributes?["inherit"]?.Value == "None") { - connectionInfo.RDGatewayUsageMethod = gatewaySettingsNode.SelectSingleNode("./enabled")?.InnerText == "True" ? RdpProtocol.RDGatewayUsageMethod.Always : RdpProtocol.RDGatewayUsageMethod.Never; + connectionInfo.RDGatewayUsageMethod = gatewaySettingsNode.SelectSingleNode("./enabled")?.InnerText == "True" + ? RdpProtocol.RDGatewayUsageMethod.Always + : RdpProtocol.RDGatewayUsageMethod.Never; connectionInfo.RDGatewayHostname = gatewaySettingsNode.SelectSingleNode("./hostName")?.InnerText; connectionInfo.RDGatewayUsername = gatewaySettingsNode.SelectSingleNode("./userName")?.InnerText; var passwordNode = gatewaySettingsNode.SelectSingleNode("./password"); - connectionInfo.RDGatewayPassword = passwordNode?.Attributes?["storeAsClearText"]?.Value == "True" ? passwordNode.InnerText : DecryptRdcManPassword(passwordNode?.InnerText); + connectionInfo.RDGatewayPassword = passwordNode?.Attributes?["storeAsClearText"]?.Value == "True" + ? passwordNode.InnerText + : DecryptRdcManPassword(passwordNode?.InnerText); connectionInfo.RDGatewayDomain = gatewaySettingsNode.SelectSingleNode("./domain")?.InnerText; // ./logonMethod @@ -203,15 +217,10 @@ namespace mRemoteNG.Config.Serializers var remoteDesktopNode = xmlNode.SelectSingleNode("./remoteDesktop"); if (remoteDesktopNode?.Attributes?["inherit"]?.Value == "None") { - var resolutionString = remoteDesktopNode.SelectSingleNode("./size")?.InnerText.Replace(" ", ""); - try - { - connectionInfo.Resolution = (RdpProtocol.RDPResolutions)Enum.Parse(typeof(RdpProtocol.RDPResolutions), "Res" + resolutionString); - } - catch (ArgumentException) - { - connectionInfo.Resolution = RdpProtocol.RDPResolutions.FitToWindow; - } + connectionInfo.Resolution = + Enum.TryParse(remoteDesktopNode.SelectSingleNode("./size")?.InnerText.Replace(" ", ""), true, out var rdpResolution) + ? rdpResolution + : RdpProtocol.RDPResolutions.FitToWindow; if (remoteDesktopNode.SelectSingleNode("./sameSizeAsClientArea")?.InnerText == "True") { @@ -223,9 +232,8 @@ namespace mRemoteNG.Config.Serializers connectionInfo.Resolution = RdpProtocol.RDPResolutions.Fullscreen; } - var colorDepth = remoteDesktopNode.SelectSingleNode("./colorDepth")?.InnerText; - if (colorDepth != null) - connectionInfo.Colors = (RdpProtocol.RDPColors)Enum.Parse(typeof(RdpProtocol.RDPColors), colorDepth); + if (Enum.TryParse(remoteDesktopNode.SelectSingleNode("./colorDepth")?.InnerText, true, out var rdpColors)) + connectionInfo.Colors = rdpColors; } else { @@ -274,10 +282,17 @@ namespace mRemoteNG.Config.Serializers } // ./redirectClipboard - connectionInfo.RedirectDiskDrives = bool.Parse(localResourcesNode?.SelectSingleNode("./redirectDrives")?.InnerText ?? "false"); - connectionInfo.RedirectPorts = bool.Parse(localResourcesNode?.SelectSingleNode("./redirectPorts")?.InnerText ?? "false"); - connectionInfo.RedirectPrinters = bool.Parse(localResourcesNode?.SelectSingleNode("./redirectPrinters")?.InnerText ?? "false"); - connectionInfo.RedirectSmartCards = bool.Parse(localResourcesNode?.SelectSingleNode("./redirectSmartCards")?.InnerText ?? "false"); + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectDrives")?.InnerText, out var redirectDisks)) + connectionInfo.RedirectDiskDrives = redirectDisks; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectPorts")?.InnerText, out var redirectPorts)) + connectionInfo.RedirectPorts = redirectPorts; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectPrinters")?.InnerText, out var redirectPrinters)) + connectionInfo.RedirectPrinters = redirectPrinters; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectSmartCards")?.InnerText, out var redirectSmartCards)) + connectionInfo.RedirectSmartCards = redirectSmartCards; } else { From 5c6c76b89838536af23cd264684668074142b534 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 11:30:34 -0600 Subject: [PATCH 06/11] one more parse bug fix --- .../RemoteDesktopConnectionManagerDeserializer.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs index 7cd5aa45..82f0a060 100644 --- a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs +++ b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs @@ -40,11 +40,15 @@ namespace mRemoteNG.Config.Serializers private static void VerifySchemaVersion(XmlNode rdcManNode) { - _schemaVersion = Convert.ToInt32(rdcManNode?.Attributes?["schemaVersion"].Value); - if (_schemaVersion != 1 && _schemaVersion != 3) + if (!int.TryParse(rdcManNode?.Attributes?["schemaVersion"]?.Value, out var version)) + throw new FileFormatException("Could not find schema version attribute."); + + if (version != 1 && version != 3) { - throw (new FileFormatException($"Unsupported schema version ({_schemaVersion}).")); + throw new FileFormatException($"Unsupported schema version ({version})."); } + + _schemaVersion = version; } private static void VerifyFileVersion(XmlNode rdcManNode) @@ -114,7 +118,8 @@ namespace mRemoteNG.Config.Serializers containerPropertiesNode = containerPropertiesNode.SelectSingleNode("./properties"); } newContainer.Name = containerPropertiesNode?.SelectSingleNode("./name")?.InnerText ?? Language.strNewFolder; - newContainer.IsExpanded = bool.Parse(containerPropertiesNode?.SelectSingleNode("./expanded")?.InnerText ?? "false"); + if (bool.TryParse(containerPropertiesNode?.SelectSingleNode("./expanded")?.InnerText, out var expanded)) + newContainer.IsExpanded = expanded; parentContainer.AddChild(newContainer); return newContainer; } From 5cd201440e800d339e6f881439852f0af38b1a85 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 11:44:30 -0600 Subject: [PATCH 07/11] added tests for the disposable action class --- mRemoteNGTests/Tools/DisposableActionTests.cs | 42 +++++++++++++++++++ mRemoteNGTests/mRemoteNGTests.csproj | 1 + mRemoteV1/Tools/DisposableAction.cs | 10 +++++ 3 files changed, 53 insertions(+) create mode 100644 mRemoteNGTests/Tools/DisposableActionTests.cs diff --git a/mRemoteNGTests/Tools/DisposableActionTests.cs b/mRemoteNGTests/Tools/DisposableActionTests.cs new file mode 100644 index 00000000..8fab6f80 --- /dev/null +++ b/mRemoteNGTests/Tools/DisposableActionTests.cs @@ -0,0 +1,42 @@ +using mRemoteNG.Tools; +using NUnit.Framework; + +namespace mRemoteNGTests.Tools +{ + public class DisposableActionTests + { + [Test] + public void InitializerActionRunsWhenObjectIsCreated() + { + var initializerRan = false; + new DisposableAction(() => initializerRan = true, () => { }); + + Assert.That(initializerRan); + } + + [Test] + public void DisposalActionRunsWhenDisposeIsCalled() + { + var disposeActionRan = false; + var action = new DisposableAction(() => {}, () => disposeActionRan = true); + + Assert.That(disposeActionRan, Is.False); + action.Dispose(); + Assert.That(disposeActionRan, Is.True); + } + + [Test] + public void DisposeActionOnlyExecutedOnceWhenCallingDisposeMultipleTimes() + { + var invokeCount = 0; + var action = new DisposableAction(() => { }, () => invokeCount++); + + action.Dispose(); + action.Dispose(); + action.Dispose(); + action.Dispose(); + action.Dispose(); + Assert.That(invokeCount, Is.EqualTo(1)); + } + } +} diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index 44703c54..deb86aa3 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -176,6 +176,7 @@ + diff --git a/mRemoteV1/Tools/DisposableAction.cs b/mRemoteV1/Tools/DisposableAction.cs index cc5fd8ee..19f2f86b 100644 --- a/mRemoteV1/Tools/DisposableAction.cs +++ b/mRemoteV1/Tools/DisposableAction.cs @@ -9,6 +9,7 @@ namespace mRemoteNG.Tools /// public class DisposableAction : IDisposable { + private bool _isDisposed; private readonly Action _disposeAction; /// @@ -29,6 +30,15 @@ namespace mRemoteNG.Tools public void Dispose() { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing || _isDisposed) + return; + + _isDisposed = true; _disposeAction(); } } From e8f2e4f50c20ef80acca512e67d1dcc2d0126d76 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Mon, 4 Mar 2019 11:45:07 -0600 Subject: [PATCH 08/11] bumped patch version --- mRemoteV1/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mRemoteV1/Properties/AssemblyInfo.cs b/mRemoteV1/Properties/AssemblyInfo.cs index 70ff1d15..5a2aab2e 100644 --- a/mRemoteV1/Properties/AssemblyInfo.cs +++ b/mRemoteV1/Properties/AssemblyInfo.cs @@ -33,5 +33,5 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: // -[assembly: AssemblyVersion("1.76.14.*")] +[assembly: AssemblyVersion("1.76.15.*")] [assembly: NeutralResourcesLanguage("en")] \ No newline at end of file From 738b159e95d41cc19d211bfdad5565d42721afe5 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Fri, 8 Mar 2019 10:31:39 -0600 Subject: [PATCH 09/11] handle sql first-run case fixes #1303 --- .../Connections/SqlConnectionsLoader.cs | 23 +++++---- .../Config/Connections/SqlConnectionsSaver.cs | 28 +++++------ .../MsSql/SqlDatabaseMetaDataRetriever.cs | 47 ++++++++++++++++++- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs index ed566424..4ad6f3e1 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs @@ -1,23 +1,23 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; 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; +using mRemoteNG.Tools; +using mRemoteNG.Tree; +using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Connections { - public class SqlConnectionsLoader : IConnectionsLoader + public class SqlConnectionsLoader : IConnectionsLoader { private readonly IDeserializer> _localConnectionPropertiesDeserializer; private readonly IDataProvider _dataProvider; @@ -41,7 +41,8 @@ namespace mRemoteNG.Config.Connections var databaseVersionVerifier = new SqlDatabaseVersionVerifier(connector); var cryptoProvider = new LegacyRijndaelCryptographyProvider(); - var metaData = metaDataRetriever.GetDatabaseMetaData(connector); + var metaData = metaDataRetriever.GetDatabaseMetaData(connector) ?? + HandleFirstRun(metaDataRetriever, connector); var decryptionKey = GetDecryptionKey(metaData); if (!decryptionKey.Any()) @@ -85,5 +86,11 @@ namespace mRemoteNG.Config.Connections container.IsExpanded = x.LocalProperties.Expanded; }); } + + private SqlConnectionListMetaData HandleFirstRun(SqlDatabaseMetaDataRetriever metaDataRetriever, SqlDatabaseConnector connector) + { + metaDataRetriever.WriteDatabaseMetaData(new RootNodeInfo(RootNodeType.Connection), connector); + return metaDataRetriever.GetDatabaseMetaData(connector); + } } } \ No newline at end of file diff --git a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs index 922bee10..2f5e80b9 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs @@ -1,10 +1,16 @@ -using mRemoteNG.App; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Globalization; +using System.Linq; +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.Connection; using mRemoteNG.Container; using mRemoteNG.Messages; using mRemoteNG.Security; @@ -12,19 +18,11 @@ 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 { - public class SqlConnectionsSaver : ISaver + public class SqlConnectionsSaver : ISaver { - private SecureString _password = Runtime.EncryptionKey; private readonly SaveFilter _saveFilter; private readonly ISerializer, string> _localPropertiesSerializer; private readonly IDataProvider _dataProvider; @@ -73,7 +71,7 @@ namespace mRemoteNG.Config.Connections return; } - UpdateRootNodeTable(rootTreeNode, sqlConnector); + metaDataRetriever.WriteDatabaseMetaData(rootTreeNode, sqlConnector); UpdateConnectionsTable(rootTreeNode, sqlConnector); UpdateUpdatesTable(sqlConnector); } @@ -117,17 +115,17 @@ namespace mRemoteNG.Config.Connections { if (rootTreeNode.Password) { - _password = rootTreeNode.PasswordString.ConvertToSecureString(); - strProtected = cryptographyProvider.Encrypt("ThisIsProtected", _password); + var password = rootTreeNode.PasswordString.ConvertToSecureString(); + strProtected = cryptographyProvider.Encrypt("ThisIsProtected", password); } else { - strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", _password); + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); } } else { - strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", _password); + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); } var sqlQuery = new SqlCommand("DELETE FROM tblRoot", sqlDatabaseConnector.SqlConnection); diff --git a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs index 8c97292d..e48b2c85 100644 --- a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs @@ -2,12 +2,17 @@ using System.Data.SqlClient; using System.Globalization; using mRemoteNG.App; +using mRemoteNG.App.Info; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Messages; +using mRemoteNG.Security; +using mRemoteNG.Security.SymmetricEncryption; +using mRemoteNG.Tools; +using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Serializers.MsSql { - public class SqlDatabaseMetaDataRetriever + public class SqlDatabaseMetaDataRetriever { public SqlConnectionListMetaData GetDatabaseMetaData(SqlDatabaseConnector sqlDatabaseConnector) { @@ -44,5 +49,45 @@ namespace mRemoteNG.Config.Serializers.MsSql } return metaData; } + + public void WriteDatabaseMetaData(RootNodeInfo rootTreeNode, SqlDatabaseConnector sqlDatabaseConnector) + { + var cryptographyProvider = new LegacyRijndaelCryptographyProvider(); + string strProtected; + if (rootTreeNode != null) + { + if (rootTreeNode.Password) + { + var password = rootTreeNode.PasswordString.ConvertToSecureString(); + strProtected = cryptographyProvider.Encrypt("ThisIsProtected", password); + } + else + { + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); + } + } + else + { + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); + } + + var sqlQuery = new SqlCommand("DELETE FROM tblRoot", sqlDatabaseConnector.SqlConnection); + sqlQuery.ExecuteNonQuery(); + + if (rootTreeNode != null) + { + sqlQuery = + new SqlCommand( + "INSERT INTO tblRoot (Name, Export, Protected, ConfVersion) VALUES(\'" + + MiscTools.PrepareValueForDB(rootTreeNode.Name) + "\', 0, \'" + strProtected + "\'," + + ConnectionsFileInfo.ConnectionFileVersion.ToString(CultureInfo.InvariantCulture) + ")", + sqlDatabaseConnector.SqlConnection); + sqlQuery.ExecuteNonQuery(); + } + else + { + Runtime.MessageCollector.AddMessage(MessageClass.ErrorMsg, $"UpdateRootNodeTable: rootTreeNode was null. Could not insert!"); + } + } } } \ No newline at end of file From 54322ca949b078418d42351dd52279aae6e3ed8a Mon Sep 17 00:00:00 2001 From: David Sparer Date: Fri, 8 Mar 2019 10:41:57 -0600 Subject: [PATCH 10/11] updated changelog --- CHANGELOG.TXT | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index 223c56dd..ce514580 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -1,3 +1,22 @@ +1.76.15 (2019-03-08): + +Fixes: +------ +#1303: Exception on first connection with new SQL server database +#1304: Resolved several issues with importing multiple RDP Manager v2.7 files + +Features/Enhancements: +---------------------- +Importing multiple files now only causes 1 save event, rather than 1 per file imported. + + +1.76.14 (2019-02-08): + +Features/Enhancements: +---------------------- +#222: Allow FIPS to be enabled + + 1.76.13 (2018-12-22): Changes: From ed81030976f8223b48ac296feec5fe883e7c3295 Mon Sep 17 00:00:00 2001 From: David Sparer Date: Sat, 9 Mar 2019 11:21:21 -0600 Subject: [PATCH 11/11] bumped v1.76.15 release date --- CHANGELOG.TXT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index ce514580..b54af80f 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -1,4 +1,4 @@ -1.76.15 (2019-03-08): +1.76.15 (2019-03-09): Fixes: ------