2
0

ScpClient.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Renci.SshNet.Channels;
  6. using System.IO;
  7. using Renci.SshNet.Common;
  8. using Renci.SshNet.Messages.Connection;
  9. using System.Text.RegularExpressions;
  10. using System.Threading;
  11. using System.Diagnostics;
  12. using System.Diagnostics.CodeAnalysis;
  13. namespace Renci.SshNet
  14. {
  15. /// <summary>
  16. /// Provides SCP client functionality.
  17. /// </summary>
  18. public partial class ScpClient : BaseClient
  19. {
  20. private static Regex _fileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
  21. private static Regex _directoryInfoRe = new Regex(@"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
  22. private static Regex _timestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0");
  23. private static char[] _byteToChar;
  24. private bool _disposeConnectionInfo;
  25. /// <summary>
  26. /// Gets or sets the operation timeout.
  27. /// </summary>
  28. /// <value>The operation timeout.</value>
  29. public TimeSpan OperationTimeout { get; set; }
  30. /// <summary>
  31. /// Gets or sets the size of the buffer.
  32. /// </summary>
  33. /// <value>The size of the buffer.</value>
  34. public uint BufferSize { get; set; }
  35. /// <summary>
  36. /// Occurs when downloading file.
  37. /// </summary>
  38. public event EventHandler<ScpDownloadEventArgs> Downloading;
  39. /// <summary>
  40. /// Occurs when uploading file.
  41. /// </summary>
  42. public event EventHandler<ScpUploadEventArgs> Uploading;
  43. #region Constructors
  44. /// <summary>
  45. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  46. /// </summary>
  47. /// <param name="connectionInfo">The connection info.</param>
  48. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
  49. public ScpClient(ConnectionInfo connectionInfo)
  50. : base(connectionInfo)
  51. {
  52. this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
  53. this.BufferSize = 1024 * 32 - 38;
  54. if (_byteToChar == null)
  55. {
  56. _byteToChar = new char[128];
  57. var ch = '\0';
  58. for (int i = 0; i < 128; i++)
  59. {
  60. _byteToChar[i] = ch++;
  61. }
  62. }
  63. }
  64. /// <summary>
  65. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  66. /// </summary>
  67. /// <param name="host">Connection host.</param>
  68. /// <param name="port">Connection port.</param>
  69. /// <param name="username">Authentication username.</param>
  70. /// <param name="password">Authentication password.</param>
  71. /// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
  72. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
  73. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
  74. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  75. public ScpClient(string host, int port, string username, string password)
  76. : this(new PasswordConnectionInfo(host, port, username, password))
  77. {
  78. this._disposeConnectionInfo = true;
  79. }
  80. /// <summary>
  81. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  82. /// </summary>
  83. /// <param name="host">Connection host.</param>
  84. /// <param name="username">Authentication username.</param>
  85. /// <param name="password">Authentication password.</param>
  86. /// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
  87. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
  88. public ScpClient(string host, string username, string password)
  89. : this(host, 22, username, password)
  90. {
  91. }
  92. /// <summary>
  93. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  94. /// </summary>
  95. /// <param name="host">Connection host.</param>
  96. /// <param name="port">Connection port.</param>
  97. /// <param name="username">Authentication username.</param>
  98. /// <param name="keyFiles">Authentication private key file(s) .</param>
  99. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
  100. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
  101. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
  102. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  103. public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
  104. : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
  105. {
  106. this._disposeConnectionInfo = true;
  107. }
  108. /// <summary>
  109. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  110. /// </summary>
  111. /// <param name="host">Connection host.</param>
  112. /// <param name="username">Authentication username.</param>
  113. /// <param name="keyFiles">Authentication private key file(s) .</param>
  114. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
  115. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
  116. public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles)
  117. : this(host, 22, username, keyFiles)
  118. {
  119. }
  120. #endregion
  121. /// <summary>
  122. /// Uploads the specified stream to the remote host.
  123. /// </summary>
  124. /// <param name="source">Stream to upload.</param>
  125. /// <param name="filename">Remote host file name.</param>
  126. public void Upload(Stream source, string filename)
  127. {
  128. using (var input = new PipeStream())
  129. using (var channel = this.Session.CreateChannel<ChannelSession>())
  130. {
  131. channel.DataReceived += delegate(object sender, Common.ChannelDataEventArgs e)
  132. {
  133. input.Write(e.Data, 0, e.Data.Length);
  134. input.Flush();
  135. };
  136. channel.Open();
  137. // Send channel command request
  138. channel.SendExecRequest(string.Format("scp -qt \"{0}\"", filename));
  139. this.CheckReturnCode(input);
  140. this.InternalFileUpload(channel, input, source, filename);
  141. channel.Close();
  142. }
  143. }
  144. /// <summary>
  145. /// Downloads the specified file from the remote host to the stream.
  146. /// </summary>
  147. /// <param name="filename">Remote host file name.</param>
  148. /// <param name="destination">The stream where to download remote file.</param>
  149. /// <exception cref="ArgumentException"><paramref name="filename"/> is null or contains whitespace characters.</exception>
  150. /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception>
  151. /// <remarks>Method calls made by this method to <paramref name="destination"/>, may under certain conditions result in exceptions thrown by the stream.</remarks>
  152. public void Download(string filename, Stream destination)
  153. {
  154. if (filename.IsNullOrWhiteSpace())
  155. throw new ArgumentException("filename");
  156. if (destination == null)
  157. throw new ArgumentNullException("destination");
  158. // UNDONE: Should call EnsureConnection here to keep it consistent? If you add the call, please add to comment: <exception cref="SshConnectionException">Client is not connected.</exception>
  159. using (var input = new PipeStream())
  160. using (var channel = this.Session.CreateChannel<ChannelSession>())
  161. {
  162. channel.DataReceived += delegate(object sender, Common.ChannelDataEventArgs e)
  163. {
  164. input.Write(e.Data, 0, e.Data.Length);
  165. input.Flush();
  166. };
  167. channel.Open();
  168. // Send channel command request
  169. channel.SendExecRequest(string.Format("scp -qf \"{0}\"", filename));
  170. this.SendConfirmation(channel); // Send reply
  171. var message = ReadString(input);
  172. var match = _fileInfoRe.Match(message);
  173. if (match.Success)
  174. {
  175. // Read file
  176. this.SendConfirmation(channel); // Send reply
  177. var mode = match.Result("${mode}");
  178. var length = long.Parse(match.Result("${length}"));
  179. var fileName = match.Result("${filename}");
  180. this.InternalFileDownload(channel, input, destination, fileName, length);
  181. }
  182. else
  183. {
  184. this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
  185. }
  186. channel.Close();
  187. }
  188. }
  189. private void InternalSetTimestamp(ChannelSession channel, Stream input, DateTime lastWriteTime, DateTime lastAccessime)
  190. {
  191. var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
  192. var modificationSeconds = (long)(lastWriteTime - zeroTime).TotalSeconds;
  193. var accessSeconds = (long)(lastAccessime - zeroTime).TotalSeconds;
  194. this.SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
  195. this.CheckReturnCode(input);
  196. }
  197. private void InternalFileUpload(ChannelSession channel, Stream input, Stream source, string filename)
  198. {
  199. var length = source.Length;
  200. this.SendData(channel, string.Format("C0644 {0} {1}\n", length, filename));
  201. var buffer = new byte[this.BufferSize];
  202. var read = source.Read(buffer, 0, buffer.Length);
  203. long totalRead = 0;
  204. while (read > 0)
  205. {
  206. this.SendData(channel, buffer, read);
  207. totalRead += read;
  208. this.RaiseUploadingEvent(filename, length, totalRead);
  209. read = source.Read(buffer, 0, buffer.Length);
  210. }
  211. this.SendConfirmation(channel);
  212. this.CheckReturnCode(input);
  213. }
  214. private void InternalFileDownload(ChannelSession channel, Stream input, Stream output, string filename, long length)
  215. {
  216. var buffer = new byte[Math.Min(length, this.BufferSize)];
  217. var needToRead = length;
  218. do
  219. {
  220. var read = input.Read(buffer, 0, (int)Math.Min(needToRead, this.BufferSize));
  221. output.Write(buffer, 0, read);
  222. this.RaiseDownloadingEvent(filename, length, length - needToRead);
  223. needToRead -= read;
  224. }
  225. while (needToRead > 0);
  226. output.Flush();
  227. // Raise one more time when file downloaded
  228. this.RaiseDownloadingEvent(filename, length, length - needToRead);
  229. // Send confirmation byte after last data byte was read
  230. this.SendConfirmation(channel);
  231. this.CheckReturnCode(input);
  232. }
  233. private void RaiseDownloadingEvent(string filename, long size, long downloaded)
  234. {
  235. if (this.Downloading != null)
  236. {
  237. this.Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded));
  238. }
  239. }
  240. private void RaiseUploadingEvent(string filename, long size, long uploaded)
  241. {
  242. if (this.Uploading != null)
  243. {
  244. this.Uploading(this, new ScpUploadEventArgs(filename, size, uploaded));
  245. }
  246. }
  247. private void SendConfirmation(ChannelSession channel)
  248. {
  249. this.SendData(channel, new byte[] { 0 });
  250. }
  251. private void SendConfirmation(ChannelSession channel, byte errorCode, string message)
  252. {
  253. this.SendData(channel, new byte[] { errorCode });
  254. this.SendData(channel, string.Format("{0}\n", message));
  255. }
  256. /// <summary>
  257. /// Checks the return code.
  258. /// </summary>
  259. /// <param name="input">The output stream.</param>
  260. private void CheckReturnCode(Stream input)
  261. {
  262. var b = ReadByte(input);
  263. if (b > 0)
  264. {
  265. var errorText = ReadString(input);
  266. throw new ScpException(errorText);
  267. }
  268. }
  269. partial void SendData(ChannelSession channel, string command);
  270. private void SendData(ChannelSession channel, byte[] buffer, int length)
  271. {
  272. if (length == buffer.Length)
  273. {
  274. channel.SendData(buffer);
  275. }
  276. else
  277. {
  278. channel.SendData(buffer.Take(length).ToArray());
  279. }
  280. }
  281. private void SendData(ChannelSession channel, byte[] buffer)
  282. {
  283. channel.SendData(buffer);
  284. }
  285. private static int ReadByte(Stream stream)
  286. {
  287. var b = stream.ReadByte();
  288. while (b < 0)
  289. {
  290. Thread.Sleep(100);
  291. b = stream.ReadByte();
  292. }
  293. return b;
  294. }
  295. private static string ReadString(Stream stream)
  296. {
  297. var hasError = false;
  298. StringBuilder sb = new StringBuilder();
  299. var b = ReadByte(stream);
  300. if (b == 1 || b == 2)
  301. {
  302. hasError = true;
  303. b = ReadByte(stream);
  304. }
  305. var ch = _byteToChar[b];
  306. while (ch != '\n')
  307. {
  308. sb.Append(ch);
  309. b = ReadByte(stream);
  310. ch = _byteToChar[b];
  311. }
  312. if (hasError)
  313. throw new ScpException(sb.ToString());
  314. return sb.ToString();
  315. }
  316. /// <summary>
  317. /// Releases unmanaged and - optionally - managed resources
  318. /// </summary>
  319. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
  320. protected override void Dispose(bool disposing)
  321. {
  322. base.Dispose(disposing);
  323. if (this._disposeConnectionInfo)
  324. ((IDisposable)this.ConnectionInfo).Dispose();
  325. }
  326. }
  327. }