Skip to content

Commit b70e218

Browse files
committed
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.
1 parent 2d840aa commit b70e218

7 files changed

+150
-91
lines changed

src/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj

+4-1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@
197197
<Compile Include="..\Renci.SshNet\Common\PortForwardEventArgs.cs">
198198
<Link>Common\PortForwardEventArgs.cs</Link>
199199
</Compile>
200+
<Compile Include="..\Renci.SshNet\Common\PosixPath.cs">
201+
<Link>Common\PosixPath.cs</Link>
202+
</Compile>
200203
<Compile Include="..\Renci.SshNet\Common\ProxyException.cs">
201204
<Link>Common\ProxyException.cs</Link>
202205
</Compile>
@@ -953,7 +956,7 @@
953956
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
954957
<ProjectExtensions>
955958
<VisualStudio>
956-
<UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
959+
<UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
957960
</VisualStudio>
958961
</ProjectExtensions>
959962
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_SendExecRequestReturnsFalse.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ protected void Arrange()
4848
_fileName = CreateTemporaryFile(new byte[] {1});
4949
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
5050
_fileInfo = new FileInfo(_fileName);
51-
_path = random.Next().ToString(CultureInfo.InvariantCulture);
51+
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
5252
_quotedPath = _path.ShellQuote();
5353
_uploadingRegister = new List<ScpUploadEventArgs>();
5454

src/Renci.SshNet.Tests/Classes/ScpClientTest_Upload_FileInfoAndPath_Success.cs

+2-6
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private void SetupData()
5656
_fileName = CreateTemporaryFile(_fileContent);
5757
_connectionInfo = new ConnectionInfo("host", 22, "user", new PasswordAuthenticationMethod("user", "pwd"));
5858
_fileInfo = new FileInfo(_fileName);
59-
_path = random.Next().ToString(CultureInfo.InvariantCulture);
59+
_path = "/home/sshnet/" + random.Next().ToString(CultureInfo.InvariantCulture);
6060
_quotedPath = _path.ShellQuote();
6161
_uploadingRegister = new List<ScpUploadEventArgs>();
6262
}
@@ -86,11 +86,7 @@ private void SetupMocks()
8686
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
8787
_channelSessionMock.InSequence(sequence)
8888
.Setup(p => p.SendData(It.Is<byte[]>(b => b.SequenceEqual(CreateData(
89-
string.Format("C0644 {0} {1}\n",
90-
_fileInfo.Length,
91-
Path.GetFileName(_fileName)
92-
)
93-
)))));
89+
string.Format("C0644 {0} {1}\n", _fileInfo.Length, string.Empty))))));
9490
_pipeStreamMock.InSequence(sequence).Setup(p => p.ReadByte()).Returns(0);
9591
_channelSessionMock.InSequence(sequence)
9692
.Setup(

src/Renci.SshNet/Common/PosixPath.cs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Renci.SshNet.Common
2+
{
3+
internal class PosixPath
4+
{
5+
/// <summary>
6+
/// Gets the file name part of a given POSIX path.
7+
/// </summary>
8+
/// <param name="path">The POSIX path to get the file name for.</param>
9+
/// <returns>
10+
/// The file name part of <paramref name="path"/>.
11+
/// </returns>
12+
/// <remarks>
13+
/// If <paramref name="path"/> contains no forward slash or has a trailing
14+
/// forward slash, then <paramref name="path"/> is returned.
15+
/// </remarks>
16+
public static string GetFileName(string path)
17+
{
18+
var pathEnd = path.LastIndexOf('/');
19+
if (pathEnd == -1 || pathEnd == path.Length - 1)
20+
return path;
21+
return path.Substring(pathEnd + 1);
22+
}
23+
}
24+
}

src/Renci.SshNet/Renci.SshNet.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
<Compile Include="Common\ChannelRequestEventArgs.cs" />
9898
<Compile Include="Common\CountdownEvent.cs" />
9999
<Compile Include="Common\Pack.cs" />
100+
<Compile Include="Common\PosixPath.cs" />
100101
<Compile Include="Common\ProxyException.cs">
101102
<SubType>Code</SubType>
102103
</Compile>

src/Renci.SshNet/ScpClient.NET.cs

+48-46
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ public partial class ScpClient
1818
/// Uploads the specified file to the remote host.
1919
/// </summary>
2020
/// <param name="fileInfo">The file system info.</param>
21-
/// <param name="path">The path.</param>
21+
/// <param name="path">A relative or absolute path for the remote file.</param>
2222
/// <exception cref="ArgumentNullException"><paramref name="fileInfo" /> is <c>null</c>.</exception>
2323
/// <exception cref="ArgumentException"><paramref name="path"/> is <c>null</c> or empty.</exception>
24+
/// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
2425
public void Upload(FileInfo fileInfo, string path)
2526
{
2627
if (fileInfo == null)
@@ -36,20 +37,25 @@ public void Upload(FileInfo fileInfo, string path)
3637

3738
if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
3839
throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
39-
4040
CheckReturnCode(input);
4141

42-
InternalUpload(channel, input, fileInfo);
42+
using (var source = fileInfo.OpenRead())
43+
{
44+
UploadTimes(channel, input, fileInfo);
45+
UploadFileModeAndName(channel, input, source.Length, string.Empty);
46+
UploadFileContent(channel, input, source, fileInfo.Name);
47+
}
4348
}
4449
}
4550

4651
/// <summary>
4752
/// Uploads the specified directory to the remote host.
4853
/// </summary>
4954
/// <param name="directoryInfo">The directory info.</param>
50-
/// <param name="path">The path.</param>
55+
/// <param name="path">A relative or absolute path for the remote directory.</param>
5156
/// <exception cref="ArgumentNullException">fileSystemInfo</exception>
5257
/// <exception cref="ArgumentException"><paramref name="path"/> is <c>null</c> or empty.</exception>
58+
/// <exception cref="ScpException"><paramref name="path"/> exists on the remote host, and is not a directory.</exception>
5359
public void Upload(DirectoryInfo directoryInfo, string path)
5460
{
5561
if (directoryInfo == null)
@@ -67,17 +73,9 @@ public void Upload(DirectoryInfo directoryInfo, string path)
6773
channel.SendExecRequest(string.Format("scp -rt {0}", path.ShellQuote()));
6874
CheckReturnCode(input);
6975

70-
// set last write and last access time on specified remote path
71-
InternalSetTimestamp(channel, input, directoryInfo.LastWriteTimeUtc, directoryInfo.LastAccessTimeUtc);
72-
SendData(channel, string.Format("D0755 0 {0}\n", "."));
73-
CheckReturnCode(input);
74-
75-
// recursively upload files and directories in specified remote path
76-
InternalUpload(channel, input, directoryInfo);
77-
78-
// terminate upload of specified remote path
79-
SendData(channel, "E\n");
80-
CheckReturnCode(input);
76+
UploadTimes(channel, input, directoryInfo);
77+
UploadDirectoryModeAndName(channel, input, ".");
78+
UploadDirectoryContent(channel, input, directoryInfo);
8179
}
8280
}
8381

@@ -88,6 +86,7 @@ public void Upload(DirectoryInfo directoryInfo, string path)
8886
/// <param name="fileInfo">Local file information.</param>
8987
/// <exception cref="ArgumentNullException"><paramref name="fileInfo"/> is <c>null</c>.</exception>
9088
/// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or empty.</exception>
89+
/// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
9190
public void Download(string filename, FileInfo fileInfo)
9291
{
9392
if (string.IsNullOrEmpty(filename))
@@ -104,7 +103,7 @@ public void Download(string filename, FileInfo fileInfo)
104103
// Send channel command request
105104
channel.SendExecRequest(string.Format("scp -pf {0}", filename.ShellQuote()));
106105
// Send reply
107-
SendConfirmation(channel);
106+
SendSuccessConfirmation(channel);
108107

109108
InternalDownload(channel, input, fileInfo);
110109
}
@@ -117,6 +116,7 @@ public void Download(string filename, FileInfo fileInfo)
117116
/// <param name="directoryInfo">Local directory information.</param>
118117
/// <exception cref="ArgumentException"><paramref name="directoryName"/> is <c>null</c> or empty.</exception>
119118
/// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <c>null</c>.</exception>
119+
/// <exception cref="ScpException">File or directory with the specified path does not exist on the remote host.</exception>
120120
public void Download(string directoryName, DirectoryInfo directoryInfo)
121121
{
122122
if (string.IsNullOrEmpty(directoryName))
@@ -133,51 +133,53 @@ public void Download(string directoryName, DirectoryInfo directoryInfo)
133133
// Send channel command request
134134
channel.SendExecRequest(string.Format("scp -prf {0}", directoryName.ShellQuote()));
135135
// Send reply
136-
SendConfirmation(channel);
136+
SendSuccessConfirmation(channel);
137137

138138
InternalDownload(channel, input, directoryInfo);
139139
}
140140
}
141141

142142
/// <summary>
143-
/// Uploads the file in the active directory context, and set
143+
/// Upload the files and subdirectories in the specified directory.
144144
/// </summary>
145145
/// <param name="channel">The channel to perform the upload in.</param>
146146
/// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
147-
/// <param name="fileInfo">The file to upload.</param>
148-
private void InternalUpload(IChannelSession channel, Stream input, FileInfo fileInfo)
149-
{
150-
using (var source = fileInfo.OpenRead())
151-
{
152-
// set the last write and last access time for the next file uploaded
153-
InternalSetTimestamp(channel, input, fileInfo.LastWriteTimeUtc, fileInfo.LastAccessTimeUtc);
154-
// upload the actual file
155-
InternalUpload(channel, input, source, fileInfo.Name);
156-
}
157-
}
158-
159-
private void InternalUpload(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
147+
/// <param name="directoryInfo">The directory to upload.</param>
148+
private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
160149
{
161150
// Upload files
162151
var files = directoryInfo.GetFiles();
163152
foreach (var file in files)
164153
{
165-
InternalUpload(channel, input, file);
154+
using (var source = file.OpenRead())
155+
{
156+
UploadTimes(channel, input, file);
157+
UploadFileModeAndName(channel, input, source.Length, file.Name);
158+
UploadFileContent(channel, input, source, file.Name);
159+
}
166160
}
167161

168162
// Upload directories
169163
var directories = directoryInfo.GetDirectories();
170164
foreach (var directory in directories)
171165
{
172-
InternalSetTimestamp(channel, input, directory.LastWriteTimeUtc, directory.LastAccessTimeUtc);
173-
SendData(channel, string.Format("D0755 0 {0}\n", directory.Name));
174-
CheckReturnCode(input);
166+
UploadTimes(channel, input, directory);
167+
UploadDirectoryModeAndName(channel, input, directory.Name);
168+
UploadDirectoryContent(channel, input, directory);
169+
}
175170

176-
InternalUpload(channel, input, directory);
171+
// Mark upload of current directory complete
172+
SendData(channel, "E\n");
173+
CheckReturnCode(input);
174+
}
177175

178-
SendData(channel, "E\n");
179-
CheckReturnCode(input);
180-
}
176+
/// <summary>
177+
/// Sets mode and name of the directory being upload.
178+
/// </summary>
179+
private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName)
180+
{
181+
SendData(channel, string.Format("D0755 0 {0}\n", directoryName));
182+
CheckReturnCode(input);
181183
}
182184

183185
private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
@@ -195,7 +197,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
195197

196198
if (message == "E")
197199
{
198-
SendConfirmation(channel); // Send reply
200+
SendSuccessConfirmation(channel); // Send reply
199201

200202
directoryCounter--;
201203

@@ -209,15 +211,15 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
209211
var match = DirectoryInfoRe.Match(message);
210212
if (match.Success)
211213
{
212-
SendConfirmation(channel); // Send reply
214+
SendSuccessConfirmation(channel); // Send reply
213215

214216
// Read directory
215217
var filename = match.Result("${filename}");
216218

217219
DirectoryInfo newDirectoryInfo;
218220
if (directoryCounter > 0)
219221
{
220-
newDirectoryInfo = Directory.CreateDirectory(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, filename));
222+
newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename));
221223
newDirectoryInfo.LastAccessTime = accessedTime;
222224
newDirectoryInfo.LastWriteTime = modifiedTime;
223225
}
@@ -237,15 +239,15 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
237239
if (match.Success)
238240
{
239241
// Read file
240-
SendConfirmation(channel); // Send reply
242+
SendSuccessConfirmation(channel); // Send reply
241243

242244
var length = long.Parse(match.Result("${length}"));
243245
var fileName = match.Result("${filename}");
244246

245247
var fileInfo = fileSystemInfo as FileInfo;
246248

247249
if (fileInfo == null)
248-
fileInfo = new FileInfo(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, fileName));
250+
fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName));
249251

250252
using (var output = fileInfo.OpenWrite())
251253
{
@@ -264,7 +266,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
264266
if (match.Success)
265267
{
266268
// Read timestamp
267-
SendConfirmation(channel); // Send reply
269+
SendSuccessConfirmation(channel); // Send reply
268270

269271
var mtime = long.Parse(match.Result("${mtime}"));
270272
var atime = long.Parse(match.Result("${atime}"));
@@ -275,7 +277,7 @@ private void InternalDownload(IChannelSession channel, Stream input, FileSystemI
275277
continue;
276278
}
277279

278-
SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
280+
SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
279281
}
280282
}
281283
}

0 commit comments

Comments
 (0)