Pass full remote file path to scp command when uploading or downloading a file, and use zero-length pass in C directive. Combined, this ensure the SCP server will reject a remote file path that points to a directory.

Fixes issue #286.
This commit is contained in:
Gert Driesen
2017-08-27 08:59:26 +02:00
parent 2d840aab35
commit b70e21801e
7 changed files with 151 additions and 92 deletions
@@ -197,6 +197,9 @@
<Compile Include="..\Renci.SshNet\Common\PortForwardEventArgs.cs">
<Link>Common\PortForwardEventArgs.cs</Link>
</Compile>
<Compile Include="..\Renci.SshNet\Common\PosixPath.cs">
<Link>Common\PosixPath.cs</Link>
</Compile>
<Compile Include="..\Renci.SshNet\Common\ProxyException.cs">
<Link>Common\ProxyException.cs</Link>
</Compile>
@@ -953,7 +956,7 @@
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ProjectExtensions>
<VisualStudio>
<UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
<UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
</VisualStudio>
</ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
@@ -48,7 +48,7 @@ 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 = random.Next().ToString(CultureInfo.InvariantCulture);
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
_quotedPath = _path.ShellQuote();
_uploadingRegister = new List<ScpUploadEventArgs>();
@@ -56,7 +56,7 @@ namespace Renci.SshNet.Tests.Classes
_fileName = CreateTemporaryFile(_fileContent);
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
_fileInfo = new FileInfo(_fileName);
_path = random.Next().ToString(CultureInfo.InvariantCulture);
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
_quotedPath = _path.ShellQuote();
_uploadingRegister = new List<ScpUploadEventArgs>();
}
@@ -86,11 +86,7 @@ namespace Renci.SshNet.Tests.Classes
_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,
Path.GetFileName(_fileName)
)
)))));
string.Format("C0644 {0} {1}\n", _fileInfo.Length, string.Empty))))));
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
_channelSessionMock.InSequence(sequence)
.Setup(
+24
View File
@@ -0,0 +1,24 @@
namespace Renci.SshNet.Common
{
internal class PosixPath
{
/// <summary>
/// Gets the file name part of a given POSIX path.
/// </summary>
/// <param name="path">The POSIX path to get the file name for.</param>
/// <returns>
/// The file name part of <paramref name="path"/>.
/// </returns>
/// <remarks>
/// If <paramref name="path"/> contains no forward slash or has a trailing
/// forward slash, then <paramref name="path"/> is returned.
/// </remarks>
public static string GetFileName(string path)
{
var pathEnd = path.LastIndexOf('/');
if (pathEnd == -1 || pathEnd == path.Length - 1)
return path;
return path.Substring(pathEnd + 1);
}
}
}
+1
View File
@@ -97,6 +97,7 @@
<Compile Include="Common\ChannelRequestEventArgs.cs" />
<Compile Include="Common\CountdownEvent.cs" />
<Compile Include="Common\Pack.cs" />
<Compile Include="Common\PosixPath.cs" />
<Compile Include="Common\ProxyException.cs">
<SubType>Code</SubType>
</Compile>
+49 -47
View File
@@ -18,9 +18,10 @@ namespace Renci.SshNet
/// Uploads the specified file to the remote host.
/// </summary>
/// <param name="fileInfo">The file system info.</param>
/// <param name="path">The path.</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="ScpException">A directory with the specified path exists on the remote host.</exception>
public void Upload(FileInfo fileInfo, string path)
{
if (fileInfo == null)
@@ -36,10 +37,14 @@ namespace Renci.SshNet
if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
CheckReturnCode(input);
InternalUpload(channel, input, fileInfo);
using (var source = fileInfo.OpenRead())
{
UploadTimes(channel, input, fileInfo);
UploadFileModeAndName(channel, input, source.Length, string.Empty);
UploadFileContent(channel, input, source, fileInfo.Name);
}
}
}
@@ -47,9 +52,10 @@ namespace Renci.SshNet
/// Uploads the specified directory to the remote host.
/// </summary>
/// <param name="directoryInfo">The directory info.</param>
/// <param name="path">The path.</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>
public void Upload(DirectoryInfo directoryInfo, string path)
{
if (directoryInfo == null)
@@ -67,17 +73,9 @@ namespace Renci.SshNet
channel.SendExecRequest(string.Format("scp -rt {0}", path.ShellQuote()));
CheckReturnCode(input);
// set last write and last access time on specified remote path
InternalSetTimestamp(channel, input, directoryInfo.LastWriteTimeUtc, directoryInfo.LastAccessTimeUtc);
SendData(channel, string.Format("D0755 0 {0}\n", "."));
CheckReturnCode(input);
// recursively upload files and directories in specified remote path
InternalUpload(channel, input, directoryInfo);
// terminate upload of specified remote path
SendData(channel, "E\n");
CheckReturnCode(input);
UploadTimes(channel, input, directoryInfo);
UploadDirectoryModeAndName(channel, input, ".");
UploadDirectoryContent(channel, input, directoryInfo);
}
}
@@ -88,6 +86,7 @@ namespace Renci.SshNet
/// <param name="fileInfo">Local file information.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or empty.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
public void Download(string filename, FileInfo fileInfo)
{
if (string.IsNullOrEmpty(filename))
@@ -104,7 +103,7 @@ namespace Renci.SshNet
// Send channel command request
channel.SendExecRequest(string.Format("scp -pf {0}", filename.ShellQuote()));
// Send reply
SendConfirmation(channel);
SendSuccessConfirmation(channel);
InternalDownload(channel, input, fileInfo);
}
@@ -117,6 +116,7 @@ namespace Renci.SshNet
/// <param name="directoryInfo">Local directory information.</param>
/// <exception cref="ArgumentException"><paramref name="directoryName"/> is <c>null</c> or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <c>null</c>.</exception>
/// <exception cref="ScpException">File or directory with the specified path does not exist on the remote host.</exception>
public void Download(string directoryName, DirectoryInfo directoryInfo)
{
if (string.IsNullOrEmpty(directoryName))
@@ -133,51 +133,53 @@ namespace Renci.SshNet
// Send channel command request
channel.SendExecRequest(string.Format("scp -prf {0}", directoryName.ShellQuote()));
// Send reply
SendConfirmation(channel);
SendSuccessConfirmation(channel);
InternalDownload(channel, input, directoryInfo);
}
}
/// <summary>
/// Uploads the file in the active directory context, and set
/// Upload the files and subdirectories in the specified directory.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="fileInfo">The file to upload.</param>
private void InternalUpload(IChannelSession channel, Stream input, FileInfo fileInfo)
{
using (var source = fileInfo.OpenRead())
{
// set the last write and last access time for the next file uploaded
InternalSetTimestamp(channel, input, fileInfo.LastWriteTimeUtc, fileInfo.LastAccessTimeUtc);
// upload the actual file
InternalUpload(channel, input, source, fileInfo.Name);
}
}
private void InternalUpload(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
/// <param name="directoryInfo">The directory to upload.</param>
private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
{
// Upload files
var files = directoryInfo.GetFiles();
foreach (var file in files)
{
InternalUpload(channel, input, file);
using (var source = file.OpenRead())
{
UploadTimes(channel, input, file);
UploadFileModeAndName(channel, input, source.Length, file.Name);
UploadFileContent(channel, input, source, file.Name);
}
}
// Upload directories
var directories = directoryInfo.GetDirectories();
foreach (var directory in directories)
{
InternalSetTimestamp(channel, input, directory.LastWriteTimeUtc, directory.LastAccessTimeUtc);
SendData(channel, string.Format("D0755 0 {0}\n", directory.Name));
CheckReturnCode(input);
InternalUpload(channel, input, directory);
SendData(channel, "E\n");
CheckReturnCode(input);
UploadTimes(channel, input, directory);
UploadDirectoryModeAndName(channel, input, directory.Name);
UploadDirectoryContent(channel, input, directory);
}
// Mark upload of current directory complete
SendData(channel, "E\n");
CheckReturnCode(input);
}
/// <summary>
/// Sets mode and name of the directory being upload.
/// </summary>
private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName)
{
SendData(channel, string.Format("D0755 0 {0}\n", directoryName));
CheckReturnCode(input);
}
private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
@@ -195,7 +197,7 @@ namespace Renci.SshNet
if (message == "E")
{
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
directoryCounter--;
@@ -209,7 +211,7 @@ namespace Renci.SshNet
var match = DirectoryInfoRe.Match(message);
if (match.Success)
{
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
// Read directory
var filename = match.Result("${filename}");
@@ -217,7 +219,7 @@ namespace Renci.SshNet
DirectoryInfo newDirectoryInfo;
if (directoryCounter > 0)
{
newDirectoryInfo = Directory.CreateDirectory(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, filename));
newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename));
newDirectoryInfo.LastAccessTime = accessedTime;
newDirectoryInfo.LastWriteTime = modifiedTime;
}
@@ -237,7 +239,7 @@ namespace Renci.SshNet
if (match.Success)
{
// Read file
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");
@@ -245,7 +247,7 @@ namespace Renci.SshNet
var fileInfo = fileSystemInfo as FileInfo;
if (fileInfo == null)
fileInfo = new FileInfo(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, fileName));
fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName));
using (var output = fileInfo.OpenWrite())
{
@@ -264,7 +266,7 @@ namespace Renci.SshNet
if (match.Success)
{
// Read timestamp
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
var mtime = long.Parse(match.Result("${mtime}"));
var atime = long.Parse(match.Result("${atime}"));
@@ -275,7 +277,7 @@ namespace Renci.SshNet
continue;
}
SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
}
}
}
+70 -37
View File
@@ -22,7 +22,7 @@ namespace Renci.SshNet
/// <list type="bullet">
/// <item>
/// <description>Recursive download (-prf) does not deal well with specific UTF-8 and newline characters.</description>
/// <description>Recursive update does not support empty path for uploading to home directorydeal well with specific UTF-8 and newline characters.</description>
/// <description>Recursive update does not support empty path for uploading to home directory.</description>
/// </item>
/// </list>
/// </para>
@@ -30,6 +30,8 @@ namespace Renci.SshNet
public partial class ScpClient : BaseClient
{
private static readonly Regex FileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
private static readonly byte[] SuccessConfirmationCode = {0};
private static readonly byte[] ErrorConfirmationCode = { 1 };
/// <summary>
/// Gets or sets the operation timeout.
@@ -167,8 +169,9 @@ namespace Renci.SshNet
/// <summary>
/// Uploads the specified stream to the remote host.
/// </summary>
/// <param name="source">Stream to upload.</param>
/// <param name="path">Remote host file name.</param>
/// <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="ScpException">A directory with the specified path exists on the remote host.</exception>
public void Upload(Stream source, string path)
{
using (var input = ServiceFactory.CreatePipeStream())
@@ -177,30 +180,27 @@ namespace Renci.SshNet
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.Open();
var pathEnd = path.LastIndexOfAny(new[] { '\\', '/' });
if (pathEnd != -1)
{
// split the path from the file
var pathOnly = path.Substring(0, pathEnd);
var fileOnly = path.Substring(pathEnd + 1);
// Send channel command request
channel.SendExecRequest(string.Format("scp -t \"{0}\"", pathOnly));
CheckReturnCode(input);
// 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}", path.ShellQuote())))
return;
CheckReturnCode(input);
path = fileOnly;
}
InternalUpload(channel, input, source, path);
// 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));
}
}
/// <summary>
/// Downloads the specified file from the remote host to the stream.
/// </summary>
/// <param name="filename">Remote host file name.</param>
/// <param name="destination">The stream where to download remote file.</param>
/// <param name="filename">A relative or absolute path for the remote file.</param>
/// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or contains only whitespace characters.</exception>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <c>null</c>.</exception>
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
/// <remarks>
/// Method calls made by this method to <paramref name="destination"/>, may under certain conditions result
/// in exceptions thrown by the stream.
@@ -221,7 +221,7 @@ namespace Renci.SshNet
// Send channel command request
channel.SendExecRequest(string.Format("scp -f {0}", filename.ShellQuote()));
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
var message = ReadString(input);
var match = FileInfoRe.Match(message);
@@ -229,7 +229,7 @@ namespace Renci.SshNet
if (match.Success)
{
// Read file
SendConfirmation(channel); // Send reply
SendSuccessConfirmation(channel); // Send reply
var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");
@@ -238,28 +238,61 @@ namespace Renci.SshNet
}
else
{
SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
}
}
}
private void InternalSetTimestamp(IChannelSession channel, Stream input, DateTime lastWriteTime, DateTime lastAccessime)
/// <summary>
/// Uploads the <see cref="FileSystemInfo.LastWriteTimeUtc"/> and <see cref="FileSystemInfo.LastAccessTimeUtc"/>
/// of the next file or directory to upload.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="fileOrDirectory">The file or directory to upload.</param>
private void UploadTimes(IChannelSession channel, Stream input, FileSystemInfo fileOrDirectory)
{
var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var modificationSeconds = (long) (lastWriteTime - zeroTime).TotalSeconds;
var accessSeconds = (long) (lastAccessime - zeroTime).TotalSeconds;
var modificationSeconds = (long) (fileOrDirectory.LastWriteTimeUtc - zeroTime).TotalSeconds;
var accessSeconds = (long) (fileOrDirectory.LastAccessTimeUtc - zeroTime).TotalSeconds;
SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
CheckReturnCode(input);
}
private void InternalUpload(IChannelSession channel, Stream input, Stream source, string filename)
/// <summary>
/// Sets mode, size and name of file being upload.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="fileSize">The size of the content to upload.</param>
/// <param name="serverFileName">The name of the file, without path, to which the content is to be uploaded.</param>
/// <remarks>
/// <para>
/// When the SCP transfer is already initiated for a file, a zero-length <see cref="string"/> should
/// be specified for <paramref name="serverFileName"/>. This prevents the server from uploading the
/// content to a file with path <c>&lt;file path&gt;/<paramref name="serverFileName"/></c> if there's
/// already a directory with this path, and allows us to receive an error response.
/// </para>
/// </remarks>
private void UploadFileModeAndName(IChannelSession channel, Stream input, long fileSize, string serverFileName)
{
var length = source.Length;
// specify permissions, length and name of file being upload
SendData(channel, string.Format("C0644 {0} {1}\n", length, filename));
SendData(channel, string.Format("C0644 {0} {1}\n", fileSize, serverFileName));
CheckReturnCode(input);
}
/// <summary>
/// Uploads the content of a file.
/// </summary>
/// <param name="channel">The channel to perform the upload in.</param>
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
/// <param name="source">The content to upload.</param>
/// <param name="remoteFileName">The name of the remote file, without path, to which the content is uploaded.</param>
/// <remarks>
/// <paramref name="remoteFileName"/> is only used for raising the <see cref="Uploading"/> event.
/// </remarks>
private void UploadFileContent(IChannelSession channel, Stream input, Stream source, string remoteFileName)
{
var totalLength = source.Length;
var buffer = new byte[BufferSize];
var read = source.Read(buffer, 0, buffer.Length);
@@ -272,12 +305,12 @@ namespace Renci.SshNet
totalRead += read;
RaiseUploadingEvent(filename, length, totalRead);
RaiseUploadingEvent(remoteFileName, totalLength, totalRead);
read = source.Read(buffer, 0, buffer.Length);
}
SendConfirmation(channel);
SendSuccessConfirmation(channel);
CheckReturnCode(input);
}
@@ -304,7 +337,7 @@ namespace Renci.SshNet
RaiseDownloadingEvent(filename, length, length - needToRead);
// Send confirmation byte after last data byte was read
SendConfirmation(channel);
SendSuccessConfirmation(channel);
CheckReturnCode(input);
}
@@ -325,15 +358,15 @@ namespace Renci.SshNet
}
}
private static void SendConfirmation(IChannel channel)
private static void SendSuccessConfirmation(IChannel channel)
{
SendData(channel, new byte[] { 0 });
SendData(channel, SuccessConfirmationCode);
}
private void SendConfirmation(IChannel channel, byte errorCode, string message)
private void SendErrorConfirmation(IChannel channel, string message)
{
SendData(channel, new[] { errorCode });
SendData(channel, string.Format("{0}\n", message));
SendData(channel, ErrorConfirmationCode);
SendData(channel, string.Concat(message, "\n"));
}
/// <summary>