using System; using System.Text; using System.Threading; using Renci.SshNet.Common; using System.Collections.Generic; using System.Globalization; using Renci.SshNet.Sftp.Responses; using Renci.SshNet.Sftp.Requests; namespace Renci.SshNet.Sftp { internal class SftpSession : SubsystemSession, ISftpSession { private const int MaximumSupportedVersion = 3; private const int MinimumSupportedVersion = 0; private readonly Dictionary _requests = new Dictionary(); //FIXME: obtain from SftpClient! private readonly List _data = new List(32 * 1024); private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false); private IDictionary _supportedExtensions; /// /// Gets the remote working directory. /// /// /// The remote working directory. /// public string WorkingDirectory { get; private set; } /// /// Gets the SFTP protocol version. /// /// /// The SFTP protocol version. /// public uint ProtocolVersion { get; private set; } private long _requestId; /// /// Gets the next request id for sftp session. /// public uint NextRequestId { get { return (uint) Interlocked.Increment(ref _requestId); } } public SftpSession(ISession session, int operationTimeout, Encoding encoding) : base(session, "sftp", operationTimeout, encoding) { } /// /// Changes the current working directory to the specified path. /// /// The new working directory. public void ChangeDirectory(string path) { var fullPath = GetCanonicalPath(path); var handle = RequestOpenDir(fullPath); RequestClose(handle); WorkingDirectory = fullPath; } internal void SendMessage(SftpMessage sftpMessage) { var data = sftpMessage.GetBytes(); SendData(data); } /// /// Resolves a given path into an absolute path on the server. /// /// The path to resolve. /// /// The absolute path. /// public string GetCanonicalPath(string path) { var fullPath = GetFullRemotePath(path); var canonizedPath = string.Empty; var realPathFiles = RequestRealPath(fullPath, true); if (realPathFiles != null) { canonizedPath = realPathFiles[0].Key; } if (!string.IsNullOrEmpty(canonizedPath)) return canonizedPath; // Check for special cases if (fullPath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) || fullPath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) || fullPath.Equals("/", StringComparison.OrdinalIgnoreCase) || fullPath.IndexOf('/') < 0) return fullPath; var pathParts = fullPath.Split('/'); var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1); if (string.IsNullOrEmpty(partialFullPath)) partialFullPath = "/"; realPathFiles = RequestRealPath(partialFullPath, true); if (realPathFiles != null) { canonizedPath = realPathFiles[0].Key; } if (string.IsNullOrEmpty(canonizedPath)) { return fullPath; } var slash = string.Empty; if (canonizedPath[canonizedPath.Length - 1] != '/') slash = "/"; return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", canonizedPath, slash, pathParts[pathParts.Length - 1]); } public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize) { return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize); } internal string GetFullRemotePath(string path) { var fullPath = path; if (!string.IsNullOrEmpty(path) && path[0] != '/' && WorkingDirectory != null) { if (WorkingDirectory[WorkingDirectory.Length - 1] == '/') { fullPath = string.Format(CultureInfo.InvariantCulture, "{0}{1}", WorkingDirectory, path); } else { fullPath = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", WorkingDirectory, path); } } return fullPath; } protected override void OnChannelOpen() { SendMessage(new SftpInitRequest(MaximumSupportedVersion)); WaitOnHandle(_sftpVersionConfirmed, OperationTimeout); if (ProtocolVersion > MaximumSupportedVersion || ProtocolVersion < MinimumSupportedVersion) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Server SFTP version {0} is not supported.", ProtocolVersion)); } // Resolve current directory WorkingDirectory = RequestRealPath(".")[0].Key; } protected override void OnDataReceived(byte[] data) { const int packetLengthByteCount = 4; const int sftpMessageTypeByteCount = 1; const int minimumChannelDataLength = packetLengthByteCount + sftpMessageTypeByteCount; var offset = 0; var count = data.Length; // improve performance and reduce GC pressure by not buffering channel data if the received // chunk contains the complete packet data. // // for this, the buffer should be empty and the chunk should contain at least the packet length // and the type of the SFTP message if (_data.Count == 0) { while (count >= minimumChannelDataLength) { // extract packet length var packetDataLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]; var packetTotalLength = packetDataLength + packetLengthByteCount; // check if complete packet data (or more) is available if (count >= packetTotalLength) { // load and process SFTP message if (!TryLoadSftpMessage(data, offset + packetLengthByteCount, packetDataLength)) { return; } // remove processed bytes from the number of bytes to process as the channel // data we received may contain (part of) another message count -= packetTotalLength; // move offset beyond bytes we just processed offset += packetTotalLength; } else { // we don't have a complete message break; } } // check if there is channel data left to process or buffer if (count == 0) { return; } // check if we processed part of the channel data we received if (offset > 0) { // add (remaining) channel data to internal data holder var remainingChannelData = new byte[count]; Buffer.BlockCopy(data, offset, remainingChannelData, 0, count); _data.AddRange(remainingChannelData); } else { // add (remaining) channel data to internal data holder _data.AddRange(data); } // skip further processing as we'll need a new chunk to complete the message return; } // add (remaining) channel data to internal data holder _data.AddRange(data); while (_data.Count >= minimumChannelDataLength) { // extract packet length var packetDataLength = _data[0] << 24 | _data[1] << 16 | _data[2] << 8 | _data[3]; var packetTotalLength = packetDataLength + packetLengthByteCount; // check if complete packet data is available if (_data.Count < packetTotalLength) { // wait for complete message to arrive first break; } // create buffer to hold packet data var packetData = new byte[packetDataLength]; // copy packet data and bytes for length to array _data.CopyTo(packetLengthByteCount, packetData, 0, packetDataLength); // remove loaded data and bytes for length from _data holder if (_data.Count == packetTotalLength) { // the only buffered data is the data we're processing _data.Clear(); } else { // remove only the data we're processing _data.RemoveRange(0, packetTotalLength); } // load and process SFTP message if (!TryLoadSftpMessage(packetData, 0, packetDataLength)) { break; } } } private bool TryLoadSftpMessage(byte[] packetData, int offset, int count) { // Load SFTP Message and handle it var response = SftpMessage.Load(ProtocolVersion, packetData, offset, count, Encoding); try { var versionResponse = response as SftpVersionResponse; if (versionResponse != null) { ProtocolVersion = versionResponse.Version; _supportedExtensions = versionResponse.Extentions; _sftpVersionConfirmed.Set(); } else { HandleResponse(response as SftpResponse); } return true; } catch (Exception exp) { RaiseError(exp); return false; } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { var sftpVersionConfirmed = _sftpVersionConfirmed; if (sftpVersionConfirmed != null) { _sftpVersionConfirmed = null; sftpVersionConfirmed.Dispose(); } } } private void SendRequest(SftpRequest request) { lock (_requests) { _requests.Add(request.RequestId, request); } SendMessage(request); } #region SFTP API functions /// /// Performs SSH_FXP_OPEN request /// /// The path. /// The flags. /// if set to true returns null instead of throwing an exception. /// File handle. public byte[] RequestOpen(string path, Flags flags, bool nullOnError = false) { byte[] handle = null; SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags, response => { handle = response.Handle; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return handle; } /// /// Performs SSH_FXP_OPEN request /// /// The path. /// The flags. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback callback, object state) { var asyncResult = new SftpOpenAsyncResult(callback, state); var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags, response => { asyncResult.SetAsCompleted(response.Handle, false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous open. /// /// An that represents an asynchronous call. /// /// A array representing a file handle. /// /// /// If all available data has been read, the method completes /// immediately and returns zero bytes. /// /// is null. public byte[] EndOpen(SftpOpenAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndOpen has already been called."); if (asyncResult.IsCompleted) return asyncResult.EndInvoke(); using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); return asyncResult.EndInvoke(); } } /// /// Performs SSH_FXP_CLOSE request. /// /// The handle. public void RequestClose(byte[] handle) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpCloseRequest(ProtocolVersion, NextRequestId, handle, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_CLOSE request. /// /// The handle. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SftpCloseAsyncResult BeginClose(byte[] handle, AsyncCallback callback, object state) { var asyncResult = new SftpCloseAsyncResult(callback, state); var request = new SftpCloseRequest(ProtocolVersion, NextRequestId, handle, response => { asyncResult.SetAsCompleted(GetSftpException(response), false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous close. /// /// An that represents an asynchronous call. /// is null. public void EndClose(SftpCloseAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndClose has already been called."); if (asyncResult.IsCompleted) { asyncResult.EndInvoke(); } else { using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); asyncResult.EndInvoke(); } } } /// /// Begins an asynchronous read using a SSH_FXP_READ request. /// /// The handle to the file to read from. /// The offset in the file to start reading from. /// The number of bytes to read. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SftpReadAsyncResult BeginRead(byte[] handle, ulong offset, uint length, AsyncCallback callback, object state) { var asyncResult = new SftpReadAsyncResult(callback, state); var request = new SftpReadRequest(ProtocolVersion, NextRequestId, handle, offset, length, response => { asyncResult.SetAsCompleted(response.Data, false); }, response => { if (response.StatusCode != StatusCodes.Eof) { asyncResult.SetAsCompleted(GetSftpException(response), false); } else { asyncResult.SetAsCompleted(Array.Empty, false); } }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous read. /// /// An that represents an asynchronous call. /// /// A array representing the data read. /// /// /// If all available data has been read, the method completes /// immediately and returns zero bytes. /// /// is null. public byte[] EndRead(SftpReadAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndRead has already been called."); if (asyncResult.IsCompleted) return asyncResult.EndInvoke(); using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); return asyncResult.EndInvoke(); } } /// /// Performs SSH_FXP_READ request. /// /// The handle. /// The offset. /// The length. /// data array; null if EOF public byte[] RequestRead(byte[] handle, ulong offset, uint length) { SshException exception = null; byte[] data = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpReadRequest(ProtocolVersion, NextRequestId, handle, offset, length, response => { data = response.Data; wait.Set(); }, response => { if (response.StatusCode != StatusCodes.Eof) { exception = GetSftpException(response); } else { data = Array.Empty; } wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } return data; } /// /// Performs SSH_FXP_WRITE request. /// /// The handle. /// The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at. /// The buffer holding the data to write. /// the zero-based offset in at which to begin taking bytes to write. /// The length (in bytes) of the data to write. /// The wait event handle if needed. /// The callback to invoke when the write has completed. public void RequestWrite(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, AutoResetEvent wait, Action writeCompleted = null) { SshException exception = null; var request = new SftpWriteRequest(ProtocolVersion, NextRequestId, handle, serverOffset, data, offset, length, response => { if (writeCompleted != null) { writeCompleted(response); } exception = GetSftpException(response); if (wait != null) wait.Set(); }); SendRequest(request); if (wait != null) WaitOnHandle(wait, OperationTimeout); if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_LSTAT request. /// /// The path. /// /// File attributes /// public SftpFileAttributes RequestLStat(string path) { SshException exception = null; SftpFileAttributes attributes = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { attributes = response.Attributes; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } return attributes; } /// /// Performs SSH_FXP_LSTAT request. /// /// The path. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, object state) { var asyncResult = new SFtpStatAsyncResult(callback, state); var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { asyncResult.SetAsCompleted(response.Attributes, false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous SSH_FXP_LSTAT request. /// /// An that represents an asynchronous call. /// /// The file attributes. /// /// is null. public SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndLStat has already been called."); if (asyncResult.IsCompleted) return asyncResult.EndInvoke(); using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); return asyncResult.EndInvoke(); } } /// /// Performs SSH_FXP_FSTAT request. /// /// The handle. /// if set to true returns null instead of throwing an exception. /// /// File attributes /// public SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError) { SshException exception = null; SftpFileAttributes attributes = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpFStatRequest(ProtocolVersion, NextRequestId, handle, response => { attributes = response.Attributes; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null && !nullOnError) { throw exception; } return attributes; } /// /// Performs SSH_FXP_SETSTAT request. /// /// The path. /// The attributes. public void RequestSetStat(string path, SftpFileAttributes attributes) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpSetStatRequest(ProtocolVersion, NextRequestId, path, Encoding, attributes, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_FSETSTAT request. /// /// The handle. /// The attributes. public void RequestFSetStat(byte[] handle, SftpFileAttributes attributes) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpFSetStatRequest(ProtocolVersion, NextRequestId, handle, attributes, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_OPENDIR request /// /// The path. /// if set to true returns null instead of throwing an exception. /// File handle. public byte[] RequestOpenDir(string path, bool nullOnError = false) { SshException exception = null; byte[] handle = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpOpenDirRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { handle = response.Handle; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return handle; } /// /// Performs SSH_FXP_READDIR request /// /// The handle. /// public KeyValuePair[] RequestReadDir(byte[] handle) { SshException exception = null; KeyValuePair[] result = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpReadDirRequest(ProtocolVersion, NextRequestId, handle, response => { result = response.Files; wait.Set(); }, response => { if (response.StatusCode != StatusCodes.Eof) { exception = GetSftpException(response); } wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } return result; } /// /// Performs SSH_FXP_REMOVE request. /// /// The path. public void RequestRemove(string path) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpRemoveRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_MKDIR request. /// /// The path. public void RequestMkDir(string path) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpMkDirRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_RMDIR request. /// /// The path. public void RequestRmDir(string path) { SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpRmDirRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_REALPATH request /// /// The path. /// if set to true returns null instead of throwing an exception. /// /// The absolute path. /// internal KeyValuePair[] RequestRealPath(string path, bool nullOnError = false) { SshException exception = null; KeyValuePair[] result = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { result = response.Files; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return result; } /// /// Performs SSH_FXP_REALPATH request. /// /// The path. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SftpRealPathAsyncResult BeginRealPath(string path, AsyncCallback callback, object state) { var asyncResult = new SftpRealPathAsyncResult(callback, state); var request = new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { asyncResult.SetAsCompleted(response.Files[0].Key, false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous SSH_FXP_REALPATH request. /// /// An that represents an asynchronous call. /// /// The absolute path. /// /// is null. public string EndRealPath(SftpRealPathAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndRealPath has already been called."); if (asyncResult.IsCompleted) return asyncResult.EndInvoke(); using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); return asyncResult.EndInvoke(); } } /// /// Performs SSH_FXP_STAT request. /// /// The path. /// if set to true returns null instead of throwing an exception. /// /// File attributes /// public SftpFileAttributes RequestStat(string path, bool nullOnError = false) { SshException exception = null; SftpFileAttributes attributes = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { attributes = response.Attributes; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return attributes; } /// /// Performs SSH_FXP_STAT request /// /// The path. /// The delegate that is executed when completes. /// An object that contains any additional user-defined data. /// /// A that represents the asynchronous call. /// public SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object state) { var asyncResult = new SFtpStatAsyncResult(callback, state); var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { asyncResult.SetAsCompleted(response.Attributes, false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous stat. /// /// An that represents an asynchronous call. /// /// The file attributes. /// /// is null. public SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult) { if (asyncResult == null) throw new ArgumentNullException("asyncResult"); if (asyncResult.EndInvokeCalled) throw new InvalidOperationException("EndStat has already been called."); if (asyncResult.IsCompleted) return asyncResult.EndInvoke(); using (var waitHandle = asyncResult.AsyncWaitHandle) { WaitOnHandle(waitHandle, OperationTimeout); return asyncResult.EndInvoke(); } } /// /// Performs SSH_FXP_RENAME request. /// /// The old path. /// The new path. public void RequestRename(string oldPath, string newPath) { if (ProtocolVersion < 2) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_RENAME operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs SSH_FXP_READLINK request. /// /// The path. /// if set to true returns null instead of throwing an exception. /// internal KeyValuePair[] RequestReadLink(string path, bool nullOnError = false) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_READLINK operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; KeyValuePair[] result = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpReadLinkRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { result = response.Files; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return result; } /// /// Performs SSH_FXP_SYMLINK request. /// /// The linkpath. /// The targetpath. public void RequestSymLink(string linkpath, string targetpath) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_SYMLINK operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new SftpSymLinkRequest(ProtocolVersion, NextRequestId, linkpath, targetpath, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } #endregion #region SFTP Extended API functions /// /// Performs posix-rename@openssh.com extended request. /// /// The old path. /// The new path. public void RequestPosixRename(string oldPath, string newPath) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new PosixRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, Encoding, response => { exception = GetSftpException(response); wait.Set(); }); if (!_supportedExtensions.ContainsKey(request.Name)) throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name)); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } /// /// Performs statvfs@openssh.com extended request. /// /// The path. /// if set to true [null on error]. /// public SftpFileSytemInformation RequestStatVfs(string path, bool nullOnError = false) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; SftpFileSytemInformation information = null; using (var wait = new AutoResetEvent(false)) { var request = new StatVfsRequest(ProtocolVersion, NextRequestId, path, Encoding, response => { information = response.GetReply().Information; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); if (!_supportedExtensions.ContainsKey(request.Name)) throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name)); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return information; } /// /// Performs fstatvfs@openssh.com extended request. /// /// The file handle. /// if set to true [null on error]. /// /// internal SftpFileSytemInformation RequestFStatVfs(byte[] handle, bool nullOnError = false) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; SftpFileSytemInformation information = null; using (var wait = new AutoResetEvent(false)) { var request = new FStatVfsRequest(ProtocolVersion, NextRequestId, handle, response => { information = response.GetReply().Information; wait.Set(); }, response => { exception = GetSftpException(response); wait.Set(); }); if (!_supportedExtensions.ContainsKey(request.Name)) throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name)); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception != null) { throw exception; } return information; } /// /// Performs hardlink@openssh.com extended request. /// /// The old path. /// The new path. internal void HardLink(string oldPath, string newPath) { if (ProtocolVersion < 3) { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion)); } SshException exception = null; using (var wait = new AutoResetEvent(false)) { var request = new HardLinkRequest(ProtocolVersion, NextRequestId, oldPath, newPath, response => { exception = GetSftpException(response); wait.Set(); }); if (!_supportedExtensions.ContainsKey(request.Name)) throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name)); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception != null) { throw exception; } } #endregion /// /// Calculates the optimal size of the buffer to read data from the channel. /// /// The buffer size configured on the client. /// /// The optimal size of the buffer to read data from the channel. /// public uint CalculateOptimalReadLength(uint bufferSize) { // a SSH_FXP_DATA message has 13 bytes of protocol fields: // bytes 1 to 4: packet length // byte 5: message type // bytes 6 to 9: response id // bytes 10 to 13: length of payload‏ // // most ssh servers limit the size of the payload of a SSH_MSG_CHANNEL_DATA // response to 16 KB; if we requested 16 KB of data, then the SSH_FXP_DATA // payload of the SSH_MSG_CHANNEL_DATA message would be too big (16 KB + 13 bytes), and // as a result, the ssh server would split this into two responses: // one containing 16384 bytes (13 bytes header, and 16371 bytes file data) // and one with the remaining 13 bytes of file data const uint lengthOfNonDataProtocolFields = 13u; var maximumPacketSize = Channel.LocalPacketSize; return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields; } /// /// Calculates the optimal size of the buffer to write data on the channel. /// /// The buffer size configured on the client. /// The file handle. /// /// The optimal size of the buffer to write data on the channel. /// /// /// Currently, we do not take the remote window size into account. /// public uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle) { // 1-4: package length of SSH_FXP_WRITE message // 5: message type // 6-9: request id // 10-13: handle length // // 14-21: offset // 22-25: data length var lengthOfNonDataProtocolFields = 25u + (uint)handle.Length; var maximumPacketSize = Channel.RemotePacketSize; return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields; } private static SshException GetSftpException(SftpStatusResponse response) { switch (response.StatusCode) { case StatusCodes.Ok: return null; case StatusCodes.PermissionDenied: return new SftpPermissionDeniedException(response.ErrorMessage); case StatusCodes.NoSuchFile: return new SftpPathNotFoundException(response.ErrorMessage); default: return new SshException(response.ErrorMessage); } } private void HandleResponse(SftpResponse response) { SftpRequest request; lock (_requests) { _requests.TryGetValue(response.ResponseId, out request); if (request != null) { _requests.Remove(response.ResponseId); } } if (request == null) throw new InvalidOperationException("Invalid response."); request.Complete(response); } } }