ScpClient.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. using System;
  2. using Renci.SshNet.Channels;
  3. using System.IO;
  4. using Renci.SshNet.Common;
  5. using System.Text.RegularExpressions;
  6. using System.Diagnostics.CodeAnalysis;
  7. using System.Net;
  8. using System.Collections.Generic;
  9. namespace Renci.SshNet
  10. {
  11. /// <summary>
  12. /// Provides SCP client functionality.
  13. /// </summary>
  14. /// <remarks>
  15. /// <para>
  16. /// More information on the SCP protocol is available here:
  17. /// https://github.com/net-ssh/net-scp/blob/master/lib/net/scp.rb
  18. /// </para>
  19. /// <para>
  20. /// Known issues in OpenSSH:
  21. /// <list type="bullet">
  22. /// <item>
  23. /// <description>Recursive download (-prf) does not deal well with specific UTF-8 and newline characters.</description>
  24. /// <description>Recursive update does not support empty path for uploading to home directory.</description>
  25. /// </item>
  26. /// </list>
  27. /// </para>
  28. /// </remarks>
  29. public partial class ScpClient : BaseClient
  30. {
  31. private static readonly Regex FileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
  32. private static readonly byte[] SuccessConfirmationCode = {0};
  33. private static readonly byte[] ErrorConfirmationCode = { 1 };
  34. /// <summary>
  35. /// Gets or sets the operation timeout.
  36. /// </summary>
  37. /// <value>
  38. /// The timeout to wait until an operation completes. The default value is negative
  39. /// one (-1) milliseconds, which indicates an infinite time-out period.
  40. /// </value>
  41. public TimeSpan OperationTimeout { get; set; }
  42. /// <summary>
  43. /// Gets or sets the size of the buffer.
  44. /// </summary>
  45. /// <value>
  46. /// The size of the buffer. The default buffer size is 16384 bytes.
  47. /// </value>
  48. public uint BufferSize { get; set; }
  49. /// <summary>
  50. /// Occurs when downloading file.
  51. /// </summary>
  52. public event EventHandler<ScpDownloadEventArgs> Downloading;
  53. /// <summary>
  54. /// Occurs when uploading file.
  55. /// </summary>
  56. public event EventHandler<ScpUploadEventArgs> Uploading;
  57. #region Constructors
  58. /// <summary>
  59. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  60. /// </summary>
  61. /// <param name="connectionInfo">The connection info.</param>
  62. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
  63. public ScpClient(ConnectionInfo connectionInfo)
  64. : this(connectionInfo, false)
  65. {
  66. }
  67. /// <summary>
  68. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  69. /// </summary>
  70. /// <param name="host">Connection host.</param>
  71. /// <param name="port">Connection port.</param>
  72. /// <param name="username">Authentication username.</param>
  73. /// <param name="password">Authentication password.</param>
  74. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <c>null</c>.</exception>
  75. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
  76. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  77. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  78. public ScpClient(string host, int port, string username, string password)
  79. : this(new PasswordConnectionInfo(host, port, username, password), true)
  80. {
  81. }
  82. /// <summary>
  83. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  84. /// </summary>
  85. /// <param name="host">Connection host.</param>
  86. /// <param name="username">Authentication username.</param>
  87. /// <param name="password">Authentication password.</param>
  88. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <c>null</c>.</exception>
  89. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
  90. public ScpClient(string host, string username, string password)
  91. : this(host, ConnectionInfo.DefaultPort, username, password)
  92. {
  93. }
  94. /// <summary>
  95. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  96. /// </summary>
  97. /// <param name="host">Connection host.</param>
  98. /// <param name="port">Connection port.</param>
  99. /// <param name="username">Authentication username.</param>
  100. /// <param name="keyFiles">Authentication private key file(s) .</param>
  101. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <c>null</c>.</exception>
  102. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
  103. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  104. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  105. public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
  106. : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
  107. {
  108. }
  109. /// <summary>
  110. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  111. /// </summary>
  112. /// <param name="host">Connection host.</param>
  113. /// <param name="username">Authentication username.</param>
  114. /// <param name="keyFiles">Authentication private key file(s) .</param>
  115. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <c>null</c>.</exception>
  116. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <c>null</c> or contains only whitespace characters.</exception>
  117. public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles)
  118. : this(host, ConnectionInfo.DefaultPort, username, keyFiles)
  119. {
  120. }
  121. /// <summary>
  122. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  123. /// </summary>
  124. /// <param name="connectionInfo">The connection info.</param>
  125. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  126. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
  127. /// <remarks>
  128. /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
  129. /// connection info will be disposed when this instance is disposed.
  130. /// </remarks>
  131. private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
  132. : this(connectionInfo, ownsConnectionInfo, new ServiceFactory())
  133. {
  134. }
  135. /// <summary>
  136. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  137. /// </summary>
  138. /// <param name="connectionInfo">The connection info.</param>
  139. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  140. /// <param name="serviceFactory">The factory to use for creating new services.</param>
  141. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
  142. /// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <c>null</c>.</exception>
  143. /// <remarks>
  144. /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
  145. /// connection info will be disposed when this instance is disposed.
  146. /// </remarks>
  147. internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
  148. : base(connectionInfo, ownsConnectionInfo, serviceFactory)
  149. {
  150. OperationTimeout = SshNet.Session.InfiniteTimeSpan;
  151. BufferSize = 1024 * 16;
  152. }
  153. #endregion
  154. /// <summary>
  155. /// Uploads the specified stream to the remote host.
  156. /// </summary>
  157. /// <param name="source">The <see cref="Stream"/> to upload.</param>
  158. /// <param name="path">A relative or absolute path for the remote file.</param>
  159. /// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
  160. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  161. public void Upload(Stream source, string path)
  162. {
  163. using (var input = ServiceFactory.CreatePipeStream())
  164. using (var channel = Session.CreateChannelSession())
  165. {
  166. channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
  167. channel.Open();
  168. // pass the full path to ensure the server does not create the directory part
  169. // as a file in case the directory does not exist
  170. if (!channel.SendExecRequest(string.Format("scp -t {0}", path.ShellQuote())))
  171. {
  172. throw SecureExecutionRequestRejectedException();
  173. }
  174. CheckReturnCode(input);
  175. // specify a zero-length file name to avoid creating a file with absolute
  176. // path '<path>/<filename part of path>' if directory '<path>' already exists
  177. UploadFileModeAndName(channel, input, source.Length, string.Empty);
  178. UploadFileContent(channel, input, source, PosixPath.GetFileName(path));
  179. }
  180. }
  181. /// <summary>
  182. /// Downloads the specified file from the remote host to the stream.
  183. /// </summary>
  184. /// <param name="filename">A relative or absolute path for the remote file.</param>
  185. /// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
  186. /// <exception cref="ArgumentException"><paramref name="filename"/> is <c>null</c> or contains only whitespace characters.</exception>
  187. /// <exception cref="ArgumentNullException"><paramref name="destination"/> is <c>null</c>.</exception>
  188. /// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
  189. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  190. public void Download(string filename, Stream destination)
  191. {
  192. if (filename.IsNullOrWhiteSpace())
  193. throw new ArgumentException("filename");
  194. if (destination == null)
  195. throw new ArgumentNullException("destination");
  196. using (var input = ServiceFactory.CreatePipeStream())
  197. using (var channel = Session.CreateChannelSession())
  198. {
  199. channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
  200. channel.Open();
  201. // Send channel command request
  202. if (!channel.SendExecRequest(string.Format("scp -f {0}", filename.ShellQuote())))
  203. {
  204. throw SecureExecutionRequestRejectedException();
  205. }
  206. SendSuccessConfirmation(channel); // Send reply
  207. var message = ReadString(input);
  208. var match = FileInfoRe.Match(message);
  209. if (match.Success)
  210. {
  211. // Read file
  212. SendSuccessConfirmation(channel); // Send reply
  213. var length = long.Parse(match.Result("${length}"));
  214. var fileName = match.Result("${filename}");
  215. InternalDownload(channel, input, destination, fileName, length);
  216. }
  217. else
  218. {
  219. SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
  220. }
  221. }
  222. }
  223. /// <summary>
  224. /// Sets mode, size and name of file being upload.
  225. /// </summary>
  226. /// <param name="channel">The channel to perform the upload in.</param>
  227. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  228. /// <param name="fileSize">The size of the content to upload.</param>
  229. /// <param name="serverFileName">The name of the file, without path, to which the content is to be uploaded.</param>
  230. /// <remarks>
  231. /// <para>
  232. /// When the SCP transfer is already initiated for a file, a zero-length <see cref="string"/> should
  233. /// be specified for <paramref name="serverFileName"/>. This prevents the server from uploading the
  234. /// content to a file with path <c>&lt;file path&gt;/<paramref name="serverFileName"/></c> if there's
  235. /// already a directory with this path, and allows us to receive an error response.
  236. /// </para>
  237. /// </remarks>
  238. private void UploadFileModeAndName(IChannelSession channel, Stream input, long fileSize, string serverFileName)
  239. {
  240. SendData(channel, string.Format("C0644 {0} {1}\n", fileSize, serverFileName));
  241. CheckReturnCode(input);
  242. }
  243. /// <summary>
  244. /// Uploads the content of a file.
  245. /// </summary>
  246. /// <param name="channel">The channel to perform the upload in.</param>
  247. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  248. /// <param name="source">The content to upload.</param>
  249. /// <param name="remoteFileName">The name of the remote file, without path, to which the content is uploaded.</param>
  250. /// <remarks>
  251. /// <paramref name="remoteFileName"/> is only used for raising the <see cref="Uploading"/> event.
  252. /// </remarks>
  253. private void UploadFileContent(IChannelSession channel, Stream input, Stream source, string remoteFileName)
  254. {
  255. var totalLength = source.Length;
  256. var buffer = new byte[BufferSize];
  257. var read = source.Read(buffer, 0, buffer.Length);
  258. long totalRead = 0;
  259. while (read > 0)
  260. {
  261. SendData(channel, buffer, read);
  262. totalRead += read;
  263. RaiseUploadingEvent(remoteFileName, totalLength, totalRead);
  264. read = source.Read(buffer, 0, buffer.Length);
  265. }
  266. SendSuccessConfirmation(channel);
  267. CheckReturnCode(input);
  268. }
  269. private void InternalDownload(IChannel channel, Stream input, Stream output, string filename, long length)
  270. {
  271. var buffer = new byte[Math.Min(length, BufferSize)];
  272. var needToRead = length;
  273. do
  274. {
  275. var read = input.Read(buffer, 0, (int) Math.Min(needToRead, BufferSize));
  276. output.Write(buffer, 0, read);
  277. RaiseDownloadingEvent(filename, length, length - needToRead);
  278. needToRead -= read;
  279. }
  280. while (needToRead > 0);
  281. output.Flush();
  282. // Raise one more time when file downloaded
  283. RaiseDownloadingEvent(filename, length, length - needToRead);
  284. // Send confirmation byte after last data byte was read
  285. SendSuccessConfirmation(channel);
  286. CheckReturnCode(input);
  287. }
  288. private void RaiseDownloadingEvent(string filename, long size, long downloaded)
  289. {
  290. if (Downloading != null)
  291. {
  292. Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded));
  293. }
  294. }
  295. private void RaiseUploadingEvent(string filename, long size, long uploaded)
  296. {
  297. if (Uploading != null)
  298. {
  299. Uploading(this, new ScpUploadEventArgs(filename, size, uploaded));
  300. }
  301. }
  302. private static void SendSuccessConfirmation(IChannel channel)
  303. {
  304. SendData(channel, SuccessConfirmationCode);
  305. }
  306. private void SendErrorConfirmation(IChannel channel, string message)
  307. {
  308. SendData(channel, ErrorConfirmationCode);
  309. SendData(channel, string.Concat(message, "\n"));
  310. }
  311. /// <summary>
  312. /// Checks the return code.
  313. /// </summary>
  314. /// <param name="input">The output stream.</param>
  315. private void CheckReturnCode(Stream input)
  316. {
  317. var b = ReadByte(input);
  318. if (b > 0)
  319. {
  320. var errorText = ReadString(input);
  321. throw new ScpException(errorText);
  322. }
  323. }
  324. private void SendData(IChannel channel, string command)
  325. {
  326. channel.SendData(ConnectionInfo.Encoding.GetBytes(command));
  327. }
  328. private static void SendData(IChannel channel, byte[] buffer, int length)
  329. {
  330. channel.SendData(buffer, 0, length);
  331. }
  332. private static void SendData(IChannel channel, byte[] buffer)
  333. {
  334. channel.SendData(buffer);
  335. }
  336. private static int ReadByte(Stream stream)
  337. {
  338. var b = stream.ReadByte();
  339. if (b == -1)
  340. throw new SshException("Stream has been closed.");
  341. return b;
  342. }
  343. /// <summary>
  344. /// Read a LF-terminated string from the <see cref="Stream"/>.
  345. /// </summary>
  346. /// <param name="stream">The <see cref="Stream"/> to read from.</param>
  347. /// <returns>
  348. /// The string without trailing LF.
  349. /// </returns>
  350. private string ReadString(Stream stream)
  351. {
  352. var hasError = false;
  353. var buffer = new List<byte>();
  354. var b = ReadByte(stream);
  355. if (b == 1 || b == 2)
  356. {
  357. hasError = true;
  358. b = ReadByte(stream);
  359. }
  360. while (b != SshNet.Session.LineFeed)
  361. {
  362. buffer.Add((byte) b);
  363. b = ReadByte(stream);
  364. }
  365. var readBytes = buffer.ToArray();
  366. if (hasError)
  367. throw new ScpException(ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length));
  368. return ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length);
  369. }
  370. private static SshException SecureExecutionRequestRejectedException()
  371. {
  372. throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
  373. }
  374. }
  375. }