From c66b9f8fb06c12e71761e58a577b1e796026310f Mon Sep 17 00:00:00 2001 From: Rob Hague Date: Sat, 18 Jul 2026 17:34:46 +0200 Subject: [PATCH] Require an explicit IRemotePathTransformation for ScpClient SCP performs a transfer by running scp on the server with the remote path embedded in a command. On a shell-based server that command is interpreted by a shell, so a path that is not quoted to suit that shell can be executed as a command on the server (GHSA-mggc-4xg6-vcxf); on a non-shell-based server the path is used literally and must not be quoted at all. The right encoding therefore depends on the server, and no single transformation is safe for every server. Rather than default this choice, obsolete the ScpClient constructors that implicitly used DoubleQuote and add constructors that take an IRemotePathTransformation explicitly, so callers must choose one suited to their server and trust environment. DoubleQuote remains the default for the obsolete constructors, so existing behaviour is unchanged. Document the consideration on ScpClient and IRemotePathTransformation, and recommend using SFTP. --- src/Renci.SshNet/.editorconfig | 3 + src/Renci.SshNet/IRemotePathTransformation.cs | 17 ++ src/Renci.SshNet/IServiceFactory.cs | 11 -- src/Renci.SshNet/ScpClient.cs | 174 +++++++++++++----- src/Renci.SshNet/ServiceFactory.cs | 14 -- .../ScpClientBenchmark.cs | 80 -------- .../CompressionTests.cs | 2 +- .../OldIntegrationTests/ScpClientTest.cs | 2 + .../RemoteSshdConfig.cs | 4 +- .../ScpClientTests.cs | 2 + .../Renci.SshNet.IntegrationTests/ScpTests.cs | 2 + .../Renci.SshNet.IntegrationTests/SshTests.cs | 4 +- .../Classes/ScpClientTest.cs | 102 ++++++++-- ...rectoryInfo_SendExecRequestReturnsFalse.cs | 5 +- ...AndFileInfo_SendExecRequestReturnsFalse.cs | 5 +- ...thAndStream_SendExecRequestReturnsFalse.cs | 5 +- ...InfoAndPath_SendExecRequestReturnsFalse.cs | 5 +- ...InfoAndPath_SendExecRequestReturnsFalse.cs | 5 +- ...ientTest_Upload_FileInfoAndPath_Success.cs | 5 +- ...reamAndPath_SendExecRequestReturnsFalse.cs | 5 +- 20 files changed, 250 insertions(+), 202 deletions(-) delete mode 100644 test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs diff --git a/src/Renci.SshNet/.editorconfig b/src/Renci.SshNet/.editorconfig index 8dee1215..834fe9a6 100644 --- a/src/Renci.SshNet/.editorconfig +++ b/src/Renci.SshNet/.editorconfig @@ -192,6 +192,9 @@ dotnet_diagnostic.MA0040.severity = none # duplicate of CA1849 dotnet_diagnostic.MA0042.severity = none +# S1133: Deprecated code should be removed +dotnet_diagnostic.S1133.severity = suggestion + # S3236: Caller information arguments should not be provided explicitly dotnet_diagnostic.S3236.severity = none diff --git a/src/Renci.SshNet/IRemotePathTransformation.cs b/src/Renci.SshNet/IRemotePathTransformation.cs index d139db5d..735a3a1b 100644 --- a/src/Renci.SshNet/IRemotePathTransformation.cs +++ b/src/Renci.SshNet/IRemotePathTransformation.cs @@ -3,6 +3,22 @@ /// /// Represents a transformation that can be applied to a remote path. /// + /// + /// + /// A remote path transformation is used by to encode a remote path before it + /// is embedded in the scp command that is sent to the server. The correct transformation + /// depends on the server: a shell-based server requires the path to be quoted or escaped according to + /// its shell's rules, whereas a non-shell-based server uses the path literally and requires no + /// transformation (see ). + /// + /// + /// See for the implementations supplied with SSH.NET. On a + /// shell-based server, choosing a transformation that does not match the shell can leave a crafted + /// path able to execute as a command on the server; on a non-shell-based server, applying quoting or + /// escaping corrupts the path. A transformation should therefore be selected deliberately for the + /// target server and the trust placed in the supplied paths. + /// + /// public interface IRemotePathTransformation { /// @@ -12,6 +28,7 @@ /// /// The transformed path. /// + /// is . string Transform(string path); } } diff --git a/src/Renci.SshNet/IServiceFactory.cs b/src/Renci.SshNet/IServiceFactory.cs index 48ca70c8..6830c7ba 100644 --- a/src/Renci.SshNet/IServiceFactory.cs +++ b/src/Renci.SshNet/IServiceFactory.cs @@ -136,17 +136,6 @@ namespace Renci.SshNet /// Client is not connected. ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize); - /// - /// Creates an that encloses a path in double quotes, and escapes - /// any embedded double quote with a backslash. - /// - /// - /// An that encloses a path in double quotes, and escapes any - /// embedded double quote with a backslash. - /// with a shell. - /// - IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation(); - /// /// Creates an that can be used to establish a connection /// to the server identified by the specified . diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs index fffa9601..ac2d3496 100644 --- a/src/Renci.SshNet/ScpClient.cs +++ b/src/Renci.SshNet/ScpClient.cs @@ -29,10 +29,40 @@ namespace Renci.SshNet /// /// /// + /// + /// + /// SCP performs a transfer by running scp on the server with the remote path embedded in the + /// command. How that path must be encoded depends on the kind of server: + /// + /// + /// + /// On a shell-based server the command is interpreted by a shell, so the path must be quoted or + /// escaped according to that shell's rules. An unsuitable transformation can allow a crafted path + /// to be executed as a command on the server. + /// + /// + /// + /// + /// On a non-shell-based server the path is used literally and must not be quoted or escaped + /// (see ); otherwise the quoting or escape + /// characters end up as part of the file or directory path. + /// + /// + /// + /// Choose the supplied to the constructor to suit the remote + /// server and the trust you place in the paths you pass. Prefer , which does + /// not involve a remote shell, where possible. + /// + /// /// #pragma warning disable MA0204 // Remove unnecessary partial modifier; not true for all targets public partial class ScpClient : BaseClient { + private const string ConstructorObsoleteMessage = + @"SCP with insufficiently-escaped paths can allow remote command injection. Use a constructor " + + "taking an IRemotePathTransformation which suits the escaping rules of the remote server and " + + "the trust environment in which this code runs, and consider using SFTP where possible."; + private const string FileInfoPattern = @"C(?\d{4}) (?\d+) (?.+)"; private const string DirectoryInfoPattern = @"D(?\d{4}) (?\d+) (?.+)"; private const string TimestampPattern = @"T(?\d+) 0 (?\d+) 0"; @@ -95,7 +125,9 @@ namespace Renci.SshNet /// Gets or sets the transformation to apply to remote paths. /// /// - /// The transformation to apply to remote paths. The default is . + /// The transformation to apply to remote paths. This is initialized from the transformation + /// passed to the constructor; the obsolete constructors that do not take one use + /// . /// /// is . /// @@ -122,6 +154,14 @@ namespace Renci.SshNet } } + private static IRemotePathTransformation DefaultTransform + { + get + { + return SshNet.RemotePathTransformation.DoubleQuote; + } + } + /// /// Gets or sets a value indicating whether the "-d" flag should be passed to the scp process on the server /// when uploading files. Defaults to . @@ -155,65 +195,105 @@ namespace Renci.SshNet /// Initializes a new instance of the class. /// /// The connection info. + /// The transformation to apply to remote paths. /// is . + public ScpClient(ConnectionInfo connectionInfo, IRemotePathTransformation remotePathTransformation) + : this(connectionInfo, ownsConnectionInfo: false, remotePathTransformation) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection port. + /// Authentication username. + /// Authentication password. + /// The transformation to apply to remote paths. + /// is . + /// is invalid, or is or contains only whitespace characters. + /// is not within and . + public ScpClient(string host, int port, string username, string password, IRemotePathTransformation remotePathTransformation) + : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true, remotePathTransformation) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Authentication username. + /// Authentication password. + /// The transformation to apply to remote paths. + /// is . + /// is invalid, or is or contains only whitespace characters. + public ScpClient(string host, string username, string password, IRemotePathTransformation remotePathTransformation) + : this(host, ConnectionInfo.DefaultPort, username, password, remotePathTransformation) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Connection port. + /// Authentication username. + /// The transformation to apply to remote paths. + /// Authentication private key file(s) . + /// is . + /// is invalid, -or- is or contains only whitespace characters. + /// is not within and . + public ScpClient(string host, int port, string username, IRemotePathTransformation remotePathTransformation, params IPrivateKeySource[] keyFiles) + : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true, remotePathTransformation) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Connection host. + /// Authentication username. + /// The transformation to apply to remote paths. + /// Authentication private key file(s) . + /// is . + /// is invalid, -or- is or contains only whitespace characters. + public ScpClient(string host, string username, IRemotePathTransformation remotePathTransformation, params IPrivateKeySource[] keyFiles) + : this(host, ConnectionInfo.DefaultPort, username, remotePathTransformation, keyFiles) + { + } + + /// + [Obsolete(ConstructorObsoleteMessage)] public ScpClient(ConnectionInfo connectionInfo) - : this(connectionInfo, ownsConnectionInfo: false) + : this(connectionInfo, ownsConnectionInfo: false, DefaultTransform) { } - /// - /// Initializes a new instance of the class. - /// - /// Connection host. - /// Connection port. - /// Authentication username. - /// Authentication password. - /// is . - /// is invalid, or is or contains only whitespace characters. - /// is not within and . + /// + [Obsolete(ConstructorObsoleteMessage)] public ScpClient(string host, int port, string username, string password) - : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true) + : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true, DefaultTransform) { } - /// - /// Initializes a new instance of the class. - /// - /// Connection host. - /// Authentication username. - /// Authentication password. - /// is . - /// is invalid, or is or contains only whitespace characters. + /// + [Obsolete(ConstructorObsoleteMessage)] public ScpClient(string host, string username, string password) - : this(host, ConnectionInfo.DefaultPort, username, password) + : this(host, ConnectionInfo.DefaultPort, username, password, DefaultTransform) { } - /// - /// Initializes a new instance of the class. - /// - /// Connection host. - /// Connection port. - /// Authentication username. - /// Authentication private key file(s) . - /// is . - /// is invalid, -or- is or contains only whitespace characters. - /// is not within and . + /// + [Obsolete(ConstructorObsoleteMessage)] public ScpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles) - : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true) + : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true, DefaultTransform) { } - /// - /// Initializes a new instance of the class. - /// - /// Connection host. - /// Authentication username. - /// Authentication private key file(s) . - /// is . - /// is invalid, -or- is or contains only whitespace characters. + /// + [Obsolete(ConstructorObsoleteMessage)] public ScpClient(string host, string username, params IPrivateKeySource[] keyFiles) - : this(host, ConnectionInfo.DefaultPort, username, keyFiles) + : this(host, ConnectionInfo.DefaultPort, username, DefaultTransform, keyFiles) { } @@ -222,13 +302,14 @@ namespace Renci.SshNet /// /// The connection info. /// Specified whether this instance owns the connection info. + /// The transformation to apply to remote paths. /// is . /// /// If is , then the /// connection info will be disposed when this instance is disposed. /// - private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo) - : this(connectionInfo, ownsConnectionInfo, new ServiceFactory()) + private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IRemotePathTransformation remotePathTransformation) + : this(connectionInfo, ownsConnectionInfo, new ServiceFactory(), remotePathTransformation) { } @@ -238,18 +319,19 @@ namespace Renci.SshNet /// The connection info. /// Specified whether this instance owns the connection info. /// The factory to use for creating new services. + /// The transformation to apply to remote paths. /// is . /// is . /// /// If is , then the /// connection info will be disposed when this instance is disposed. /// - internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory) + internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory, IRemotePathTransformation remotePathTransformation) : base(connectionInfo, ownsConnectionInfo, serviceFactory) { OperationTimeout = Timeout.InfiniteTimeSpan; BufferSize = 1024 * 16; - _remotePathTransformation = serviceFactory.CreateRemotePathDoubleQuoteTransformation(); + _remotePathTransformation = remotePathTransformation; } /// diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index 937f4224..e560b433 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -139,20 +139,6 @@ namespace Renci.SshNet return new ShellStream(session, bufferSize); } - /// - /// Creates an that encloses a path in double quotes, and escapes - /// any embedded double quote with a backslash. - /// - /// - /// An that encloses a path in double quotes, and escapes any - /// embedded double quote with a backslash. - /// with a shell. - /// - public IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation() - { - return RemotePathTransformation.DoubleQuote; - } - /// /// Creates an that can be used to establish a connection /// to the server identified by the specified . diff --git a/test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs deleted file mode 100644 index f73643a9..00000000 --- a/test/Renci.SshNet.IntegrationBenchmarks/ScpClientBenchmark.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text; - -using BenchmarkDotNet.Attributes; - -using Renci.SshNet.IntegrationTests.TestsFixtures; - -namespace Renci.SshNet.IntegrationBenchmarks -{ - [MemoryDiagnoser] - [SimpleJob] - public class ScpClientBenchmark : IntegrationBenchmarkBase - { - private readonly InfrastructureFixture _infrastructureFixture; - - private readonly string _file = $"/tmp/{Guid.NewGuid()}.txt"; - private ScpClient? _scpClient; - private MemoryStream? _uploadStream; - - public ScpClientBenchmark() - { - _infrastructureFixture = InfrastructureFixture.Instance; - } - - [GlobalSetup] - public async Task Setup() - { - await GlobalSetup().ConfigureAwait(false); - _scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); - await _scpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false); - - var fileContent = "File content !@#$%^&*()_+{}:,./<>[];'\\|"; - _uploadStream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent)); - } - - [GlobalCleanup] - public async Task Cleanup() - { - await GlobalCleanup().ConfigureAwait(false); - await _uploadStream!.DisposeAsync().ConfigureAwait(false); - } - - [Benchmark] - public void Connect() - { - using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); - scpClient.Connect(); - } - - [Benchmark] - public async Task ConnectAsync() - { - using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); - await scpClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false); - } - - [Benchmark] - public string ConnectUploadAndDownload() - { - using var scpClient = new ScpClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); - scpClient.Connect(); - _uploadStream!.Position = 0; - scpClient.Upload(_uploadStream, _file); - using var downloadStream = new MemoryStream(); - scpClient.Download(_file, downloadStream); - - return Encoding.UTF8.GetString(downloadStream.ToArray()); - } - - [Benchmark] - public string UploadAndDownload() - { - _uploadStream!.Position = 0; - _scpClient!.Upload(_uploadStream, _file); - using var downloadStream = new MemoryStream(); - _scpClient.Download(_file, downloadStream); - - return Encoding.UTF8.GetString(downloadStream.ToArray()); - } - } -} diff --git a/test/Renci.SshNet.IntegrationTests/CompressionTests.cs b/test/Renci.SshNet.IntegrationTests/CompressionTests.cs index fbceeae1..03e0f0c3 100644 --- a/test/Renci.SshNet.IntegrationTests/CompressionTests.cs +++ b/test/Renci.SshNet.IntegrationTests/CompressionTests.cs @@ -27,7 +27,7 @@ namespace Renci.SshNet.IntegrationTests private void DoTest(KeyValuePair> compressor) { - using (var scpClient = new ScpClient(_connectionInfoFactory.Create())) + using (var scpClient = new ScpClient(_connectionInfoFactory.Create(), RemotePathTransformation.ShellQuote)) { scpClient.ConnectionInfo.CompressionAlgorithms.Clear(); scpClient.ConnectionInfo.CompressionAlgorithms.Add(compressor); diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs index 839e4a03..702139c0 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs @@ -2,6 +2,8 @@ using Renci.SshNet.Common; +#pragma warning disable CS0618 // These SCP tests use the obsolete default-transformation constructors. + namespace Renci.SshNet.IntegrationTests.OldIntegrationTests { /// diff --git a/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs b/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs index 3594af66..b3fb1342 100644 --- a/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs +++ b/test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs @@ -16,7 +16,7 @@ namespace Renci.SshNet.IntegrationTests _remoteSshd = remoteSshd; _connectionInfoFactory = connectionInfoFactory; - using (var client = new ScpClient(_connectionInfoFactory.Create())) + using (var client = new ScpClient(_connectionInfoFactory.Create(), RemotePathTransformation.ShellQuote)) { client.Connect(); @@ -213,7 +213,7 @@ namespace Renci.SshNet.IntegrationTests public RemoteSshd Update() { - using (var client = new ScpClient(_connectionInfoFactory.Create())) + using (var client = new ScpClient(_connectionInfoFactory.Create(), RemotePathTransformation.ShellQuote)) { client.Connect(); diff --git a/test/Renci.SshNet.IntegrationTests/ScpClientTests.cs b/test/Renci.SshNet.IntegrationTests/ScpClientTests.cs index ba424a24..d7c3f91f 100644 --- a/test/Renci.SshNet.IntegrationTests/ScpClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/ScpClientTests.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS0618 // These SCP tests use the obsolete default-transformation constructors. + namespace Renci.SshNet.IntegrationTests { /// diff --git a/test/Renci.SshNet.IntegrationTests/ScpTests.cs b/test/Renci.SshNet.IntegrationTests/ScpTests.cs index efd91c87..8724ae3d 100644 --- a/test/Renci.SshNet.IntegrationTests/ScpTests.cs +++ b/test/Renci.SshNet.IntegrationTests/ScpTests.cs @@ -1,5 +1,7 @@ using Renci.SshNet.Common; +#pragma warning disable CS0618 // These SCP tests use the obsolete default-transformation constructors. + namespace Renci.SshNet.IntegrationTests { // TODO SCP: UPLOAD / DOWNLOAD ZERO LENGTH FILES diff --git a/test/Renci.SshNet.IntegrationTests/SshTests.cs b/test/Renci.SshNet.IntegrationTests/SshTests.cs index 07daf548..2c9dc565 100644 --- a/test/Renci.SshNet.IntegrationTests/SshTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SshTests.cs @@ -789,7 +789,7 @@ namespace Renci.SshNet.IntegrationTests { const string hostsFile = "/etc/hosts"; - using (var client = new ScpClient(linuxAdminConnectionFactory.Create())) + using (var client = new ScpClient(linuxAdminConnectionFactory.Create(), RemotePathTransformation.ShellQuote)) { client.Connect(); @@ -876,7 +876,7 @@ namespace Renci.SshNet.IntegrationTests { const string hostsFile = "/etc/hosts"; - using (var client = new ScpClient(linuxAdminConnectionFactory.Create())) + using (var client = new ScpClient(linuxAdminConnectionFactory.Create(), RemotePathTransformation.ShellQuote)) { client.Connect(); diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest.cs index 83bf8227..47362524 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Tests.Common; +#pragma warning disable CS0618 // These SCP tests use the obsolete default-transformation constructors. + namespace Renci.SshNet.Tests.Classes { /// @@ -22,13 +24,17 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void Ctor_ConnectionInfo_Null() + [DataRow(false)] + [DataRow(true)] + public void Ctor_ConnectionInfo_Null(bool remoteTransformCtor) { const ConnectionInfo connectionInfo = null; try { - _ = new ScpClient(connectionInfo); + _ = remoteTransformCtor + ? new ScpClient(connectionInfo, RemotePathTransformation.ShellQuote) + : new ScpClient(connectionInfo); Assert.Fail(); } catch (ArgumentNullException ex) @@ -39,35 +45,59 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void Ctor_ConnectionInfo_NotNull() + [DataRow(false)] + [DataRow(true)] + public void Ctor_ConnectionInfo_NotNull(bool remoteTransformCtor) { var connectionInfo = new ConnectionInfo("HOST", "USER", new PasswordAuthenticationMethod("USER", "PWD")); - var client = new ScpClient(connectionInfo); + ScpClient client; + if (remoteTransformCtor) + { + client = new ScpClient(connectionInfo, RemotePathTransformation.ShellQuote); + Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); + } + else + { + client = new ScpClient(connectionInfo); + Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); + } + Assert.AreEqual(16 * 1024U, client.BufferSize); Assert.AreSame(connectionInfo, client.ConnectionInfo); Assert.IsFalse(client.IsConnected); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout); - Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); Assert.IsNull(client.Session); } [TestMethod] - public void Ctor_HostAndPortAndUsernameAndPassword() + [DataRow(false)] + [DataRow(true)] + public void Ctor_HostAndPortAndUsernameAndPassword(bool remoteTransformCtor) { var host = _random.Next().ToString(); var port = _random.Next(1, 100); var userName = _random.Next().ToString(); var password = _random.Next().ToString(); - var client = new ScpClient(host, port, userName, password); + ScpClient client; + if (remoteTransformCtor) + { + client = new ScpClient(host, port, userName, password, RemotePathTransformation.ShellQuote); + Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); + } + else + { + client = new ScpClient(host, port, userName, password); + Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); + } + Assert.AreEqual(16 * 1024U, client.BufferSize); Assert.IsNotNull(client.ConnectionInfo); Assert.IsFalse(client.IsConnected); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout); - Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); Assert.IsNull(client.Session); var passwordConnectionInfo = client.ConnectionInfo as PasswordConnectionInfo; @@ -85,19 +115,31 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void Ctor_HostAndUsernameAndPassword() + [DataRow(false)] + [DataRow(true)] + public void Ctor_HostAndUsernameAndPassword(bool remoteTransformCtor) { var host = _random.Next().ToString(); var userName = _random.Next().ToString(); var password = _random.Next().ToString(); - var client = new ScpClient(host, userName, password); + ScpClient client; + if (remoteTransformCtor) + { + client = new ScpClient(host, userName, password, RemotePathTransformation.ShellQuote); + Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); + } + else + { + client = new ScpClient(host, userName, password); + Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); + } + Assert.AreEqual(16 * 1024U, client.BufferSize); Assert.IsNotNull(client.ConnectionInfo); Assert.IsFalse(client.IsConnected); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout); - Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); Assert.IsNull(client.Session); var passwordConnectionInfo = client.ConnectionInfo as PasswordConnectionInfo; @@ -115,20 +157,32 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void Ctor_HostAndPortAndUsernameAndPrivateKeys() + [DataRow(false)] + [DataRow(true)] + public void Ctor_HostAndPortAndUsernameAndPrivateKeys(bool remoteTransformCtor) { var host = _random.Next().ToString(); var port = _random.Next(1, 100); var userName = _random.Next().ToString(); var privateKeys = new[] { GetRsaKey(), GetEcdsaKey() }; - var client = new ScpClient(host, port, userName, privateKeys); + ScpClient client; + if (remoteTransformCtor) + { + client = new ScpClient(host, port, userName, RemotePathTransformation.ShellQuote, privateKeys); + Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); + } + else + { + client = new ScpClient(host, port, userName, privateKeys); + Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); + } + Assert.AreEqual(16 * 1024U, client.BufferSize); Assert.IsNotNull(client.ConnectionInfo); Assert.IsFalse(client.IsConnected); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout); - Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); Assert.IsNull(client.Session); var privateKeyConnectionInfo = client.ConnectionInfo as PrivateKeyConnectionInfo; @@ -149,19 +203,31 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void Ctor_HostAndUsernameAndPrivateKeys() + [DataRow(false)] + [DataRow(true)] + public void Ctor_HostAndUsernameAndPrivateKeys(bool remoteTransformCtor) { var host = _random.Next().ToString(); var userName = _random.Next().ToString(); var privateKeys = new[] { GetRsaKey(), GetEcdsaKey() }; - var client = new ScpClient(host, userName, privateKeys); + ScpClient client; + if (remoteTransformCtor) + { + client = new ScpClient(host, userName, RemotePathTransformation.ShellQuote, privateKeys); + Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); + } + else + { + client = new ScpClient(host, userName, privateKeys); + Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); + } + Assert.AreEqual(16 * 1024U, client.BufferSize); Assert.IsNotNull(client.ConnectionInfo); Assert.IsFalse(client.IsConnected); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.KeepAliveInterval); Assert.AreEqual(new TimeSpan(0, 0, 0, 0, -1), client.OperationTimeout); - Assert.AreSame(RemotePathTransformation.DoubleQuote, client.RemotePathTransformation); Assert.IsNull(client.Session); var privateKeyConnectionInfo = client.ConnectionInfo as PrivateKeyConnectionInfo; diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs index 7e6f1aeb..9c0248bc 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndDirectoryInfo_SendExecRequestReturnsFalse.cs @@ -37,9 +37,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -64,7 +61,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); } diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs index ca638f46..c85f8afb 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndFileInfo_SendExecRequestReturnsFalse.cs @@ -37,9 +37,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -63,7 +60,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); } diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs index 0c8d1d7a..8fc083b3 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Download_PathAndStream_SendExecRequestReturnsFalse.cs @@ -37,9 +37,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - _ = ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); _ = ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -72,7 +69,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); } diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs index 68001f51..932c72d1 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs @@ -36,9 +36,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -63,7 +60,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); } diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs index 8d677f1f..5ad84a46 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs @@ -42,9 +42,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -69,7 +66,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); } diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs index 63610f34..350ef1b8 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs @@ -49,9 +49,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - _ = ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); _ = ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -106,7 +103,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object) + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object) { BufferSize = (uint)_bufferSize }; diff --git a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs index 2800b88c..f0b2f17f 100644 --- a/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs +++ b/test/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs @@ -40,9 +40,6 @@ namespace Renci.SshNet.Tests.Classes { var sequence = new MockSequence(); - _ = ServiceFactoryMock.InSequence(sequence) - .Setup(p => p.CreateRemotePathDoubleQuoteTransformation()) - .Returns(_remotePathTransformationMock.Object); _ = ServiceFactoryMock.InSequence(sequence) .Setup(p => p.CreateSocketFactory()) .Returns(SocketFactoryMock.Object); @@ -75,7 +72,7 @@ namespace Renci.SshNet.Tests.Classes { base.Arrange(); - _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object); + _scpClient = new ScpClient(_connectionInfo, false, ServiceFactoryMock.Object, _remotePathTransformationMock.Object); _scpClient.Uploading += (sender, args) => _uploadingRegister.Add(args); _scpClient.Connect(); }