Update ScpClient to work with revent versions of OpenSSH (#625)

Update ScpClient to work with versions of OpenSSH that include fix for CVE-2018-20685.
This commit is contained in:
Gert Driesen
2020-02-15 21:06:10 +01:00
committed by GitHub
parent 546e2b9ece
commit e9e9b4e463
12 changed files with 517 additions and 52 deletions
Binary file not shown.
@@ -0,0 +1,177 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Common;
using System;
namespace Renci.SshNet.Tests.Classes.Common
{
[TestClass]
public class PosixPathTest_CreateAbsoluteOrRelativeFilePath
{
[TestMethod]
public void Path_Null()
{
const string path = null;
try
{
PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.Fail();
}
catch (ArgumentNullException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual("path", ex.ParamName);
}
}
[TestMethod]
public void Path_Empty()
{
var path = string.Empty;
try
{
PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.Fail();
}
catch (ArgumentException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual(string.Format("The path is a zero-length string.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message);
Assert.AreEqual("path", ex.ParamName);
}
}
[TestMethod]
public void Path_TrailingForwardSlash()
{
var path = "/abc/";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/abc", actual.Directory);
Assert.IsNull(actual.File);
}
[TestMethod]
public void Path_FileWithoutNoDirectory()
{
var path = "abc.log";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual(".", actual.Directory);
Assert.AreSame(path, actual.File);
}
[TestMethod]
public void Path_FileInRootDirectory()
{
var path = "/abc.log";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual.Directory);
Assert.AreEqual("abc.log", actual.File);
}
[TestMethod]
public void Path_RootDirectoryOnly()
{
var path = "/";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual.Directory);
Assert.IsNull(actual.File);
}
[TestMethod]
public void Path_FileInNonRootDirectory()
{
var path = "/home/sshnet/xyz";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/home/sshnet", actual.Directory);
Assert.AreEqual("xyz", actual.File);
}
[TestMethod]
public void Path_BackslashIsNotConsideredDirectorySeparator()
{
var path = "/home\\abc.log";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual.Directory);
Assert.AreEqual("home\\abc.log", actual.File);
}
[TestMethod]
public void Path_ColonIsNotConsideredPathSeparator()
{
var path = "/home:abc.log";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual.Directory);
Assert.AreEqual("home:abc.log", actual.File);
}
[TestMethod]
public void Path_LeadingWhitespace()
{
var path = " / \tabc";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual(" ", actual.Directory);
Assert.AreEqual(" \tabc", actual.File);
}
[TestMethod]
public void Path_TrailingWhitespace()
{
var path = "/abc \t ";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual.Directory);
Assert.AreEqual("abc \t ", actual.File);
}
[TestMethod]
public void Path_OnlyWhitespace()
{
var path = " ";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual(".", actual.Directory);
Assert.AreSame(path, actual.File);
}
[TestMethod]
public void Path_FileNameOnlyWhitespace()
{
var path = "/home/\t ";
var actual = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/home", actual.Directory);
Assert.AreEqual("\t ", actual.File);
}
}
}
@@ -0,0 +1,159 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Common;
namespace Renci.SshNet.Tests.Classes.Common
{
[TestClass]
public class PosixPathTest_GetDirectoryName
{
[TestMethod]
public void Path_Null()
{
const string path = null;
try
{
PosixPath.GetDirectoryName(path);
Assert.Fail();
}
catch (ArgumentNullException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual("path", ex.ParamName);
}
}
[TestMethod]
public void Path_Empty()
{
var path = string.Empty;
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual(".", actual);
}
[TestMethod]
public void Path_TrailingForwardSlash()
{
var path = "/abc/";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/abc", actual);
}
[TestMethod]
public void Path_FileWithoutNoDirectory()
{
var path = "abc.log";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual(".", actual);
}
[TestMethod]
public void Path_FileInRootDirectory()
{
var path = "/abc.log";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual);
}
[TestMethod]
public void Path_RootDirectoryOnly()
{
var path = "/";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual);
}
[TestMethod]
public void Path_FileInNonRootDirectory()
{
var path = "/home/sshnet/xyz";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/home/sshnet", actual);
}
[TestMethod]
public void Path_BackslashIsNotConsideredDirectorySeparator()
{
var path = "/home\\abc.log";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual);
}
[TestMethod]
public void Path_ColonIsNotConsideredPathSeparator()
{
var path = "/home:abc.log";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual);
}
[TestMethod]
public void Path_LeadingWhitespace()
{
var path = " / \tabc";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual(" ", actual);
}
[TestMethod]
public void Path_TrailingWhitespace()
{
var path = "/abc \t ";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/", actual);
}
[TestMethod]
public void Path_OnlyWhitespace()
{
var path = " ";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual(".", actual);
}
[TestMethod]
public void Path_FileNameOnlyWhitespace()
{
var path = "/home/\t ";
var actual = PosixPath.GetDirectoryName(path);
Assert.IsNotNull(actual);
Assert.AreEqual("/home", actual);
}
}
}
@@ -17,8 +17,10 @@ namespace Renci.SshNet.Tests.Classes.Common
PosixPath.GetFileName(path);
Assert.Fail();
}
catch (NullReferenceException)
catch (ArgumentNullException ex)
{
Assert.IsNull(ex.InnerException);
Assert.AreEqual("path", ex.ParamName);
}
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -25,7 +24,7 @@ namespace Renci.SshNet.Tests.Classes
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
_directoryInfo = new DirectoryInfo("source");
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
_path = "/home/sshnet/" + random.Next().ToString();
_transformedPath = random.Next().ToString();
_uploadingRegister = new List<ScpUploadEventArgs>();
}
@@ -48,7 +47,7 @@ namespace Renci.SshNet.Tests.Classes
.Setup(p => p.Transform(_path))
.Returns(_transformedPath);
_channelSessionMock.InSequence(sequence)
.Setup(p => p.SendExecRequest(string.Format("scp -rt {0}", _transformedPath)))
.Setup(p => p.SendExecRequest(string.Format("scp -r -p -d -t {0}", _transformedPath)))
.Returns(false);
_channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
_pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
@@ -87,7 +86,7 @@ namespace Renci.SshNet.Tests.Classes
[TestMethod]
public void SendExecREquestOnChannelSessionShouldBeInvokedOnce()
{
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -rt {0}", _transformedPath)), Times.Once);
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -r -p -d -t {0}", _transformedPath)), Times.Once);
}
[TestMethod]
@@ -13,7 +13,9 @@ namespace Renci.SshNet.Tests.Classes
private ConnectionInfo _connectionInfo;
private ScpClient _scpClient;
private FileInfo _fileInfo;
private string _path;
private string _remoteDirectory;
private string _remoteFile;
private string _remotePath;
private string _transformedPath;
private string _fileName;
private IList<ScpUploadEventArgs> _uploadingRegister;
@@ -36,7 +38,9 @@ namespace Renci.SshNet.Tests.Classes
_fileName = CreateTemporaryFile(new byte[] { 1 });
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
_fileInfo = new FileInfo(_fileName);
_path = "/home/sshnet/" + random.Next();
_remoteDirectory = "/home/sshnet";
_remoteFile = random.Next().ToString();
_remotePath = _remoteDirectory + "/" + _remoteFile;
_transformedPath = random.Next().ToString();
_uploadingRegister = new List<ScpUploadEventArgs>();
}
@@ -56,10 +60,10 @@ namespace Renci.SshNet.Tests.Classes
_sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
_channelSessionMock.InSequence(sequence).Setup(p => p.Open());
_remotePathTransformationMock.InSequence(sequence)
.Setup(p => p.Transform(_path))
.Setup(p => p.Transform(_remoteDirectory))
.Returns(_transformedPath);
_channelSessionMock.InSequence(sequence)
.Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
.Setup(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)))
.Returns(false);
_channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
_pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
@@ -78,7 +82,7 @@ namespace Renci.SshNet.Tests.Classes
{
try
{
_scpClient.Upload(_fileInfo, _path);
_scpClient.Upload(_fileInfo, _remotePath);
Assert.Fail();
}
catch (SshException ex)
@@ -98,7 +102,7 @@ namespace Renci.SshNet.Tests.Classes
[TestMethod]
public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
{
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)), Times.Once);
}
[TestMethod]
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@@ -16,7 +15,9 @@ namespace Renci.SshNet.Tests.Classes
private ConnectionInfo _connectionInfo;
private ScpClient _scpClient;
private FileInfo _fileInfo;
private string _path;
private string _remoteDirectory;
private string _remoteFile;
private string _remotePath;
private string _transformedPath;
private int _bufferSize;
private byte[] _fileContent;
@@ -44,7 +45,9 @@ namespace Renci.SshNet.Tests.Classes
_fileName = CreateTemporaryFile(_fileContent);
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
_fileInfo = new FileInfo(_fileName);
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
_remoteDirectory = "/home/sshnet";
_remoteFile = random.Next().ToString();
_remotePath = _remoteDirectory + "/" + _remoteFile;
_transformedPath = random.Next().ToString();
_uploadingRegister = new List<ScpUploadEventArgs>();
}
@@ -64,17 +67,18 @@ namespace Renci.SshNet.Tests.Classes
_sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
_channelSessionMock.InSequence(sequence).Setup(p => p.Open());
_remotePathTransformationMock.InSequence(sequence)
.Setup(p => p.Transform(_path))
.Setup(p => p.Transform(_remoteDirectory))
.Returns(_transformedPath);
_channelSessionMock.InSequence(sequence)
.Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
.Setup(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)))
.Returns(true);
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
_channelSessionMock.InSequence(sequence).Setup(p => p.SendData(It.IsAny<byte[]>()));
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
_channelSessionMock.InSequence(sequence)
.Setup(p => p.SendData(It.Is<byte[]>(b => b.SequenceEqual(CreateData(
string.Format("C0644 {0} {1}\n", _fileInfo.Length, string.Empty))))));
.Setup(p => p.SendData(It.Is<byte[]>(b => b.SequenceEqual(
CreateData(string.Format("C0644 {0} {1}\n", _fileInfo.Length, _remoteFile),
_connectionInfo.Encoding)))));
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
_channelSessionMock.InSequence(sequence)
.Setup(
@@ -104,13 +108,13 @@ namespace Renci.SshNet.Tests.Classes
protected override void Act()
{
_scpClient.Upload(_fileInfo, _path);
_scpClient.Upload(_fileInfo, _remotePath);
}
[TestMethod]
public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
{
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)), Times.Once);
}
[TestMethod]
@@ -143,9 +147,9 @@ namespace Renci.SshNet.Tests.Classes
Assert.AreEqual(_fileSize, uploading.Uploaded);
}
private static IEnumerable<byte> CreateData(string command)
private static IEnumerable<byte> CreateData(string command, Encoding encoding)
{
return Encoding.Default.GetBytes(command);
return encoding.GetBytes(command);
}
private static byte[] CreateContent(int length)
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -14,7 +13,9 @@ namespace Renci.SshNet.Tests.Classes
private ConnectionInfo _connectionInfo;
private ScpClient _scpClient;
private Stream _source;
private string _path;
private string _remoteDirectory;
private string _remoteFile;
private string _remotePath;
private string _transformedPath;
private IList<ScpUploadEventArgs> _uploadingRegister;
private SshException _actualException;
@@ -34,7 +35,9 @@ namespace Renci.SshNet.Tests.Classes
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
_source = new MemoryStream();
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
_remoteDirectory = "/home/sshnet";
_remoteFile = random.Next().ToString();
_remotePath = _remoteDirectory + "/" + _remoteFile;
_transformedPath = random.Next().ToString();
_uploadingRegister = new List<ScpUploadEventArgs>();
}
@@ -54,10 +57,10 @@ namespace Renci.SshNet.Tests.Classes
_sessionMock.InSequence(sequence).Setup(p => p.CreateChannelSession()).Returns(_channelSessionMock.Object);
_channelSessionMock.InSequence(sequence).Setup(p => p.Open());
_remotePathTransformationMock.InSequence(sequence)
.Setup(p => p.Transform(_path))
.Setup(p => p.Transform(_remoteDirectory))
.Returns(_transformedPath);
_channelSessionMock.InSequence(sequence)
.Setup(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)))
.Setup(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)))
.Returns(false);
_channelSessionMock.InSequence(sequence).Setup(p => p.Dispose());
_pipeStreamMock.As<IDisposable>().InSequence(sequence).Setup(p => p.Dispose());
@@ -76,7 +79,7 @@ namespace Renci.SshNet.Tests.Classes
{
try
{
_scpClient.Upload(_source, _path);
_scpClient.Upload(_source, _remotePath);
Assert.Fail();
}
catch (SshException ex)
@@ -96,7 +99,7 @@ namespace Renci.SshNet.Tests.Classes
[TestMethod]
public void SendExecRequestOnChannelSessionShouldBeInvokedOnce()
{
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t {0}", _transformedPath)), Times.Once);
_channelSessionMock.Verify(p => p.SendExecRequest(string.Format("scp -t -d {0}", _transformedPath)), Times.Once);
}
[TestMethod]
@@ -173,6 +173,8 @@
<Compile Include="Classes\Common\ExtensionsTest_Pad.cs" />
<Compile Include="Classes\Common\ExtensionsTest_TrimLeadingZeros.cs" />
<Compile Include="Classes\Common\PackTest.cs" />
<Compile Include="Classes\Common\PosixPathTest_CreateAbsoluteOrRelativeFilePath.cs" />
<Compile Include="Classes\Common\PosixPathTest_GetDirectoryName.cs" />
<Compile Include="Classes\Common\PosixPathTest_GetFileName.cs" />
<Compile Include="Classes\ConnectionInfoTest_Authenticate_Failure.cs" />
<Compile Include="Classes\ConnectionInfoTest_Authenticate_Success.cs" />
@@ -731,4 +733,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
+71 -1
View File
@@ -4,6 +4,49 @@ namespace Renci.SshNet.Common
{
internal class PosixPath
{
public string Directory { get; private set; }
public string File { get; private set; }
public static PosixPath CreateAbsoluteOrRelativeFilePath(string path)
{
if (path == null)
{
throw new ArgumentNullException("path");
}
var posixPath = new PosixPath();
var pathEnd = path.LastIndexOf('/');
if (pathEnd == -1)
{
if (path.Length == 0)
{
throw new ArgumentException("The path is a zero-length string.", "path");
}
posixPath.Directory = ".";
posixPath.File = path;
}
else if (pathEnd == 0)
{
posixPath.Directory = "/";
if (path.Length > 1)
{
posixPath.File = path.Substring(pathEnd + 1);
}
}
else
{
posixPath.Directory = path.Substring(0, pathEnd);
if (pathEnd < path.Length - 1)
{
posixPath.File = path.Substring(pathEnd + 1);
}
}
return posixPath;
}
/// <summary>
/// Gets the file name part of a given POSIX path.
/// </summary>
@@ -18,11 +61,14 @@ namespace Renci.SshNet.Common
/// is returned.
/// </para>
/// <para>
/// If path has a trailing slash, but <see cref="GetFileName(string)"/> return a zero-length string.
/// If path has a trailing slash, <see cref="GetFileName(string)"/> return a zero-length string.
/// </para>
/// </remarks>
public static string GetFileName(string path)
{
if (path == null)
throw new ArgumentNullException("path");
var pathEnd = path.LastIndexOf('/');
if (pathEnd == -1)
return path;
@@ -30,5 +76,29 @@ namespace Renci.SshNet.Common
return string.Empty;
return path.Substring(pathEnd + 1);
}
/// <summary>
/// Gets the directory name part of a given POSIX path.
/// </summary>
/// <param name="path">The POSIX path to get the directory name for.</param>
/// <returns>
/// The directory part of the specified <paramref name="path"/>, or <c>.</c> if <paramref name="path"/>
/// does not contain any directory information.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
public static string GetDirectoryName(string path)
{
if (path == null)
throw new ArgumentNullException("path");
var pathEnd = path.LastIndexOf('/');
if (pathEnd == -1)
return ".";
if (pathEnd == 0)
return "/";
if (pathEnd == path.Length - 1)
return path.Substring(0, pathEnd);
return path.Substring(0, pathEnd);
}
}
}
+57 -14
View File
@@ -20,15 +20,16 @@ namespace Renci.SshNet
/// <param name="fileInfo">The file system info.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Upload(FileInfo fileInfo, string path)
{
if (fileInfo == null)
throw new ArgumentNullException("fileInfo");
if (string.IsNullOrEmpty(path))
throw new ArgumentException("path");
var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
@@ -36,7 +37,9 @@ namespace Renci.SshNet
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();
if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
// Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
// that we expect the target to be a directory.
if (!channel.SendExecRequest(string.Format("scp -t -d {0}", _remotePathTransformation.Transform(posixPath.Directory))))
{
throw SecureExecutionRequestRejectedException();
}
@@ -45,7 +48,7 @@ namespace Renci.SshNet
using (var source = fileInfo.OpenRead())
{
UploadTimes(channel, input, fileInfo);
UploadFileModeAndName(channel, input, source.Length, string.Empty);
UploadFileModeAndName(channel, input, source.Length, posixPath.File);
UploadFileContent(channel, input, source, fileInfo.Name);
}
}
@@ -56,16 +59,19 @@ namespace Renci.SshNet
/// </summary>
/// <param name="directoryInfo">The directory info.</param>
/// <param name="path">A relative or absolute path for the remote directory.</param>
/// <exception cref="ArgumentNullException">fileSystemInfo</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is <c>null</c> or empty.</exception>
/// <exception cref="ScpException"><paramref name="path"/> exists on the remote host, and is not a directory.</exception>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length string.</exception>
/// <exception cref="ScpException"><paramref name="path"/> does not exist on the remote host, is not a directory or the user does not have the required permission.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Upload(DirectoryInfo directoryInfo, string path)
{
if (directoryInfo == null)
throw new ArgumentNullException("directoryInfo");
if (string.IsNullOrEmpty(path))
throw new ArgumentException("path");
if (path == null)
throw new ArgumentNullException("path");
if (path.Length == 0)
throw new ArgumentException("The path cannot be a zero-length string.", "path");
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
@@ -73,15 +79,18 @@ namespace Renci.SshNet
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();
// start recursive upload
if (!channel.SendExecRequest(string.Format("scp -rt {0}", _remotePathTransformation.Transform(path))))
// start copy with the following options:
// -p preserve modification and access times
// -r copy directories recursively
// -d expect path to be a directory
// -t copy to remote
if (!channel.SendExecRequest(string.Format("scp -r -p -d -t {0}", _remotePathTransformation.Transform(path))))
{
throw SecureExecutionRequestRejectedException();
}
CheckReturnCode(input);
UploadTimes(channel, input, directoryInfo);
UploadDirectoryModeAndName(channel, input, ".");
UploadDirectoryContent(channel, input, directoryInfo);
}
}
@@ -311,5 +320,39 @@ namespace Renci.SshNet
SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
}
}
/// <summary>
/// Return a value indicating whether the specified path is a valid SCP file path.
/// </summary>
/// <param name="path">The path to verify.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="path"/> is a valid SCP file path; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// To match OpenSSH behavior (introduced as a result of CVE-2018-20685), a file path is considered
/// invalid in any of the following conditions:
/// <list type="bullet">
/// <item>
/// <description><paramref name="path"/> is a zero-length string.</description>
/// </item>
/// <item>
/// <description><paramref name="path"/> is &quot;<c>.</c>&quot;.</description>
/// </item>
/// <item>
/// <description><paramref name="path"/> is &quot;<c>..</c>&quot;.</description>
/// </item>
/// <item>
/// <description><paramref name="path"/> contains a forward slash (/).</description>
/// </item>
/// </list>
/// </remarks>
private static bool IsValidScpFilePath(string path)
{
return path != null &&
path.Length != 0 &&
path != "." &&
path != ".." &&
path.IndexOf('/') == -1;
}
}
}
+9 -7
View File
@@ -202,28 +202,30 @@ namespace Renci.SshNet
/// </summary>
/// <param name="source">The <see cref="Stream"/> to upload.</param>
/// <param name="path">A relative or absolute path for the remote file.</param>
/// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
/// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
public void Upload(Stream source, string path)
{
var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();
// pass the full path to ensure the server does not create the directory part
// as a file in case the directory does not exist
if (!channel.SendExecRequest(string.Format("scp -t {0}", _remotePathTransformation.Transform(path))))
// Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
// that we expect the target to be a directory.
if (!channel.SendExecRequest(string.Format("scp -t -d {0}", _remotePathTransformation.Transform(posixPath.Directory))))
{
throw SecureExecutionRequestRejectedException();
}
CheckReturnCode(input);
// specify a zero-length file name to avoid creating a file with absolute
// path '<path>/<filename part of path>' if directory '<path>' already exists
UploadFileModeAndName(channel, input, source.Length, string.Empty);
UploadFileContent(channel, input, source, PosixPath.GetFileName(path));
UploadFileModeAndName(channel, input, source.Length, posixPath.File);
UploadFileContent(channel, input, source, posixPath.File);
}
}