| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using Renci.SshNet.Channels;
- using System.IO;
- using Renci.SshNet.Common;
- using Renci.SshNet.Messages.Connection;
- using System.Text.RegularExpressions;
- using System.Threading;
- using System.Diagnostics;
- using System.Diagnostics.CodeAnalysis;
- namespace Renci.SshNet
- {
- /// <summary>
- /// Provides SCP client functionality.
- /// </summary>
- public partial class ScpClient : BaseClient
- {
- private static Regex _fileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
- private static Regex _directoryInfoRe = new Regex(@"D(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
- private static Regex _timestampRe = new Regex(@"T(?<mtime>\d+) 0 (?<atime>\d+) 0");
- private static char[] _byteToChar;
- private bool _disposeConnectionInfo;
- /// <summary>
- /// Gets or sets the operation timeout.
- /// </summary>
- /// <value>The operation timeout.</value>
- public TimeSpan OperationTimeout { get; set; }
- /// <summary>
- /// Gets or sets the size of the buffer.
- /// </summary>
- /// <value>The size of the buffer.</value>
- public uint BufferSize { get; set; }
- /// <summary>
- /// Occurs when downloading file.
- /// </summary>
- public event EventHandler<ScpDownloadEventArgs> Downloading;
- /// <summary>
- /// Occurs when uploading file.
- /// </summary>
- public event EventHandler<ScpUploadEventArgs> Uploading;
- #region Constructors
- /// <summary>
- /// Initializes a new instance of the <see cref="SftpClient"/> class.
- /// </summary>
- /// <param name="connectionInfo">The connection info.</param>
- /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
- public ScpClient(ConnectionInfo connectionInfo)
- : base(connectionInfo)
- {
- this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
- this.BufferSize = 1024 * 32 - 38;
- if (_byteToChar == null)
- {
- _byteToChar = new char[128];
- var ch = '\0';
- for (int i = 0; i < 128; i++)
- {
- _byteToChar[i] = ch++;
- }
- }
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="SftpClient"/> class.
- /// </summary>
- /// <param name="host">Connection host.</param>
- /// <param name="port">Connection port.</param>
- /// <param name="username">Authentication username.</param>
- /// <param name="password">Authentication password.</param>
- /// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
- /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
- /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
- [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
- public ScpClient(string host, int port, string username, string password)
- : this(new PasswordConnectionInfo(host, port, username, password))
- {
- this._disposeConnectionInfo = true;
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="SftpClient"/> class.
- /// </summary>
- /// <param name="host">Connection host.</param>
- /// <param name="username">Authentication username.</param>
- /// <param name="password">Authentication password.</param>
- /// <exception cref="ArgumentNullException"><paramref name="password"/> is null.</exception>
- /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is null or contains whitespace characters.</exception>
- public ScpClient(string host, string username, string password)
- : this(host, 22, username, password)
- {
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="SftpClient"/> class.
- /// </summary>
- /// <param name="host">Connection host.</param>
- /// <param name="port">Connection port.</param>
- /// <param name="username">Authentication username.</param>
- /// <param name="keyFiles">Authentication private key file(s) .</param>
- /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
- /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
- /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
- [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
- public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
- : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
- {
- this._disposeConnectionInfo = true;
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="SftpClient"/> class.
- /// </summary>
- /// <param name="host">Connection host.</param>
- /// <param name="username">Authentication username.</param>
- /// <param name="keyFiles">Authentication private key file(s) .</param>
- /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is null.</exception>
- /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is null or contains whitespace characters.</exception>
- public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles)
- : this(host, 22, username, keyFiles)
- {
- }
- #endregion
- /// <summary>
- /// Uploads the specified stream to the remote host.
- /// </summary>
- /// <param name="source">Stream to upload.</param>
- /// <param name="filename">Remote host file name.</param>
- public void Upload(Stream source, string filename)
- {
- using (var input = new PipeStream())
- using (var channel = this.Session.CreateChannel<ChannelSession>())
- {
- channel.DataReceived += delegate(object sender, Common.ChannelDataEventArgs e)
- {
- input.Write(e.Data, 0, e.Data.Length);
- input.Flush();
- };
- channel.Open();
- // Send channel command request
- channel.SendExecRequest(string.Format("scp -qt \"{0}\"", filename));
- this.CheckReturnCode(input);
- this.InternalFileUpload(channel, input, source, filename);
- channel.Close();
- }
- }
- /// <summary>
- /// Downloads the specified file from the remote host to the stream.
- /// </summary>
- /// <param name="filename">Remote host file name.</param>
- /// <param name="destination">The stream where to download remote file.</param>
- /// <exception cref="ArgumentException"><paramref name="filename"/> is null or contains whitespace characters.</exception>
- /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception>
- /// <remarks>Method calls made by this method to <paramref name="destination"/>, may under certain conditions result in exceptions thrown by the stream.</remarks>
- public void Download(string filename, Stream destination)
- {
- if (filename.IsNullOrWhiteSpace())
- throw new ArgumentException("filename");
- if (destination == null)
- throw new ArgumentNullException("destination");
- // 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>
- using (var input = new PipeStream())
- using (var channel = this.Session.CreateChannel<ChannelSession>())
- {
- channel.DataReceived += delegate(object sender, Common.ChannelDataEventArgs e)
- {
- input.Write(e.Data, 0, e.Data.Length);
- input.Flush();
- };
- channel.Open();
- // Send channel command request
- channel.SendExecRequest(string.Format("scp -qf \"{0}\"", filename));
- this.SendConfirmation(channel); // Send reply
- var message = ReadString(input);
- var match = _fileInfoRe.Match(message);
- if (match.Success)
- {
- // Read file
- this.SendConfirmation(channel); // Send reply
- var mode = match.Result("${mode}");
- var length = long.Parse(match.Result("${length}"));
- var fileName = match.Result("${filename}");
- this.InternalFileDownload(channel, input, destination, fileName, length);
- }
- else
- {
- this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
- }
- channel.Close();
- }
- }
- private void InternalSetTimestamp(ChannelSession channel, Stream input, DateTime lastWriteTime, DateTime lastAccessime)
- {
- var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
- var modificationSeconds = (long)(lastWriteTime - zeroTime).TotalSeconds;
- var accessSeconds = (long)(lastAccessime - zeroTime).TotalSeconds;
- this.SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
- this.CheckReturnCode(input);
- }
- private void InternalFileUpload(ChannelSession channel, Stream input, Stream source, string filename)
- {
- var length = source.Length;
- this.SendData(channel, string.Format("C0644 {0} {1}\n", length, filename));
- var buffer = new byte[this.BufferSize];
- var read = source.Read(buffer, 0, buffer.Length);
- long totalRead = 0;
- while (read > 0)
- {
- this.SendData(channel, buffer, read);
- totalRead += read;
- this.RaiseUploadingEvent(filename, length, totalRead);
- read = source.Read(buffer, 0, buffer.Length);
- }
- this.SendConfirmation(channel);
- this.CheckReturnCode(input);
- }
- private void InternalFileDownload(ChannelSession channel, Stream input, Stream output, string filename, long length)
- {
- var buffer = new byte[Math.Min(length, this.BufferSize)];
- var needToRead = length;
- do
- {
- var read = input.Read(buffer, 0, (int)Math.Min(needToRead, this.BufferSize));
- output.Write(buffer, 0, read);
- this.RaiseDownloadingEvent(filename, length, length - needToRead);
- needToRead -= read;
- }
- while (needToRead > 0);
- output.Flush();
- // Raise one more time when file downloaded
- this.RaiseDownloadingEvent(filename, length, length - needToRead);
- // Send confirmation byte after last data byte was read
- this.SendConfirmation(channel);
- this.CheckReturnCode(input);
- }
- private void RaiseDownloadingEvent(string filename, long size, long downloaded)
- {
- if (this.Downloading != null)
- {
- this.Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded));
- }
- }
- private void RaiseUploadingEvent(string filename, long size, long uploaded)
- {
- if (this.Uploading != null)
- {
- this.Uploading(this, new ScpUploadEventArgs(filename, size, uploaded));
- }
- }
- private void SendConfirmation(ChannelSession channel)
- {
- this.SendData(channel, new byte[] { 0 });
- }
- private void SendConfirmation(ChannelSession channel, byte errorCode, string message)
- {
- this.SendData(channel, new byte[] { errorCode });
- this.SendData(channel, string.Format("{0}\n", message));
- }
- /// <summary>
- /// Checks the return code.
- /// </summary>
- /// <param name="input">The output stream.</param>
- private void CheckReturnCode(Stream input)
- {
- var b = ReadByte(input);
- if (b > 0)
- {
- var errorText = ReadString(input);
- throw new ScpException(errorText);
- }
- }
- partial void SendData(ChannelSession channel, string command);
- private void SendData(ChannelSession channel, byte[] buffer, int length)
- {
- if (length == buffer.Length)
- {
- channel.SendData(buffer);
- }
- else
- {
- channel.SendData(buffer.Take(length).ToArray());
- }
- }
- private void SendData(ChannelSession channel, byte[] buffer)
- {
- channel.SendData(buffer);
- }
- private static int ReadByte(Stream stream)
- {
- var b = stream.ReadByte();
- while (b < 0)
- {
- Thread.Sleep(100);
- b = stream.ReadByte();
- }
- return b;
- }
- private static string ReadString(Stream stream)
- {
- var hasError = false;
- StringBuilder sb = new StringBuilder();
- var b = ReadByte(stream);
- if (b == 1 || b == 2)
- {
- hasError = true;
- b = ReadByte(stream);
- }
- var ch = _byteToChar[b];
- while (ch != '\n')
- {
- sb.Append(ch);
- b = ReadByte(stream);
- ch = _byteToChar[b];
- }
- if (hasError)
- throw new ScpException(sb.ToString());
- return sb.ToString();
- }
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (this._disposeConnectionInfo)
- ((IDisposable)this.ConnectionInfo).Dispose();
- }
- }
- }
|