using System; using System.Linq; using System.Text; using Renci.SshNet.Channels; using System.IO; using Renci.SshNet.Common; using System.Text.RegularExpressions; using System.Threading; using System.Diagnostics.CodeAnalysis; namespace Renci.SshNet { /// /// Provides SCP client functionality. /// public partial class ScpClient : BaseClient { private static readonly Regex _fileInfoRe = new Regex(@"C(?\d{4}) (?\d+) (?.+)"); private static char[] _byteToChar; /// /// Gets or sets the operation timeout. /// /// /// The timeout to wait until an operation completes. The default value is negative /// one (-1) milliseconds, which indicates an infinite time-out period. /// public TimeSpan OperationTimeout { get; set; } /// /// Gets or sets the size of the buffer. /// /// /// The size of the buffer. The default buffer size is 16384 bytes. /// public uint BufferSize { get; set; } /// /// Occurs when downloading file. /// public event EventHandler Downloading; /// /// Occurs when uploading file. /// public event EventHandler Uploading; #region Constructors /// /// Initializes a new instance of the class. /// /// The connection info. /// is null. public ScpClient(ConnectionInfo connectionInfo) : this(connectionInfo, false) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Connection port. /// Authentication username. /// Authentication password. /// is null. /// is invalid, or is null or contains whitespace characters. /// is not within and . [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), true) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Authentication username. /// Authentication password. /// is null. /// is invalid, or is null or contains whitespace characters. public ScpClient(string host, string username, string password) : this(host, ConnectionInfo.DEFAULT_PORT, username, password) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Connection port. /// Authentication username. /// Authentication private key file(s) . /// is null. /// is invalid, -or- is null or contains whitespace characters. /// is not within and . [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), true) { } /// /// Initializes a new instance of the class. /// /// Connection host. /// Authentication username. /// Authentication private key file(s) . /// is null. /// is invalid, -or- is null or contains whitespace characters. public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles) : this(host, ConnectionInfo.DEFAULT_PORT, username, keyFiles) { } /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// is null. /// /// If is true, then the /// connection info will be disposed when this instance is disposed. /// private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo) : base(connectionInfo, ownsConnectionInfo) { this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1); this.BufferSize = 1024 * 16; if (_byteToChar == null) { _byteToChar = new char[128]; var ch = '\0'; for (int i = 0; i < 128; i++) { _byteToChar[i] = ch++; } } } #endregion /// /// Uploads the specified stream to the remote host. /// /// Stream to upload. /// Remote host file name. public void Upload(Stream source, string path) { using (var input = new PipeStream()) using (var channel = this.Session.CreateClientChannel()) { channel.DataReceived += delegate(object sender, ChannelDataEventArgs e) { input.Write(e.Data, 0, e.Data.Length); input.Flush(); }; channel.Open(); int pathEnd = path.LastIndexOfAny(new[] { '\\', '/' }); if (pathEnd != -1) { // split the path from the file string pathOnly = path.Substring(0, pathEnd); string fileOnly = path.Substring(pathEnd + 1); // Send channel command request channel.SendExecRequest(string.Format("scp -t \"{0}\"", pathOnly)); this.CheckReturnCode(input); path = fileOnly; } this.InternalUpload(channel, input, source, path); channel.Close(); } } /// /// Downloads the specified file from the remote host to the stream. /// /// Remote host file name. /// The stream where to download remote file. /// is null or contains whitespace characters. /// is null. /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream. public void Download(string filename, Stream destination) { if (filename.IsNullOrWhiteSpace()) throw new ArgumentException("filename"); if (destination == null) throw new ArgumentNullException("destination"); using (var input = new PipeStream()) using (var channel = this.Session.CreateClientChannel()) { channel.DataReceived += delegate(object sender, ChannelDataEventArgs e) { input.Write(e.Data, 0, e.Data.Length); input.Flush(); }; channel.Open(); // Send channel command request channel.SendExecRequest(string.Format("scp -f \"{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.InternalDownload(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 InternalUpload(ChannelSession channel, Stream input, Stream source, string filename) { var length = source.Length; this.SendData(channel, string.Format("C0644 {0} {1}\n", length, Path.GetFileName(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 InternalDownload(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[] { errorCode }); this.SendData(channel, string.Format("{0}\n", message)); } /// /// Checks the return code. /// /// The output stream. 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; var 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(); } } }