diff --git a/src/References/How the SCP protocol works.pdf b/src/References/How the SCP protocol works.pdf new file mode 100644 index 00000000..74728daa Binary files /dev/null and b/src/References/How the SCP protocol works.pdf differ diff --git a/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_CreateAbsoluteOrRelativeFilePath.cs b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_CreateAbsoluteOrRelativeFilePath.cs new file mode 100644 index 00000000..986ccdea --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_CreateAbsoluteOrRelativeFilePath.cs @@ -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); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetDirectoryName.cs b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetDirectoryName.cs new file mode 100644 index 00000000..db1a38e7 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetDirectoryName.cs @@ -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); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs index d45f115f..3c7d74e2 100644 --- a/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs +++ b/src/Renci.SshNet.Tests/Classes/Common/PosixPathTest_GetFileName.cs @@ -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); } } diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs index fc9b75b6..1c47aeae 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_DirectoryInfoAndPath_SendExecRequestReturnsFalse.cs @@ -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(); } @@ -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().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] diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs index a3d1ca09..0c842a21 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs @@ -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 _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(); } @@ -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().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] diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs index a6ac4a11..c04525e0 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs @@ -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(); } @@ -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())); _pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0); _channelSessionMock.InSequence(sequence) - .Setup(p => p.SendData(It.Is(b => b.SequenceEqual(CreateData( - string.Format("C0644 {0} {1}\n", _fileInfo.Length, string.Empty)))))); + .Setup(p => p.SendData(It.Is(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 CreateData(string command) + private static IEnumerable CreateData(string command, Encoding encoding) { - return Encoding.Default.GetBytes(command); + return encoding.GetBytes(command); } private static byte[] CreateContent(int length) diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs index b03a4484..d2c6992b 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_StreamAndPath_SendExecRequestReturnsFalse.cs @@ -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 _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(); } @@ -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().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] diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 5b9ccafc..043c7977 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -173,6 +173,8 @@ + + @@ -731,4 +733,4 @@ --> - + \ No newline at end of file diff --git a/src/Renci.SshNet/Common/PosixPath.cs b/src/Renci.SshNet/Common/PosixPath.cs index f9c099b8..465916b3 100644 --- a/src/Renci.SshNet/Common/PosixPath.cs +++ b/src/Renci.SshNet/Common/PosixPath.cs @@ -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; + } + /// /// Gets the file name part of a given POSIX path. /// @@ -18,11 +61,14 @@ namespace Renci.SshNet.Common /// is returned. /// /// - /// If path has a trailing slash, but return a zero-length string. + /// If path has a trailing slash, return a zero-length string. /// /// 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); } + + /// + /// Gets the directory name part of a given POSIX path. + /// + /// The POSIX path to get the directory name for. + /// + /// The directory part of the specified , or . if + /// does not contain any directory information. + /// + /// is null. + 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); + } } } diff --git a/src/Renci.SshNet/ScpClient.NET.cs b/src/Renci.SshNet/ScpClient.NET.cs index 4665dabf..cb23cb69 100644 --- a/src/Renci.SshNet/ScpClient.NET.cs +++ b/src/Renci.SshNet/ScpClient.NET.cs @@ -20,15 +20,16 @@ namespace Renci.SshNet /// The file system info. /// A relative or absolute path for the remote file. /// is null. - /// is null or empty. + /// is null. + /// is a zero-length . /// A directory with the specified path exists on the remote host. /// The secure copy execution request was rejected by the server. 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 /// /// The directory info. /// A relative or absolute path for the remote directory. - /// fileSystemInfo - /// is null or empty. - /// exists on the remote host, and is not a directory. + /// is null. + /// is null. + /// is a zero-length string. + /// does not exist on the remote host, is not a directory or the user does not have the required permission. /// The secure copy execution request was rejected by the server. 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)); } } + + /// + /// Return a value indicating whether the specified path is a valid SCP file path. + /// + /// The path to verify. + /// + /// if is a valid SCP file path; otherwise, . + /// + /// + /// To match OpenSSH behavior (introduced as a result of CVE-2018-20685), a file path is considered + /// invalid in any of the following conditions: + /// + /// + /// is a zero-length string. + /// + /// + /// is ".". + /// + /// + /// is "..". + /// + /// + /// contains a forward slash (/). + /// + /// + /// + private static bool IsValidScpFilePath(string path) + { + return path != null && + path.Length != 0 && + path != "." && + path != ".." && + path.IndexOf('/') == -1; + } } } diff --git a/src/Renci.SshNet/ScpClient.cs b/src/Renci.SshNet/ScpClient.cs index 8fb23ebc..a30107e2 100644 --- a/src/Renci.SshNet/ScpClient.cs +++ b/src/Renci.SshNet/ScpClient.cs @@ -202,28 +202,30 @@ namespace Renci.SshNet /// /// The to upload. /// A relative or absolute path for the remote file. + /// is null. + /// is a zero-length . /// A directory with the specified path exists on the remote host. /// The secure copy execution request was rejected by the server. 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 '/' if directory '' 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); } }