You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1586 lines
57 KiB
C#

9 months ago
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<uint, SftpRequest> _requests = new Dictionary<uint, SftpRequest>();
//FIXME: obtain from SftpClient!
private readonly List<byte> _data = new List<byte>(32 * 1024);
private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false);
private IDictionary<string, string> _supportedExtensions;
/// <summary>
/// Gets the remote working directory.
/// </summary>
/// <value>
/// The remote working directory.
/// </value>
public string WorkingDirectory { get; private set; }
/// <summary>
/// Gets the SFTP protocol version.
/// </summary>
/// <value>
/// The SFTP protocol version.
/// </value>
public uint ProtocolVersion { get; private set; }
private long _requestId;
/// <summary>
/// Gets the next request id for sftp session.
/// </summary>
public uint NextRequestId
{
get
{
return (uint) Interlocked.Increment(ref _requestId);
}
}
public SftpSession(ISession session, int operationTimeout, Encoding encoding)
: base(session, "sftp", operationTimeout, encoding)
{
}
/// <summary>
/// Changes the current working directory to the specified path.
/// </summary>
/// <param name="path">The new working directory.</param>
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);
}
/// <summary>
/// Resolves a given path into an absolute path on the server.
/// </summary>
/// <param name="path">The path to resolve.</param>
/// <returns>
/// The absolute path.
/// </returns>
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
/// <summary>
/// Performs SSH_FXP_OPEN request
/// </summary>
/// <param name="path">The path.</param>
/// <param name="flags">The flags.</param>
/// <param name="nullOnError">if set to <c>true</c> returns <c>null</c> instead of throwing an exception.</param>
/// <returns>File handle.</returns>
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;
}
/// <summary>
/// Performs SSH_FXP_OPEN request
/// </summary>
/// <param name="path">The path.</param>
/// <param name="flags">The flags.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginOpen(string, Flags, AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SftpOpenAsyncResult"/> that represents the asynchronous call.
/// </returns>
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;
}
/// <summary>
/// Handles the end of an asynchronous open.
/// </summary>
/// <param name="asyncResult">An <see cref="SftpOpenAsyncResult"/> that represents an asynchronous call.</param>
/// <returns>
/// A <see cref="byte"/> array representing a file handle.
/// </returns>
/// <remarks>
/// If all available data has been read, the <see cref="EndOpen(SftpOpenAsyncResult)"/> method completes
/// immediately and returns zero bytes.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
/// <summary>
/// Performs SSH_FXP_CLOSE request.
/// </summary>
/// <param name="handle">The handle.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_CLOSE request.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginClose(byte[], AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SftpCloseAsyncResult"/> that represents the asynchronous call.
/// </returns>
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;
}
/// <summary>
/// Handles the end of an asynchronous close.
/// </summary>
/// <param name="asyncResult">An <see cref="SftpCloseAsyncResult"/> that represents an asynchronous call.</param>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
}
/// <summary>
/// Begins an asynchronous read using a SSH_FXP_READ request.
/// </summary>
/// <param name="handle">The handle to the file to read from.</param>
/// <param name="offset">The offset in the file to start reading from.</param>
/// <param name="length">The number of bytes to read.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRead(byte[], ulong, uint, AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SftpReadAsyncResult"/> that represents the asynchronous call.
/// </returns>
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<byte>.Empty, false);
}
});
SendRequest(request);
return asyncResult;
}
/// <summary>
/// Handles the end of an asynchronous read.
/// </summary>
/// <param name="asyncResult">An <see cref="SftpReadAsyncResult"/> that represents an asynchronous call.</param>
/// <returns>
/// A <see cref="byte"/> array representing the data read.
/// </returns>
/// <remarks>
/// If all available data has been read, the <see cref="EndRead(SftpReadAsyncResult)"/> method completes
/// immediately and returns zero bytes.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
/// <summary>
/// Performs SSH_FXP_READ request.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="offset">The offset.</param>
/// <param name="length">The length.</param>
/// <returns>data array; null if EOF</returns>
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<byte>.Empty;
}
wait.Set();
});
SendRequest(request);
WaitOnHandle(wait, OperationTimeout);
}
if (exception != null)
{
throw exception;
}
return data;
}
/// <summary>
/// Performs SSH_FXP_WRITE request.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="serverOffset">The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at.</param>
/// <param name="data">The buffer holding the data to write.</param>
/// <param name="offset">the zero-based offset in <paramref name="data" /> at which to begin taking bytes to write.</param>
/// <param name="length">The length (in bytes) of the data to write.</param>
/// <param name="wait">The wait event handle if needed.</param>
/// <param name="writeCompleted">The callback to invoke when the write has completed.</param>
public void RequestWrite(byte[] handle,
ulong serverOffset,
byte[] data,
int offset,
int length,
AutoResetEvent wait,
Action<SftpStatusResponse> 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;
}
}
/// <summary>
/// Performs SSH_FXP_LSTAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>
/// File attributes
/// </returns>
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;
}
/// <summary>
/// Performs SSH_FXP_LSTAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginLStat(string, AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
/// </returns>
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;
}
/// <summary>
/// Handles the end of an asynchronous SSH_FXP_LSTAT request.
/// </summary>
/// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
/// <returns>
/// The file attributes.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
/// <summary>
/// Performs SSH_FXP_FSTAT request.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="nullOnError">if set to <c>true</c> returns <c>null</c> instead of throwing an exception.</param>
/// <returns>
/// File attributes
/// </returns>
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;
}
/// <summary>
/// Performs SSH_FXP_SETSTAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="attributes">The attributes.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_FSETSTAT request.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="attributes">The attributes.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_OPENDIR request
/// </summary>
/// <param name="path">The path.</param>
/// <param name="nullOnError">if set to <c>true</c> returns <c>null</c> instead of throwing an exception.</param>
/// <returns>File handle.</returns>
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;
}
/// <summary>
/// Performs SSH_FXP_READDIR request
/// </summary>
/// <param name="handle">The handle.</param>
/// <returns></returns>
public KeyValuePair<string, SftpFileAttributes>[] RequestReadDir(byte[] handle)
{
SshException exception = null;
KeyValuePair<string, SftpFileAttributes>[] 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;
}
/// <summary>
/// Performs SSH_FXP_REMOVE request.
/// </summary>
/// <param name="path">The path.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_MKDIR request.
/// </summary>
/// <param name="path">The path.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_RMDIR request.
/// </summary>
/// <param name="path">The path.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_REALPATH request
/// </summary>
/// <param name="path">The path.</param>
/// <param name="nullOnError">if set to <c>true</c> returns null instead of throwing an exception.</param>
/// <returns>
/// The absolute path.
/// </returns>
internal KeyValuePair<string, SftpFileAttributes>[] RequestRealPath(string path, bool nullOnError = false)
{
SshException exception = null;
KeyValuePair<string, SftpFileAttributes>[] 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;
}
/// <summary>
/// Performs SSH_FXP_REALPATH request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRealPath(string, AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SftpRealPathAsyncResult"/> that represents the asynchronous call.
/// </returns>
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;
}
/// <summary>
/// Handles the end of an asynchronous SSH_FXP_REALPATH request.
/// </summary>
/// <param name="asyncResult">An <see cref="SftpRealPathAsyncResult"/> that represents an asynchronous call.</param>
/// <returns>
/// The absolute path.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
/// <summary>
/// Performs SSH_FXP_STAT request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="nullOnError">if set to <c>true</c> returns null instead of throwing an exception.</param>
/// <returns>
/// File attributes
/// </returns>
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;
}
/// <summary>
/// Performs SSH_FXP_STAT request
/// </summary>
/// <param name="path">The path.</param>
/// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginStat(string, AsyncCallback, object)"/> completes.</param>
/// <param name="state">An object that contains any additional user-defined data.</param>
/// <returns>
/// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
/// </returns>
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;
}
/// <summary>
/// Handles the end of an asynchronous stat.
/// </summary>
/// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
/// <returns>
/// The file attributes.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
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();
}
}
/// <summary>
/// Performs SSH_FXP_RENAME request.
/// </summary>
/// <param name="oldPath">The old path.</param>
/// <param name="newPath">The new path.</param>
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;
}
}
/// <summary>
/// Performs SSH_FXP_READLINK request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="nullOnError">if set to <c>true</c> returns null instead of throwing an exception.</param>
/// <returns></returns>
internal KeyValuePair<string, SftpFileAttributes>[] 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<string, SftpFileAttributes>[] 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;
}
/// <summary>
/// Performs SSH_FXP_SYMLINK request.
/// </summary>
/// <param name="linkpath">The linkpath.</param>
/// <param name="targetpath">The targetpath.</param>
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
/// <summary>
/// Performs posix-rename@openssh.com extended request.
/// </summary>
/// <param name="oldPath">The old path.</param>
/// <param name="newPath">The new path.</param>
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;
}
}
/// <summary>
/// Performs statvfs@openssh.com extended request.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="nullOnError">if set to <c>true</c> [null on error].</param>
/// <returns></returns>
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<StatVfsReplyInfo>().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;
}
/// <summary>
/// Performs fstatvfs@openssh.com extended request.
/// </summary>
/// <param name="handle">The file handle.</param>
/// <param name="nullOnError">if set to <c>true</c> [null on error].</param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
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<StatVfsReplyInfo>().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;
}
/// <summary>
/// Performs hardlink@openssh.com extended request.
/// </summary>
/// <param name="oldPath">The old path.</param>
/// <param name="newPath">The new path.</param>
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
/// <summary>
/// Calculates the optimal size of the buffer to read data from the channel.
/// </summary>
/// <param name="bufferSize">The buffer size configured on the client.</param>
/// <returns>
/// The optimal size of the buffer to read data from the channel.
/// </returns>
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;
}
/// <summary>
/// Calculates the optimal size of the buffer to write data on the channel.
/// </summary>
/// <param name="bufferSize">The buffer size configured on the client.</param>
/// <param name="handle">The file handle.</param>
/// <returns>
/// The optimal size of the buffer to write data on the channel.
/// </returns>
/// <remarks>
/// Currently, we do not take the remote window size into account.
/// </remarks>
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
// <handle>
// 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);
}
}
}