|
|
@@ -0,0 +1,376 @@
|
|
|
+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;
|
|
|
+
|
|
|
+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;
|
|
|
+
|
|
|
+ /// <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>
|
|
|
+ public ScpClient(string host, int port, string username, string password)
|
|
|
+ : this(new PasswordConnectionInfo(host, port, username, password))
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <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>
|
|
|
+ public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
|
|
|
+ : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <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>
|
|
|
+ public void Download(string filename, Stream destination)
|
|
|
+ {
|
|
|
+ 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)
|
|
|
+ {
|
|
|
+ this.Session.SendMessage(new ChannelDataMessage(channel.RemoteChannelNumber, buffer));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ this.Session.SendMessage(new ChannelDataMessage(channel.RemoteChannelNumber, buffer.Take(length).ToArray()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SendData(ChannelSession channel, byte[] buffer)
|
|
|
+ {
|
|
|
+ this.Session.SendMessage(new ChannelDataMessage(channel.RemoteChannelNumber, 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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|