using System; using System.Text; using Renci.SshNet.Channels; using System.IO; using Renci.SshNet.Common; using System.Text.RegularExpressions; using System.Diagnostics.CodeAnalysis; using System.Net; namespace Renci.SshNet { /// /// Provides SCP client functionality. /// /// /// More information on the SCP protocol is available here: /// https://github.com/net-ssh/net-scp/blob/master/lib/net/scp.rb /// 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 only 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 only whitespace characters. public ScpClient(string host, string username, string password) : this(host, ConnectionInfo.DefaultPort, 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 only 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 only whitespace characters. public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles) : this(host, ConnectionInfo.DefaultPort, 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) : this(connectionInfo, ownsConnectionInfo, new ServiceFactory()) { } /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// The factory to use for creating new services. /// is null. /// is null. /// /// If is true, then the /// connection info will be disposed when this instance is disposed. /// internal ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory) : base(connectionInfo, ownsConnectionInfo, serviceFactory) { OperationTimeout = SshNet.Session.InfiniteTimeSpan; BufferSize = 1024 * 16; if (_byteToChar == null) { _byteToChar = new char[128]; var ch = '\0'; for (var 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 = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); var pathEnd = path.LastIndexOfAny(new[] { '\\', '/' }); if (pathEnd != -1) { // split the path from the file var pathOnly = path.Substring(0, pathEnd); var fileOnly = path.Substring(pathEnd + 1); // Send channel command request channel.SendExecRequest(string.Format("scp -t \"{0}\"", pathOnly)); CheckReturnCode(input); path = fileOnly; } InternalUpload(channel, input, source, path); } } /// /// 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 only 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 = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); // Send channel command request channel.SendExecRequest(string.Format("scp -f \"{0}\"", filename)); SendConfirmation(channel); // Send reply var message = ReadString(input); var match = FileInfoRe.Match(message); if (match.Success) { // Read file SendConfirmation(channel); // Send reply var length = long.Parse(match.Result("${length}")); var fileName = match.Result("${filename}"); InternalDownload(channel, input, destination, fileName, length); } else { SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message)); } } } private static void InternalSetTimestamp(IChannelSession 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; SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds)); CheckReturnCode(input); } private void InternalUpload(IChannelSession channel, Stream input, Stream source, string filename) { var length = source.Length; SendData(channel, string.Format("C0644 {0} {1}\n", length, Path.GetFileName(filename))); CheckReturnCode(input); var buffer = new byte[BufferSize]; var read = source.Read(buffer, 0, buffer.Length); long totalRead = 0; while (read > 0) { SendData(channel, buffer, read); totalRead += read; RaiseUploadingEvent(filename, length, totalRead); read = source.Read(buffer, 0, buffer.Length); } SendConfirmation(channel); CheckReturnCode(input); } private void InternalDownload(IChannel channel, Stream input, Stream output, string filename, long length) { var buffer = new byte[Math.Min(length, BufferSize)]; var needToRead = length; do { var read = input.Read(buffer, 0, (int) Math.Min(needToRead, BufferSize)); output.Write(buffer, 0, read); RaiseDownloadingEvent(filename, length, length - needToRead); needToRead -= read; } while (needToRead > 0); output.Flush(); // Raise one more time when file downloaded RaiseDownloadingEvent(filename, length, length - needToRead); // Send confirmation byte after last data byte was read SendConfirmation(channel); CheckReturnCode(input); } private void RaiseDownloadingEvent(string filename, long size, long downloaded) { if (Downloading != null) { Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded)); } } private void RaiseUploadingEvent(string filename, long size, long uploaded) { if (Uploading != null) { Uploading(this, new ScpUploadEventArgs(filename, size, uploaded)); } } private static void SendConfirmation(IChannel channel) { SendData(channel, new byte[] { 0 }); } private static void SendConfirmation(IChannel channel, byte errorCode, string message) { SendData(channel, new[] { errorCode }); SendData(channel, string.Format("{0}\n", message)); } /// /// Checks the return code. /// /// The output stream. private static void CheckReturnCode(Stream input) { var b = ReadByte(input); if (b > 0) { var errorText = ReadString(input); throw new ScpException(errorText); } } private static void SendData(IChannel channel, string command) { channel.SendData(SshData.Utf8.GetBytes(command)); } private static void SendData(IChannel channel, byte[] buffer, int length) { channel.SendData(buffer, 0, length); } private static void SendData(IChannel channel, byte[] buffer) { channel.SendData(buffer); } private static int ReadByte(Stream stream) { var b = stream.ReadByte(); if (b == -1) throw new SshException("Stream has been closed."); 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(); } } }