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.
This commit is contained in:
Rob Hague
2026-07-18 17:34:46 +02:00
parent 11e7a52cb3
commit c66b9f8fb0
20 changed files with 250 additions and 202 deletions
+3
View File
@@ -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
@@ -3,6 +3,22 @@
/// <summary>
/// Represents a transformation that can be applied to a remote path.
/// </summary>
/// <remarks>
/// <para>
/// A remote path transformation is used by <see cref="ScpClient"/> to encode a remote path before it
/// is embedded in the <c>scp</c> 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 cref="RemotePathTransformation.None"/>).
/// </para>
/// <para>
/// See <see cref="RemotePathTransformation"/> 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.
/// </para>
/// </remarks>
public interface IRemotePathTransformation
{
/// <summary>
@@ -12,6 +28,7 @@
/// <returns>
/// The transformed path.
/// </returns>
/// <exception cref="System.ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
string Transform(string path);
}
}
-11
View File
@@ -136,17 +136,6 @@ namespace Renci.SshNet
/// <exception cref="SshConnectionException">Client is not connected.</exception>
ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize);
/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
/// </summary>
/// <returns>
/// An <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes any
/// embedded double quote with a backslash.
/// with a shell.
/// </returns>
IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation();
/// <summary>
/// Creates an <see cref="IConnector"/> that can be used to establish a connection
/// to the server identified by the specified <paramref name="connectionInfo"/>.
+128 -46
View File
@@ -29,10 +29,40 @@ namespace Renci.SshNet
/// </item>
/// </list>
/// </para>
/// <para>
/// <note type="caution">
/// SCP performs a transfer by running <c>scp</c> on the server with the remote path embedded in the
/// command. How that path must be encoded depends on the kind of server:
/// <list type="bullet">
/// <item>
/// <description>
/// 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.
/// </description>
/// </item>
/// <item>
/// <description>
/// On a non-shell-based server the path is used literally and must not be quoted or escaped
/// (see <see cref="RemotePathTransformation.None"/>); otherwise the quoting or escape
/// characters end up as part of the file or directory path.
/// </description>
/// </item>
/// </list>
/// Choose the <see cref="RemotePathTransformation"/> supplied to the constructor to suit the remote
/// server and the trust you place in the paths you pass. Prefer <see cref="SftpClient"/>, which does
/// not involve a remote shell, where possible.
/// </note>
/// </para>
/// </remarks>
#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(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
private const string DirectoryInfoPattern = @"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
private const string TimestampPattern = @"T(?<mtime>\d+) 0 (?<atime>\d+) 0";
@@ -95,7 +125,9 @@ namespace Renci.SshNet
/// Gets or sets the transformation to apply to remote paths.
/// </summary>
/// <value>
/// The transformation to apply to remote paths. The default is <see cref="RemotePathTransformation.DoubleQuote"/>.
/// 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
/// <see cref="RemotePathTransformation.DoubleQuote"/>.
/// </value>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <remarks>
@@ -122,6 +154,14 @@ namespace Renci.SshNet
}
}
private static IRemotePathTransformation DefaultTransform
{
get
{
return SshNet.RemotePathTransformation.DoubleQuote;
}
}
/// <summary>
/// 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 <see langword="true"/>.
@@ -155,65 +195,105 @@ namespace Renci.SshNet
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="connectionInfo">The connection info.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
public ScpClient(ConnectionInfo connectionInfo, IRemotePathTransformation remotePathTransformation)
: this(connectionInfo, ownsConnectionInfo: false, remotePathTransformation)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
public ScpClient(string host, int port, string username, string password, IRemotePathTransformation remotePathTransformation)
: this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true, remotePathTransformation)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
public ScpClient(string host, string username, string password, IRemotePathTransformation remotePathTransformation)
: this(host, ConnectionInfo.DefaultPort, username, password, remotePathTransformation)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
public ScpClient(string host, int port, string username, IRemotePathTransformation remotePathTransformation, params IPrivateKeySource[] keyFiles)
: this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true, remotePathTransformation)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
public ScpClient(string host, string username, IRemotePathTransformation remotePathTransformation, params IPrivateKeySource[] keyFiles)
: this(host, ConnectionInfo.DefaultPort, username, remotePathTransformation, keyFiles)
{
}
/// <inheritdoc cref="ScpClient(ConnectionInfo, IRemotePathTransformation)"/>
[Obsolete(ConstructorObsoleteMessage)]
public ScpClient(ConnectionInfo connectionInfo)
: this(connectionInfo, ownsConnectionInfo: false)
: this(connectionInfo, ownsConnectionInfo: false, DefaultTransform)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
/// <inheritdoc cref="ScpClient(string, int, string, string, IRemotePathTransformation)"/>
[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)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="password">Authentication password.</param>
/// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <inheritdoc cref="ScpClient(string, string, string, IRemotePathTransformation)"/>
[Obsolete(ConstructorObsoleteMessage)]
public ScpClient(string host, string username, string password)
: this(host, ConnectionInfo.DefaultPort, username, password)
: this(host, ConnectionInfo.DefaultPort, username, password, DefaultTransform)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="port">Connection port.</param>
/// <param name="username">Authentication username.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
/// <inheritdoc cref="ScpClient(string, int, string, IRemotePathTransformation, IPrivateKeySource[])"/>
[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)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScpClient"/> class.
/// </summary>
/// <param name="host">Connection host.</param>
/// <param name="username">Authentication username.</param>
/// <param name="keyFiles">Authentication private key file(s) .</param>
/// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
/// <inheritdoc cref="ScpClient(string, string, IRemotePathTransformation, IPrivateKeySource[])"/>
[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
/// </summary>
/// <param name="connectionInfo">The connection info.</param>
/// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
/// connection info will be disposed when this instance is disposed.
/// </remarks>
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
/// <param name="connectionInfo">The connection info.</param>
/// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
/// <param name="serviceFactory">The factory to use for creating new services.</param>
/// <param name="remotePathTransformation">The transformation to apply to remote paths.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
/// connection info will be disposed when this instance is disposed.
/// </remarks>
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;
}
/// <summary>
-14
View File
@@ -139,20 +139,6 @@ namespace Renci.SshNet
return new ShellStream(session, bufferSize);
}
/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
/// </summary>
/// <returns>
/// An <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes any
/// embedded double quote with a backslash.
/// with a shell.
/// </returns>
public IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation()
{
return RemotePathTransformation.DoubleQuote;
}
/// <summary>
/// Creates an <see cref="IConnector"/> that can be used to establish a connection
/// to the server identified by the specified <paramref name="connectionInfo"/>.
@@ -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());
}
}
}
@@ -27,7 +27,7 @@ namespace Renci.SshNet.IntegrationTests
private void DoTest(KeyValuePair<string, Func<Compressor>> 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);
@@ -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
{
/// <summary>
@@ -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();
@@ -1,3 +1,5 @@
#pragma warning disable CS0618 // These SCP tests use the obsolete default-transformation constructors.
namespace Renci.SshNet.IntegrationTests
{
/// <summary>
@@ -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
@@ -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();
@@ -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
{
/// <summary>
@@ -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;
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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
};
@@ -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();
}