ScpClient.cs 20 KB

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