ScpClient.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.Globalization;
  7. using System.IO;
  8. using System.Net;
  9. using System.Text.RegularExpressions;
  10. using System.Threading;
  11. using Renci.SshNet.Channels;
  12. using Renci.SshNet.Common;
  13. namespace Renci.SshNet
  14. {
  15. /// <summary>
  16. /// Provides SCP client functionality.
  17. /// </summary>
  18. /// <remarks>
  19. /// <para>
  20. /// More information on the SCP protocol is available here: https://github.com/net-ssh/net-scp/blob/master/lib/net/scp.rb.
  21. /// </para>
  22. /// <para>
  23. /// Known issues in OpenSSH:
  24. /// <list type="bullet">
  25. /// <item>
  26. /// <description>Recursive download (-prf) does not deal well with specific UTF-8 and newline characters.</description>
  27. /// <description>Recursive update does not support empty path for uploading to home directory.</description>
  28. /// </item>
  29. /// </list>
  30. /// </para>
  31. /// </remarks>
  32. public partial class ScpClient : BaseClient
  33. {
  34. private const string FileInfoPattern = @"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
  35. private const string DirectoryInfoPattern = @"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)";
  36. private const string TimestampPattern = @"T(?<mtime>\d+) 0 (?<atime>\d+) 0";
  37. #if NET
  38. private static readonly Regex FileInfoRegex = GetFileInfoRegex();
  39. private static readonly Regex DirectoryInfoRegex = GetDirectoryInfoRegex();
  40. private static readonly Regex TimestampRegex = GetTimestampRegex();
  41. [GeneratedRegex(FileInfoPattern)]
  42. private static partial Regex GetFileInfoRegex();
  43. [GeneratedRegex(DirectoryInfoPattern)]
  44. private static partial Regex GetDirectoryInfoRegex();
  45. [GeneratedRegex(TimestampPattern)]
  46. private static partial Regex GetTimestampRegex();
  47. #else
  48. private static readonly Regex FileInfoRegex = new Regex(FileInfoPattern, RegexOptions.Compiled);
  49. private static readonly Regex DirectoryInfoRegex = new Regex(DirectoryInfoPattern, RegexOptions.Compiled);
  50. private static readonly Regex TimestampRegex = new Regex(TimestampPattern, RegexOptions.Compiled);
  51. #endif
  52. private static readonly byte[] SuccessConfirmationCode = { 0 };
  53. private static readonly byte[] ErrorConfirmationCode = { 1 };
  54. private IRemotePathTransformation _remotePathTransformation;
  55. private TimeSpan _operationTimeout;
  56. /// <summary>
  57. /// Gets or sets the operation timeout.
  58. /// </summary>
  59. /// <value>
  60. /// The timeout to wait until an operation completes. The default value is negative
  61. /// one (-1) milliseconds, which indicates an infinite time-out period.
  62. /// </value>
  63. public TimeSpan OperationTimeout
  64. {
  65. get
  66. {
  67. return _operationTimeout;
  68. }
  69. set
  70. {
  71. value.EnsureValidTimeout(nameof(OperationTimeout));
  72. _operationTimeout = value;
  73. }
  74. }
  75. /// <summary>
  76. /// Gets or sets the size of the buffer.
  77. /// </summary>
  78. /// <value>
  79. /// The size of the buffer. The default buffer size is 16384 bytes.
  80. /// </value>
  81. public uint BufferSize { get; set; }
  82. /// <summary>
  83. /// Gets or sets the transformation to apply to remote paths.
  84. /// </summary>
  85. /// <value>
  86. /// The transformation to apply to remote paths. The default is <see cref="RemotePathTransformation.DoubleQuote"/>.
  87. /// </value>
  88. /// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
  89. /// <remarks>
  90. /// <para>
  91. /// This transformation is applied to the remote file or directory path that is passed to the
  92. /// <c>scp</c> command.
  93. /// </para>
  94. /// <para>
  95. /// See <see cref="SshNet.RemotePathTransformation"/> for the transformations that are supplied
  96. /// out-of-the-box with SSH.NET.
  97. /// </para>
  98. /// </remarks>
  99. public IRemotePathTransformation RemotePathTransformation
  100. {
  101. get
  102. {
  103. return _remotePathTransformation;
  104. }
  105. set
  106. {
  107. ThrowHelper.ThrowIfNull(value);
  108. _remotePathTransformation = value;
  109. }
  110. }
  111. /// <summary>
  112. /// Occurs when downloading file.
  113. /// </summary>
  114. public event EventHandler<ScpDownloadEventArgs>? Downloading;
  115. /// <summary>
  116. /// Occurs when uploading file.
  117. /// </summary>
  118. public event EventHandler<ScpUploadEventArgs>? Uploading;
  119. /// <summary>
  120. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  121. /// </summary>
  122. /// <param name="connectionInfo">The connection info.</param>
  123. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  124. public ScpClient(ConnectionInfo connectionInfo)
  125. : this(connectionInfo, ownsConnectionInfo: false)
  126. {
  127. }
  128. /// <summary>
  129. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  130. /// </summary>
  131. /// <param name="host">Connection host.</param>
  132. /// <param name="port">Connection port.</param>
  133. /// <param name="username">Authentication username.</param>
  134. /// <param name="password">Authentication password.</param>
  135. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  136. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  137. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  138. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  139. public ScpClient(string host, int port, string username, string password)
  140. : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true)
  141. {
  142. }
  143. /// <summary>
  144. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  145. /// </summary>
  146. /// <param name="host">Connection host.</param>
  147. /// <param name="username">Authentication username.</param>
  148. /// <param name="password">Authentication password.</param>
  149. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  150. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  151. public ScpClient(string host, string username, string password)
  152. : this(host, ConnectionInfo.DefaultPort, username, password)
  153. {
  154. }
  155. /// <summary>
  156. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  157. /// </summary>
  158. /// <param name="host">Connection host.</param>
  159. /// <param name="port">Connection port.</param>
  160. /// <param name="username">Authentication username.</param>
  161. /// <param name="keyFiles">Authentication private key file(s) .</param>
  162. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  163. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  164. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  165. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  166. public ScpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles)
  167. : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true)
  168. {
  169. }
  170. /// <summary>
  171. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  172. /// </summary>
  173. /// <param name="host">Connection host.</param>
  174. /// <param name="username">Authentication username.</param>
  175. /// <param name="keyFiles">Authentication private key file(s) .</param>
  176. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  177. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  178. public ScpClient(string host, string username, params IPrivateKeySource[] keyFiles)
  179. : this(host, ConnectionInfo.DefaultPort, username, keyFiles)
  180. {
  181. }
  182. /// <summary>
  183. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  184. /// </summary>
  185. /// <param name="connectionInfo">The connection info.</param>
  186. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  187. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  188. /// <remarks>
  189. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
  190. /// connection info will be disposed when this instance is disposed.
  191. /// </remarks>
  192. private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
  193. : this(connectionInfo, ownsConnectionInfo, new ServiceFactory())
  194. {
  195. }
  196. /// <summary>
  197. /// Initializes a new instance of the <see cref="ScpClient"/> class.
  198. /// </summary>
  199. /// <param name="connectionInfo">The connection info.</param>
  200. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  201. /// <param name="serviceFactory">The factory to use for creating new services.</param>
  202. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  203. /// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</exception>
  204. /// <remarks>
  205. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
  206. /// connection info will be disposed when this instance is disposed.
  207. /// </remarks>
  208. internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
  209. : base(connectionInfo, ownsConnectionInfo, serviceFactory)
  210. {
  211. OperationTimeout = Timeout.InfiniteTimeSpan;
  212. BufferSize = 1024 * 16;
  213. _remotePathTransformation = serviceFactory.CreateRemotePathDoubleQuoteTransformation();
  214. }
  215. /// <summary>
  216. /// Uploads the specified stream to the remote host.
  217. /// </summary>
  218. /// <param name="source">The <see cref="Stream"/> to upload.</param>
  219. /// <param name="path">A relative or absolute path for the remote file.</param>
  220. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  221. /// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
  222. /// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
  223. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  224. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  225. public void Upload(Stream source, string path)
  226. {
  227. if (Session is null)
  228. {
  229. throw new SshConnectionException("Client not connected.");
  230. }
  231. var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
  232. using (var input = ServiceFactory.CreatePipeStream())
  233. using (var channel = Session.CreateChannelSession())
  234. {
  235. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  236. channel.Closed += (sender, e) => input.Dispose();
  237. channel.Open();
  238. // Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
  239. // that we expect the target to be a directory.
  240. if (!channel.SendExecRequest(string.Format("scp -t -d {0}", _remotePathTransformation.Transform(posixPath.Directory))))
  241. {
  242. throw SecureExecutionRequestRejectedException();
  243. }
  244. CheckReturnCode(input);
  245. UploadFileModeAndName(channel, input, source.Length, posixPath.File);
  246. UploadFileContent(channel, input, source, posixPath.File);
  247. }
  248. }
  249. /// <summary>
  250. /// Uploads the specified file to the remote host.
  251. /// </summary>
  252. /// <param name="fileInfo">The file system info.</param>
  253. /// <param name="path">A relative or absolute path for the remote file.</param>
  254. /// <exception cref="ArgumentNullException"><paramref name="fileInfo" /> is <see langword="null"/>.</exception>
  255. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  256. /// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length <see cref="string"/>.</exception>
  257. /// <exception cref="ScpException">A directory with the specified path exists on the remote host.</exception>
  258. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  259. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  260. public void Upload(FileInfo fileInfo, string path)
  261. {
  262. ThrowHelper.ThrowIfNull(fileInfo);
  263. if (Session is null)
  264. {
  265. throw new SshConnectionException("Client not connected.");
  266. }
  267. var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path);
  268. using (var input = ServiceFactory.CreatePipeStream())
  269. using (var channel = Session.CreateChannelSession())
  270. {
  271. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  272. channel.Closed += (sender, e) => input.Dispose();
  273. channel.Open();
  274. // Pass only the directory part of the path to the server, and use the (hidden) -d option to signal
  275. // that we expect the target to be a directory.
  276. if (!channel.SendExecRequest($"scp -t -d {_remotePathTransformation.Transform(posixPath.Directory)}"))
  277. {
  278. throw SecureExecutionRequestRejectedException();
  279. }
  280. CheckReturnCode(input);
  281. using (var source = fileInfo.OpenRead())
  282. {
  283. UploadTimes(channel, input, fileInfo);
  284. UploadFileModeAndName(channel, input, source.Length, posixPath.File);
  285. UploadFileContent(channel, input, source, fileInfo.Name);
  286. }
  287. }
  288. }
  289. /// <summary>
  290. /// Uploads the specified directory to the remote host.
  291. /// </summary>
  292. /// <param name="directoryInfo">The directory info.</param>
  293. /// <param name="path">A relative or absolute path for the remote directory.</param>
  294. /// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <see langword="null"/>.</exception>
  295. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  296. /// <exception cref="ArgumentException"><paramref name="path"/> is a zero-length string.</exception>
  297. /// <exception cref="ScpException"><paramref name="path"/> does not exist on the remote host, is not a directory or the user does not have the required permission.</exception>
  298. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  299. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  300. public void Upload(DirectoryInfo directoryInfo, string path)
  301. {
  302. ThrowHelper.ThrowIfNull(directoryInfo);
  303. ThrowHelper.ThrowIfNullOrEmpty(path);
  304. if (Session is null)
  305. {
  306. throw new SshConnectionException("Client not connected.");
  307. }
  308. using (var input = ServiceFactory.CreatePipeStream())
  309. using (var channel = Session.CreateChannelSession())
  310. {
  311. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  312. channel.Closed += (sender, e) => input.Dispose();
  313. channel.Open();
  314. // start copy with the following options:
  315. // -p preserve modification and access times
  316. // -r copy directories recursively
  317. // -d expect path to be a directory
  318. // -t copy to remote
  319. if (!channel.SendExecRequest($"scp -r -p -d -t {_remotePathTransformation.Transform(path)}"))
  320. {
  321. throw SecureExecutionRequestRejectedException();
  322. }
  323. CheckReturnCode(input);
  324. UploadDirectoryContent(channel, input, directoryInfo);
  325. }
  326. }
  327. /// <summary>
  328. /// Downloads the specified file from the remote host to local file.
  329. /// </summary>
  330. /// <param name="filename">Remote host file name.</param>
  331. /// <param name="fileInfo">Local file information.</param>
  332. /// <exception cref="ArgumentNullException"><paramref name="fileInfo"/> is <see langword="null"/>.</exception>
  333. /// <exception cref="ArgumentException"><paramref name="filename"/> is <see langword="null"/> or empty.</exception>
  334. /// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
  335. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  336. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  337. public void Download(string filename, FileInfo fileInfo)
  338. {
  339. ThrowHelper.ThrowIfNullOrEmpty(filename);
  340. ThrowHelper.ThrowIfNull(fileInfo);
  341. if (Session is null)
  342. {
  343. throw new SshConnectionException("Client not connected.");
  344. }
  345. using (var input = ServiceFactory.CreatePipeStream())
  346. using (var channel = Session.CreateChannelSession())
  347. {
  348. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  349. channel.Closed += (sender, e) => input.Dispose();
  350. channel.Open();
  351. // Send channel command request
  352. if (!channel.SendExecRequest($"scp -pf {_remotePathTransformation.Transform(filename)}"))
  353. {
  354. throw SecureExecutionRequestRejectedException();
  355. }
  356. // Send reply
  357. SendSuccessConfirmation(channel);
  358. InternalDownload(channel, input, fileInfo);
  359. }
  360. }
  361. /// <summary>
  362. /// Downloads the specified directory from the remote host to local directory.
  363. /// </summary>
  364. /// <param name="directoryName">Remote host directory name.</param>
  365. /// <param name="directoryInfo">Local directory information.</param>
  366. /// <exception cref="ArgumentException"><paramref name="directoryName"/> is <see langword="null"/> or empty.</exception>
  367. /// <exception cref="ArgumentNullException"><paramref name="directoryInfo"/> is <see langword="null"/>.</exception>
  368. /// <exception cref="ScpException">File or directory with the specified path does not exist on the remote host.</exception>
  369. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  370. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  371. public void Download(string directoryName, DirectoryInfo directoryInfo)
  372. {
  373. ThrowHelper.ThrowIfNullOrEmpty(directoryName);
  374. ThrowHelper.ThrowIfNull(directoryInfo);
  375. if (Session is null)
  376. {
  377. throw new SshConnectionException("Client not connected.");
  378. }
  379. using (var input = ServiceFactory.CreatePipeStream())
  380. using (var channel = Session.CreateChannelSession())
  381. {
  382. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  383. channel.Closed += (sender, e) => input.Dispose();
  384. channel.Open();
  385. // Send channel command request
  386. if (!channel.SendExecRequest($"scp -prf {_remotePathTransformation.Transform(directoryName)}"))
  387. {
  388. throw SecureExecutionRequestRejectedException();
  389. }
  390. // Send reply
  391. SendSuccessConfirmation(channel);
  392. InternalDownload(channel, input, directoryInfo);
  393. }
  394. }
  395. /// <summary>
  396. /// Downloads the specified file from the remote host to the stream.
  397. /// </summary>
  398. /// <param name="filename">A relative or absolute path for the remote file.</param>
  399. /// <param name="destination">The <see cref="Stream"/> to download the remote file to.</param>
  400. /// <exception cref="ArgumentException"><paramref name="filename"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  401. /// <exception cref="ArgumentNullException"><paramref name="destination"/> is <see langword="null"/>.</exception>
  402. /// <exception cref="ScpException"><paramref name="filename"/> exists on the remote host, and is not a regular file.</exception>
  403. /// <exception cref="SshException">The secure copy execution request was rejected by the server.</exception>
  404. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  405. public void Download(string filename, Stream destination)
  406. {
  407. ThrowHelper.ThrowIfNullOrWhiteSpace(filename);
  408. ThrowHelper.ThrowIfNull(destination);
  409. if (Session is null)
  410. {
  411. throw new SshConnectionException("Client not connected.");
  412. }
  413. using (var input = ServiceFactory.CreatePipeStream())
  414. using (var channel = Session.CreateChannelSession())
  415. {
  416. channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  417. channel.Closed += (sender, e) => input.Dispose();
  418. channel.Open();
  419. // Send channel command request
  420. if (!channel.SendExecRequest(string.Concat("scp -f ", _remotePathTransformation.Transform(filename))))
  421. {
  422. throw SecureExecutionRequestRejectedException();
  423. }
  424. SendSuccessConfirmation(channel); // Send reply
  425. var message = ReadString(input);
  426. var match = FileInfoRegex.Match(message);
  427. if (match.Success)
  428. {
  429. // Read file
  430. SendSuccessConfirmation(channel); // Send reply
  431. var length = long.Parse(match.Result("${length}"), CultureInfo.InvariantCulture);
  432. var fileName = match.Result("${filename}");
  433. InternalDownload(channel, input, destination, fileName, length);
  434. }
  435. else
  436. {
  437. SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
  438. }
  439. }
  440. }
  441. private static void SendData(IChannel channel, byte[] buffer, int length)
  442. {
  443. channel.SendData(buffer, 0, length);
  444. }
  445. private static void SendData(IChannel channel, byte[] buffer)
  446. {
  447. channel.SendData(buffer);
  448. }
  449. private static int ReadByte(Stream stream)
  450. {
  451. var b = stream.ReadByte();
  452. if (b == -1)
  453. {
  454. throw new SshException("Stream has been closed.");
  455. }
  456. return b;
  457. }
  458. private static SshException SecureExecutionRequestRejectedException()
  459. {
  460. throw new SshException("Secure copy execution request was rejected by the server. Please consult the server logs.");
  461. }
  462. /// <summary>
  463. /// Sets mode, size and name of file being upload.
  464. /// </summary>
  465. /// <param name="channel">The channel to perform the upload in.</param>
  466. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  467. /// <param name="fileSize">The size of the content to upload.</param>
  468. /// <param name="serverFileName">The name of the file, without path, to which the content is to be uploaded.</param>
  469. /// <remarks>
  470. /// <para>
  471. /// When the SCP transfer is already initiated for a file, a zero-length <see cref="string"/> should
  472. /// be specified for <paramref name="serverFileName"/>. This prevents the server from uploading the
  473. /// content to a file with path <c>&lt;file path&gt;/<paramref name="serverFileName"/></c> if there's
  474. /// already a directory with this path, and allows us to receive an error response.
  475. /// </para>
  476. /// </remarks>
  477. private void UploadFileModeAndName(IChannelSession channel, Stream input, long fileSize, string serverFileName)
  478. {
  479. SendData(channel, string.Format("C0644 {0} {1}\n", fileSize, serverFileName));
  480. CheckReturnCode(input);
  481. }
  482. /// <summary>
  483. /// Uploads the content of a file.
  484. /// </summary>
  485. /// <param name="channel">The channel to perform the upload in.</param>
  486. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  487. /// <param name="source">The content to upload.</param>
  488. /// <param name="remoteFileName">The name of the remote file, without path, to which the content is uploaded.</param>
  489. /// <remarks>
  490. /// <paramref name="remoteFileName"/> is only used for raising the <see cref="Uploading"/> event.
  491. /// </remarks>
  492. private void UploadFileContent(IChannelSession channel, Stream input, Stream source, string remoteFileName)
  493. {
  494. var totalLength = source.Length;
  495. var buffer = new byte[BufferSize];
  496. var read = source.Read(buffer, 0, buffer.Length);
  497. long totalRead = 0;
  498. while (read > 0)
  499. {
  500. SendData(channel, buffer, read);
  501. totalRead += read;
  502. RaiseUploadingEvent(remoteFileName, totalLength, totalRead);
  503. read = source.Read(buffer, 0, buffer.Length);
  504. }
  505. SendSuccessConfirmation(channel);
  506. CheckReturnCode(input);
  507. }
  508. private void RaiseDownloadingEvent(string filename, long size, long downloaded)
  509. {
  510. Downloading?.Invoke(this, new ScpDownloadEventArgs(filename, size, downloaded));
  511. }
  512. private void RaiseUploadingEvent(string filename, long size, long uploaded)
  513. {
  514. Uploading?.Invoke(this, new ScpUploadEventArgs(filename, size, uploaded));
  515. }
  516. private static void SendSuccessConfirmation(IChannel channel)
  517. {
  518. SendData(channel, SuccessConfirmationCode);
  519. }
  520. private void SendErrorConfirmation(IChannel channel, string message)
  521. {
  522. SendData(channel, ErrorConfirmationCode);
  523. SendData(channel, string.Concat(message, "\n"));
  524. }
  525. /// <summary>
  526. /// Checks the return code.
  527. /// </summary>
  528. /// <param name="input">The output stream.</param>
  529. private void CheckReturnCode(Stream input)
  530. {
  531. var b = ReadByte(input);
  532. if (b > 0)
  533. {
  534. var errorText = ReadString(input);
  535. throw new ScpException(errorText);
  536. }
  537. }
  538. private void SendData(IChannel channel, string command)
  539. {
  540. channel.SendData(ConnectionInfo.Encoding.GetBytes(command));
  541. }
  542. /// <summary>
  543. /// Read a LF-terminated string from the <see cref="Stream"/>.
  544. /// </summary>
  545. /// <param name="stream">The <see cref="Stream"/> to read from.</param>
  546. /// <returns>
  547. /// The string without trailing LF.
  548. /// </returns>
  549. private string ReadString(Stream stream)
  550. {
  551. var hasError = false;
  552. var buffer = new List<byte>();
  553. var b = ReadByte(stream);
  554. if (b is 1 or 2)
  555. {
  556. hasError = true;
  557. b = ReadByte(stream);
  558. }
  559. while (b != SshNet.Session.LineFeed)
  560. {
  561. buffer.Add((byte)b);
  562. b = ReadByte(stream);
  563. }
  564. var readBytes = buffer.ToArray();
  565. if (hasError)
  566. {
  567. throw new ScpException(ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length));
  568. }
  569. return ConnectionInfo.Encoding.GetString(readBytes, 0, readBytes.Length);
  570. }
  571. /// <summary>
  572. /// Uploads the <see cref="FileSystemInfo.LastWriteTimeUtc"/> and <see cref="FileSystemInfo.LastAccessTimeUtc"/>
  573. /// of the next file or directory to upload.
  574. /// </summary>
  575. /// <param name="channel">The channel to perform the upload in.</param>
  576. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  577. /// <param name="fileOrDirectory">The file or directory to upload.</param>
  578. private void UploadTimes(IChannelSession channel, Stream input, FileSystemInfo fileOrDirectory)
  579. {
  580. #if NET
  581. var zeroTime = DateTime.UnixEpoch;
  582. #else
  583. var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
  584. #endif
  585. var modificationSeconds = (long)(fileOrDirectory.LastWriteTimeUtc - zeroTime).TotalSeconds;
  586. var accessSeconds = (long)(fileOrDirectory.LastAccessTimeUtc - zeroTime).TotalSeconds;
  587. SendData(channel, string.Format(CultureInfo.InvariantCulture, "T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
  588. CheckReturnCode(input);
  589. }
  590. /// <summary>
  591. /// Upload the files and subdirectories in the specified directory.
  592. /// </summary>
  593. /// <param name="channel">The channel to perform the upload in.</param>
  594. /// <param name="input">A <see cref="Stream"/> from which any feedback from the server can be read.</param>
  595. /// <param name="directoryInfo">The directory to upload.</param>
  596. private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
  597. {
  598. // Upload files
  599. var files = directoryInfo.GetFiles();
  600. foreach (var file in files)
  601. {
  602. using (var source = file.OpenRead())
  603. {
  604. UploadTimes(channel, input, file);
  605. UploadFileModeAndName(channel, input, source.Length, file.Name);
  606. UploadFileContent(channel, input, source, file.Name);
  607. }
  608. }
  609. // Upload directories
  610. var directories = directoryInfo.GetDirectories();
  611. foreach (var directory in directories)
  612. {
  613. UploadTimes(channel, input, directory);
  614. UploadDirectoryModeAndName(channel, input, directory.Name);
  615. UploadDirectoryContent(channel, input, directory);
  616. }
  617. // Mark upload of current directory complete
  618. SendData(channel, "E\n");
  619. CheckReturnCode(input);
  620. }
  621. /// <summary>
  622. /// Sets mode and name of the directory being upload.
  623. /// </summary>
  624. private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName)
  625. {
  626. SendData(channel, string.Format("D0755 0 {0}\n", directoryName));
  627. CheckReturnCode(input);
  628. }
  629. private void InternalDownload(IChannel channel, Stream input, Stream output, string filename, long length)
  630. {
  631. var buffer = new byte[Math.Min(length, BufferSize)];
  632. var needToRead = length;
  633. do
  634. {
  635. var read = input.Read(buffer, 0, (int)Math.Min(needToRead, BufferSize));
  636. output.Write(buffer, 0, read);
  637. RaiseDownloadingEvent(filename, length, length - needToRead);
  638. needToRead -= read;
  639. }
  640. while (needToRead > 0);
  641. output.Flush();
  642. // Raise one more time when file downloaded
  643. RaiseDownloadingEvent(filename, length, length - needToRead);
  644. // Send confirmation byte after last data byte was read
  645. SendSuccessConfirmation(channel);
  646. CheckReturnCode(input);
  647. }
  648. private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
  649. {
  650. var modifiedTime = DateTime.Now;
  651. var accessedTime = DateTime.Now;
  652. var startDirectoryFullName = fileSystemInfo.FullName;
  653. var currentDirectoryFullName = startDirectoryFullName;
  654. var directoryCounter = 0;
  655. while (true)
  656. {
  657. var message = ReadString(input);
  658. if (message == "E")
  659. {
  660. SendSuccessConfirmation(channel); // Send reply
  661. directoryCounter--;
  662. if (directoryCounter == 0)
  663. {
  664. break;
  665. }
  666. var currentDirectoryParent = new DirectoryInfo(currentDirectoryFullName).Parent;
  667. Debug.Assert(currentDirectoryParent is not null, $"Should be {directoryCounter.ToString(CultureInfo.InvariantCulture)} levels deeper than {startDirectoryFullName}.");
  668. currentDirectoryFullName = currentDirectoryParent.FullName;
  669. continue;
  670. }
  671. var match = DirectoryInfoRegex.Match(message);
  672. if (match.Success)
  673. {
  674. SendSuccessConfirmation(channel); // Send reply
  675. // Read directory
  676. var filename = match.Result("${filename}");
  677. DirectoryInfo newDirectoryInfo;
  678. if (directoryCounter > 0)
  679. {
  680. newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename));
  681. newDirectoryInfo.LastAccessTime = accessedTime;
  682. newDirectoryInfo.LastWriteTime = modifiedTime;
  683. }
  684. else
  685. {
  686. // Don't create directory for first level
  687. newDirectoryInfo = (DirectoryInfo)fileSystemInfo;
  688. }
  689. directoryCounter++;
  690. currentDirectoryFullName = newDirectoryInfo.FullName;
  691. continue;
  692. }
  693. match = FileInfoRegex.Match(message);
  694. if (match.Success)
  695. {
  696. // Read file
  697. SendSuccessConfirmation(channel); // Send reply
  698. var length = long.Parse(match.Result("${length}"), CultureInfo.InvariantCulture);
  699. var fileName = match.Result("${filename}");
  700. if (fileSystemInfo is not FileInfo fileInfo)
  701. {
  702. fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName));
  703. }
  704. using (var output = fileInfo.OpenWrite())
  705. {
  706. InternalDownload(channel, input, output, fileName, length);
  707. }
  708. fileInfo.LastAccessTime = accessedTime;
  709. fileInfo.LastWriteTime = modifiedTime;
  710. if (directoryCounter == 0)
  711. {
  712. break;
  713. }
  714. continue;
  715. }
  716. match = TimestampRegex.Match(message);
  717. if (match.Success)
  718. {
  719. // Read timestamp
  720. SendSuccessConfirmation(channel); // Send reply
  721. var mtime = long.Parse(match.Result("${mtime}"), CultureInfo.InvariantCulture);
  722. var atime = long.Parse(match.Result("${atime}"), CultureInfo.InvariantCulture);
  723. #if NET
  724. var zeroTime = DateTime.UnixEpoch;
  725. #else
  726. var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
  727. #endif
  728. modifiedTime = zeroTime.AddSeconds(mtime);
  729. accessedTime = zeroTime.AddSeconds(atime);
  730. continue;
  731. }
  732. SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message));
  733. }
  734. }
  735. }
  736. }