using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text; using System.Threading; using System.Threading.Tasks; using Renci.SshNet.Common; using Renci.SshNet.Sftp.Requests; using Renci.SshNet.Sftp.Responses; namespace Renci.SshNet.Sftp { /// /// Represents an SFTP session. /// internal sealed class SftpSession : SubsystemSession, ISftpSession { internal const int MaximumSupportedVersion = 3; private const int MinimumSupportedVersion = 0; private readonly Dictionary _requests = new Dictionary(); private readonly ISftpResponseFactory _sftpResponseFactory; private readonly List _data = new List(32 * 1024); private readonly Encoding _encoding; private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(initialState: 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); } } /// /// Initializes a new instance of the class. /// /// The SSH session. /// The operation timeout. /// The character encoding to use. /// The factory to create SFTP responses. public SftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpResponseFactory) : base(session, "sftp", operationTimeout) { _encoding = encoding; _sftpResponseFactory = sftpResponseFactory; } /// /// 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; } /// /// Asynchronously requests to change the current working directory to the specified path. /// /// The new working directory. /// The token to monitor for cancellation requests. /// A that tracks the asynchronous change working directory request. public async Task ChangeDirectoryAsync(string path, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var fullPath = await GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false); var handle = await RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false); await RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false); 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, nullOnError: 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) || #if NET || NETSTANDARD2_1 fullPath.IndexOf('/', StringComparison.OrdinalIgnoreCase) < 0) #else fullPath.IndexOf('/') < 0) #endif { return fullPath; } var pathParts = fullPath.Split('/'); #if NET || NETSTANDARD2_1 var partialFullPath = string.Join('/', pathParts, 0, pathParts.Length - 1); #else var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1); #endif if (string.IsNullOrEmpty(partialFullPath)) { partialFullPath = "/"; } realPathFiles = RequestRealPath(partialFullPath, nullOnError: 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]); } /// /// Asynchronously resolves a given path into an absolute path on the server. /// /// The path to resolve. /// The token to monitor for cancellation requests. /// /// A task representing the absolute path. /// public async Task GetCanonicalPathAsync(string path, CancellationToken cancellationToken) { var fullPath = GetFullRemotePath(path); var canonizedPath = string.Empty; var realPathFiles = await RequestRealPathAsync(fullPath, nullOnError: true, cancellationToken).ConfigureAwait(false); if (realPathFiles != null) { canonizedPath = realPathFiles[0].Key; } if (!string.IsNullOrEmpty(canonizedPath)) { return canonizedPath; } // Check for special cases if (fullPath.EndsWith("/.", StringComparison.Ordinal) || fullPath.EndsWith("/..", StringComparison.Ordinal) || fullPath.Equals("/", StringComparison.Ordinal) || #if NET || NETSTANDARD2_1 fullPath.IndexOf('/', StringComparison.Ordinal) < 0) #else fullPath.IndexOf('/') < 0) #endif { return fullPath; } var pathParts = fullPath.Split('/'); #if NET || NETSTANDARD2_1 var partialFullPath = string.Join('/', pathParts); #else var partialFullPath = string.Join("/", pathParts); #endif if (string.IsNullOrEmpty(partialFullPath)) { partialFullPath = "/"; } realPathFiles = await RequestRealPathAsync(partialFullPath, nullOnError: true, cancellationToken).ConfigureAwait(false); if (realPathFiles != null) { canonizedPath = realPathFiles[0].Key; } if (string.IsNullOrEmpty(canonizedPath)) { return fullPath; } var slash = string.Empty; if (canonizedPath[canonizedPath.Length - 1] != '/') { slash = "/"; } return canonizedPath + slash + pathParts[pathParts.Length - 1]; } /// /// Creates an for reading the content of the file represented by a given . /// /// The handle of the file to read. /// The SFTP session. /// The maximum number of bytes to read with each chunk. /// The maximum number of pending reads. /// The size of the file or when the size could not be determined. /// /// An for reading the content of the file represented by the /// specified . /// 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 = WorkingDirectory + path; } else { fullPath = WorkingDirectory + '/' + path; } } return fullPath; } protected override void OnChannelOpen() { SendMessage(new SftpInitRequest(MaximumSupportedVersion)); WaitOnHandle(_sftpVersionConfirmed, OperationTimeout); if (ProtocolVersion is > MaximumSupportedVersion or < 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) { // Create SFTP message var response = _sftpResponseFactory.Create(ProtocolVersion, packetData[offset], _encoding); // Load message data into it response.Load(packetData, offset + 1, count - 1); try { if (response is SftpVersionResponse versionResponse) { 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); } /// /// Performs SSH_FXP_OPEN request. /// /// The path. /// The flags. /// If set to returns 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(initialState: false)) { var request = new SftpOpenRequest(ProtocolVersion, NextRequestId, path, _encoding, flags, response => { handle = response.Handle; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not null) { throw exception; } return handle; } /// /// Asynchronously performs a SSH_FXP_OPEN request. /// /// The path. /// The flags. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_OPEN request. The value of its /// contains the file handle of the specified path. /// public Task RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpOpenRequest(ProtocolVersion, NextRequestId, path, _encoding, flags, response => tcs.TrySetResult(response.Handle), response => tcs.TrySetException(GetSftpException(response)))); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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, completedSynchronously: false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: 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 . public byte[] EndOpen(SftpOpenAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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(initialState: false)) { var request = new SftpCloseRequest(ProtocolVersion, NextRequestId, handle, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// Performs a SSH_FXP_CLOSE request. /// /// The handle. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_CLOSE request. /// public Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpCloseRequest(ProtocolVersion, NextRequestId, handle, response => { if (response.StatusCode == StatusCodes.Ok) { _ = tcs.TrySetResult(true); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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), completedSynchronously: false); }); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous close. /// /// An that represents an asynchronous call. /// is . public void EndClose(SftpCloseAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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, completedSynchronously: false); }, response => { if (response.StatusCode != StatusCodes.Eof) { asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false); } else { asyncResult.SetAsCompleted(Array.Empty(), completedSynchronously: 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 . public byte[] EndRead(SftpReadAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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. /// /// The data that was read, or an empty array when the end of the file was reached. /// public byte[] RequestRead(byte[] handle, ulong offset, uint length) { SshException exception = null; byte[] data = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpReadRequest(ProtocolVersion, NextRequestId, handle, offset, length, response => { data = response.Data; wait.SetIgnoringObjectDisposed(); }, response => { if (response.StatusCode != StatusCodes.Eof) { exception = GetSftpException(response); } else { data = Array.Empty(); } wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } return data; } /// /// Asynchronously performs 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 token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_READ request. The value of /// its contains the data read from the file, or an empty /// array when the end of the file is reached. /// public Task RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpReadRequest(ProtocolVersion, NextRequestId, handle, offset, length, response => tcs.TrySetResult(response.Data), response => { if (response.StatusCode == StatusCodes.Eof) { _ = tcs.TrySetResult(Array.Empty()); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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) { Debug.Assert((wait is null) != (writeCompleted is null), "Should have one parameter or the other."); SshException exception = null; var request = new SftpWriteRequest(ProtocolVersion, NextRequestId, handle, serverOffset, data, offset, length, response => { if (writeCompleted is not null) { writeCompleted.Invoke(response); } else { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); } }); SendRequest(request); if (wait is not null) { WaitOnHandle(wait, OperationTimeout); if (exception is not null) { throw exception; } } } /// /// Asynchronouly performs a 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 token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_WRITE request. /// public Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpWriteRequest(ProtocolVersion, NextRequestId, handle, serverOffset, data, offset, length, response => { if (response.StatusCode == StatusCodes.Ok) { _ = tcs.TrySetResult(true); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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(initialState: false)) { var request = new SftpLStatRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { attributes = response.Attributes; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } return attributes; } /// /// Asynchronously performs SSH_FXP_LSTAT request. /// /// The path. /// The token to monitor for cancellation requests. /// /// A task the represents the asynchronous SSH_FXP_LSTAT request. The value of its /// contains the file attributes of the specified path. /// public Task RequestLStatAsync(string path, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpLStatRequest(ProtocolVersion, NextRequestId, path, _encoding, response => tcs.TrySetResult(response.Attributes), response => tcs.TrySetException(GetSftpException(response)))); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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, completedSynchronously: false); }, response => { asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: 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 . public SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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 , returns 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(initialState: false)) { var request = new SftpFStatRequest(ProtocolVersion, NextRequestId, handle, response => { attributes = response.Attributes; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not null) { throw exception; } return attributes; } /// /// Asynchronously performs a SSH_FXP_FSTAT request. /// /// The handle. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_FSTAT request. The value of its /// contains the file attributes of the specified handle. /// public Task RequestFStatAsync(byte[] handle, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpFStatRequest(ProtocolVersion, NextRequestId, handle, response => tcs.TrySetResult(response.Attributes), response => tcs.TrySetException(GetSftpException(response)))); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_SETSTAT request. /// /// The path. /// The attributes. public void RequestSetStat(string path, SftpFileAttributes attributes) { SshException exception = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpSetStatRequest(ProtocolVersion, NextRequestId, path, _encoding, attributes, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not 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(initialState: false)) { var request = new SftpFSetStatRequest(ProtocolVersion, NextRequestId, handle, attributes, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// Performs SSH_FXP_OPENDIR request. /// /// The path. /// If set to , returns 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(initialState: false)) { var request = new SftpOpenDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { handle = response.Handle; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not null) { throw exception; } return handle; } /// /// Asynchronously performs a SSH_FXP_OPENDIR request. /// /// The path. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_OPENDIR request. The value of its /// contains the handle of the specified path. /// public Task RequestOpenDirAsync(string path, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpOpenDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => tcs.TrySetResult(response.Handle), response => tcs.TrySetException(GetSftpException(response)))); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_READDIR request. /// /// The handle of the directory to read. /// /// A where the key is the name of a file in /// the directory and the value is the of the file. /// public KeyValuePair[] RequestReadDir(byte[] handle) { SshException exception = null; KeyValuePair[] result = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpReadDirRequest(ProtocolVersion, NextRequestId, handle, response => { result = response.Files; wait.SetIgnoringObjectDisposed(); }, response => { if (response.StatusCode != StatusCodes.Eof) { exception = GetSftpException(response); } wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } return result; } /// /// Performs a SSH_FXP_READDIR request. /// /// The handle of the directory to read. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_READDIR request. The value of its /// contains a where the /// key is the name of a file in the directory and the value is the /// of the file. /// public Task[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled[]>(cancellationToken); } var tcs = new TaskCompletionSource[]>(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpReadDirRequest(ProtocolVersion, NextRequestId, handle, response => tcs.TrySetResult(response.Files), response => { if (response.StatusCode == StatusCodes.Eof) { _ = tcs.TrySetResult(null); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_REMOVE request. /// /// The path. public void RequestRemove(string path) { SshException exception = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpRemoveRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// Asynchronously performs a SSH_FXP_REMOVE request. /// /// The path. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_REMOVE request. /// public Task RequestRemoveAsync(string path, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpRemoveRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { if (response.StatusCode == StatusCodes.Ok) { _ = tcs.TrySetResult(true); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_MKDIR request. /// /// The path. public void RequestMkDir(string path) { SshException exception = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpMkDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// Asynchronously performs SSH_FXP_MKDIR request. /// /// The path. /// The to observe. /// A that represents the asynchronous SSH_FXP_MKDIR operation. public Task RequestMkDirAsync(string path, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpMkDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { if (response.StatusCode == StatusCodes.Ok) { _ = tcs.TrySetResult(true); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_RMDIR request. /// /// The path. public void RequestRmDir(string path) { SshException exception = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new SftpRmDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// public Task RequestRmDirAsync(string path, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpRmDirRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { var exception = GetSftpException(response); if (exception is not null) { _ = tcs.TrySetException(exception); } else { _ = tcs.TrySetResult(true); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_REALPATH request. /// /// The path. /// if set to 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(initialState: false)) { var request = new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { result = response.Files; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not null) { throw exception; } return result; } internal Task[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled[]>(cancellationToken); } var tcs = new TaskCompletionSource[]>(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, _encoding, response => tcs.TrySetResult(response.Files), response => { if (nullOnError) { _ = tcs.TrySetResult(null); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// 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, completedSynchronously: false), response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: 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 . public string EndRealPath(SftpRealPathAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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 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(initialState: false)) { var request = new SftpStatRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { attributes = response.Attributes; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not 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, completedSynchronously: false), response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false)); SendRequest(request); return asyncResult; } /// /// Handles the end of an asynchronous stat. /// /// An that represents an asynchronous call. /// /// The file attributes. /// /// is . public SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult) { ThrowHelper.ThrowIfNull(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(initialState: false)) { var request = new SftpRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// Asynchronously performs a SSH_FXP_RENAME request. /// /// The old path. /// The new path. /// The token to monitor for cancellation requests. /// /// A task that represents the asynchronous SSH_FXP_RENAME request. /// public Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new SftpRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, _encoding, response => { if (response.StatusCode == StatusCodes.Ok) { _ = tcs.TrySetResult(true); } else { _ = tcs.TrySetException(GetSftpException(response)); } })); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs SSH_FXP_READLINK request. /// /// The path. /// if set to returns instead of throwing an exception. /// /// An array of where the key is the name of /// a file and the value is the of the file. /// 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(initialState: false)) { var request = new SftpReadLinkRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { result = response.Files; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (!nullOnError && exception is not 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(initialState: false)) { var request = new SftpSymLinkRequest(ProtocolVersion, NextRequestId, linkpath, targetpath, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); SendRequest(request); WaitOnHandle(wait, OperationTimeout); } if (exception is not null) { throw exception; } } /// /// 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(initialState: false)) { var request = new PosixRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, _encoding, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); 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 is not null) { throw exception; } } /// /// Performs statvfs@openssh.com extended request. /// /// The path. /// if set to [null on error]. /// /// A for the specified path. /// public SftpFileSystemInformation 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; SftpFileSystemInformation information = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new StatVfsRequest(ProtocolVersion, NextRequestId, path, _encoding, response => { information = response.GetReply().Information; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); 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 is not null) { throw exception; } return information; } /// /// Asynchronously performs a statvfs@openssh.com extended request. /// /// The path. /// The token to monitor for cancellation requests. /// /// A task that represents the statvfs@openssh.com extended request. The value of its /// contains the file system information for the specified /// path. /// public Task RequestStatVfsAsync(string path, CancellationToken cancellationToken) { 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)); } if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); SendRequest(new StatVfsRequest(ProtocolVersion, NextRequestId, path, _encoding, response => tcs.TrySetResult(response.GetReply().Information), response => tcs.TrySetException(GetSftpException(response)))); return WaitOnHandleAsync(tcs, OperationTimeout, cancellationToken); } /// /// Performs fstatvfs@openssh.com extended request. /// /// The file handle. /// if set to [null on error]. /// /// A for the specified path. /// /// This operation is not supported for the current SFTP protocol version. internal SftpFileSystemInformation 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; SftpFileSystemInformation information = null; using (var wait = new AutoResetEvent(initialState: false)) { var request = new FStatVfsRequest(ProtocolVersion, NextRequestId, handle, response => { information = response.GetReply().Information; wait.SetIgnoringObjectDisposed(); }, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); 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 is not 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(initialState: false)) { var request = new HardLinkRequest(ProtocolVersion, NextRequestId, oldPath, newPath, response => { exception = GetSftpException(response); wait.SetIgnoringObjectDisposed(); }); 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 is not null) { throw exception; } } /// /// 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 // // WinSCP uses a payload length of 32755 bytes // // 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 /* * Putty uses data length of 4096 bytes * WinSCP uses data length of 32739 bytes (total 32768 bytes; 32739 + 25 + 4 bytes for handle) */ var lengthOfNonDataProtocolFields = 25u + (uint)handle.Length; var maximumPacketSize = Channel.RemotePacketSize; return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields; } internal static SshException GetSftpException(SftpStatusResponse response) { #pragma warning disable IDE0010 // Add missing cases 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); } #pragma warning restore IDE0010 // Add missing cases } private void HandleResponse(SftpResponse response) { SftpRequest request; lock (_requests) { _ = _requests.TryGetValue(response.ResponseId, out request); if (request is not null) { _ = _requests.Remove(response.ResponseId); } } if (request is null) { throw new InvalidOperationException("Invalid response."); } request.Complete(response); } } }