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#

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

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);
}
}
}