Selaa lähdekoodia

Add async support to SftpClient and SftpFileStream (#819)

* Add FEATURE_TAP and net472 target
* Add TAP async support to SftpClient and SftpFileStream
* Add async support to DnsAbstraction and SocketAbstraction
* Add async support to *Connector and refactor the hierarchy
* Add ConnectAsync to BaseClient
Igor Milavec 3 vuotta sitten
vanhempi
sitoutus
7bdfc9e615

+ 22 - 0
src/Renci.SshNet/Abstractions/DnsAbstraction.cs

@@ -2,6 +2,10 @@
 using System.Net;
 using System.Net.Sockets;
 
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
+
 #if FEATURE_DNS_SYNC
 #elif FEATURE_DNS_APM
 using Renci.SshNet.Common;
@@ -87,5 +91,23 @@ namespace Renci.SshNet.Abstractions
 #endif // FEATURE_DEVICEINFORMATION_APM
 #endif
         }
+
+#if FEATURE_TAP
+        /// <summary>
+        /// Returns the Internet Protocol (IP) addresses for the specified host.
+        /// </summary>
+        /// <param name="hostNameOrAddress">The host name or IP address to resolve</param>
+        /// <returns>
+        /// A task with result of an array of type <see cref="IPAddress"/> that holds the IP addresses for the host that
+        /// is specified by the <paramref name="hostNameOrAddress"/> parameter.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="hostNameOrAddress"/> is <c>null</c>.</exception>
+        /// <exception cref="SocketException">An error is encountered when resolving <paramref name="hostNameOrAddress"/>.</exception>
+        public static Task<IPAddress[]> GetHostAddressesAsync(string hostNameOrAddress)
+        {
+            return Dns.GetHostAddressesAsync(hostNameOrAddress);
+        }
+#endif
+
     }
 }

+ 17 - 0
src/Renci.SshNet/Abstractions/SocketAbstraction.cs

@@ -3,6 +3,9 @@ using System.Globalization;
 using System.Net;
 using System.Net.Sockets;
 using System.Threading;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
 
@@ -59,6 +62,13 @@ namespace Renci.SshNet.Abstractions
             ConnectCore(socket, remoteEndpoint, connectTimeout, false);
         }
 
+#if FEATURE_TAP
+        public static Task ConnectAsync(Socket socket, IPEndPoint remoteEndpoint, CancellationToken cancellationToken)
+        {
+            return socket.ConnectAsync(remoteEndpoint, cancellationToken);
+        }
+#endif
+
         private static void ConnectCore(Socket socket, IPEndPoint remoteEndpoint, TimeSpan connectTimeout, bool ownsSocket)
         {
 #if FEATURE_SOCKET_EAP
@@ -317,6 +327,13 @@ namespace Renci.SshNet.Abstractions
             return buffer;
         }
 
+#if FEATURE_TAP
+        public static Task<int> ReadAsync(Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken)
+        {
+            return socket.ReceiveAsync(buffer, offset, length, cancellationToken);
+        }
+#endif
+
         /// <summary>
         /// Receives data from a bound <see cref="Socket"/> into a receive buffer.
         /// </summary>

+ 119 - 0
src/Renci.SshNet/Abstractions/SocketExtensions.cs

@@ -0,0 +1,119 @@
+#if FEATURE_TAP
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Renci.SshNet.Abstractions
+{
+    // Async helpers based on https://devblogs.microsoft.com/pfxteam/awaiting-socket-operations/
+
+    internal static class SocketExtensions
+    {
+        sealed class SocketAsyncEventArgsAwaitable : SocketAsyncEventArgs, INotifyCompletion
+        {
+            private readonly static Action SENTINEL = () => { };
+
+            private bool isCancelled;
+            private Action continuationAction;
+
+            public SocketAsyncEventArgsAwaitable()
+            {
+                Completed += delegate { SetCompleted(); };
+            }
+
+            public SocketAsyncEventArgsAwaitable ExecuteAsync(Func<SocketAsyncEventArgs, bool> func)
+            {
+                if (!func(this))
+                {
+                    SetCompleted();
+                }
+                return this;
+            }
+
+            public void SetCompleted()
+            {
+                IsCompleted = true;
+                var continuation = continuationAction ?? Interlocked.CompareExchange(ref continuationAction, SENTINEL, null);
+                if (continuation != null)
+                {
+                    continuation();
+                }
+            }
+
+            public void SetCancelled()
+            {
+                isCancelled = true;
+                SetCompleted();
+            }
+
+            public SocketAsyncEventArgsAwaitable GetAwaiter() { return this; }
+
+            public bool IsCompleted { get; private set; }
+
+            void INotifyCompletion.OnCompleted(Action continuation)
+            {
+                if (continuationAction == SENTINEL || Interlocked.CompareExchange(ref continuationAction, continuation, null) == SENTINEL)
+                {
+                    // We have already completed; run continuation asynchronously
+                    Task.Run(continuation);
+                }
+            }
+
+            public void GetResult()
+            {
+                if (isCancelled)
+                {
+                    throw new TaskCanceledException();
+                }
+                else if (IsCompleted)
+                {
+                    if (SocketError != SocketError.Success)
+                    {
+                        throw new SocketException((int)SocketError);
+                    }
+                }
+                else
+                {
+                    // We don't support sync/async
+                    throw new InvalidOperationException("The asynchronous operation has not yet completed.");
+                }
+            }
+        }
+
+        public static async Task ConnectAsync(this Socket socket, IPEndPoint remoteEndpoint, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            using (var args = new SocketAsyncEventArgsAwaitable())
+            {
+                args.RemoteEndPoint = remoteEndpoint;
+
+                using (cancellationToken.Register(o => ((SocketAsyncEventArgsAwaitable)o).SetCancelled(), args, false))
+                {
+                    await args.ExecuteAsync(socket.ConnectAsync);
+                }
+            }
+        }
+
+        public static async Task<int> ReceiveAsync(this Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            using (var args = new SocketAsyncEventArgsAwaitable())
+            {
+                args.SetBuffer(buffer, offset, length);
+
+                using (cancellationToken.Register(o => ((SocketAsyncEventArgsAwaitable)o).SetCancelled(), args, false))
+                {
+                    await args.ExecuteAsync(socket.ReceiveAsync);
+                }
+
+                return args.BytesTransferred;
+            }
+        }
+    }
+}
+#endif

+ 80 - 0
src/Renci.SshNet/BaseClient.cs

@@ -1,6 +1,9 @@
 using System;
 using System.Net.Sockets;
 using System.Threading;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
@@ -239,6 +242,63 @@ namespace Renci.SshNet
             StartKeepAliveTimer();
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously connects client to the server.
+        /// </summary>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous connect operation.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">The client is already connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
+        /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
+        /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
+        /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
+        public async Task ConnectAsync(CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            cancellationToken.ThrowIfCancellationRequested();
+
+            // TODO (see issue #1758):
+            // we're not stopping the keep-alive timer and disposing the session here
+            // 
+            // we could do this but there would still be side effects as concrete
+            // implementations may still hang on to the original session
+            // 
+            // therefore it would be better to actually invoke the Disconnect method
+            // (and then the Dispose on the session) but even that would have side effects
+            // eg. it would remove all forwarded ports from SshClient
+            // 
+            // I think we should modify our concrete clients to better deal with a
+            // disconnect. In case of SshClient this would mean not removing the 
+            // forwarded ports on disconnect (but only on dispose ?) and link a
+            // forwarded port with a client instead of with a session
+            //
+            // To be discussed with Oleg (or whoever is interested)
+            if (IsSessionConnected())
+                throw new InvalidOperationException("The client is already connected.");
+
+            OnConnecting();
+
+            Session = await CreateAndConnectSessionAsync(cancellationToken).ConfigureAwait(false);
+            try
+            {
+                // Even though the method we invoke makes you believe otherwise, at this point only
+                // the SSH session itself is connected.
+                OnConnected();
+            }
+            catch
+            {
+                // Only dispose the session as Disconnect() would have side-effects (such as remove forwarded
+                // ports in SshClient).
+                DisposeSession();
+                throw;
+            }
+            StartKeepAliveTimer();
+        }
+#endif
+
         /// <summary>
         /// Disconnects client from the server.
         /// </summary>
@@ -473,6 +533,26 @@ namespace Renci.SshNet
             }
         }
 
+#if FEATURE_TAP
+        private async Task<ISession> CreateAndConnectSessionAsync(CancellationToken cancellationToken)
+        {
+            var session = _serviceFactory.CreateSession(ConnectionInfo, _serviceFactory.CreateSocketFactory());
+            session.HostKeyReceived += Session_HostKeyReceived;
+            session.ErrorOccured += Session_ErrorOccured;
+
+            try
+            {
+                await session.ConnectAsync(cancellationToken).ConfigureAwait(false);
+                return session;
+            }
+            catch
+            {
+                DisposeSession(session);
+                throw;
+            }
+        }
+#endif
+
         private void DisposeSession(ISession session)
         {
             session.ErrorOccured -= Session_ErrorOccured;

+ 45 - 0
src/Renci.SshNet/Connection/ConnectorBase.cs

@@ -4,6 +4,11 @@ using Renci.SshNet.Messages.Transport;
 using System;
 using System.Net;
 using System.Net.Sockets;
+using System.Threading;
+
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet.Connection
 {
@@ -21,6 +26,10 @@ namespace Renci.SshNet.Connection
 
         public abstract Socket Connect(IConnectionInfo connectionInfo);
 
+#if FEATURE_TAP
+        public abstract Task<Socket> ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Establishes a socket connection to the specified host and port.
         /// </summary>
@@ -54,6 +63,42 @@ namespace Renci.SshNet.Connection
             }
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Establishes a socket connection to the specified host and port.
+        /// </summary>
+        /// <param name="host">The host name of the server to connect to.</param>
+        /// <param name="port">The port to connect to.</param>
+        /// <param name="cancellationToken">The cancellation token to observe.</param>
+        /// <exception cref="SshOperationTimeoutException">The connection failed to establish within the configured <see cref="ConnectionInfo.Timeout"/>.</exception>
+        /// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
+        protected async Task<Socket> SocketConnectAsync(string host, int port, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var ipAddress = (await DnsAbstraction.GetHostAddressesAsync(host).ConfigureAwait(false))[0];
+            var ep = new IPEndPoint(ipAddress, port);
+
+            DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}:{1}'.", host, port));
+
+            var socket = SocketFactory.Create(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
+            try
+            {
+                await SocketAbstraction.ConnectAsync(socket, ep, cancellationToken).ConfigureAwait(false);
+
+                const int socketBufferSize = 2 * Session.MaximumSshPacketSize;
+                socket.SendBufferSize = socketBufferSize;
+                socket.ReceiveBufferSize = socketBufferSize;
+                return socket;
+            }
+            catch (Exception)
+            {
+                socket.Dispose();
+                throw;
+            }
+        }
+#endif
+
         protected static byte SocketReadByte(Socket socket)
         {
             var buffer = new byte[1];

+ 9 - 1
src/Renci.SshNet/Connection/DirectConnector.cs

@@ -1,8 +1,9 @@
 using System.Net.Sockets;
+using System.Threading;
 
 namespace Renci.SshNet.Connection
 {
-    internal class DirectConnector : ConnectorBase
+    internal sealed class DirectConnector : ConnectorBase
     {
         public DirectConnector(ISocketFactory socketFactory) : base(socketFactory)
         {
@@ -12,5 +13,12 @@ namespace Renci.SshNet.Connection
         {
             return SocketConnect(connectionInfo.Host, connectionInfo.Port, connectionInfo.Timeout);
         }
+
+#if FEATURE_TAP
+        public override System.Threading.Tasks.Task<Socket> ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken)
+        {
+            return SocketConnectAsync(connectionInfo.Host, connectionInfo.Port, cancellationToken);
+        }
+#endif
     }
 }

+ 3 - 20
src/Renci.SshNet/Connection/HttpConnector.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Net;
 using System.Net.Sockets;
 using System.Text.RegularExpressions;
+using System.Threading;
 
 namespace Renci.SshNet.Connection
 {
@@ -27,31 +28,13 @@ namespace Renci.SshNet.Connection
     ///   </item>
     /// </list>
     /// </remarks>
-    internal class HttpConnector : ConnectorBase
+    internal sealed class HttpConnector : ProxyConnector
     {
         public HttpConnector(ISocketFactory socketFactory) : base(socketFactory)
         {
         }
 
-        public override Socket Connect(IConnectionInfo connectionInfo)
-        {
-            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
-
-            try
-            {
-                HandleProxyConnect(connectionInfo, socket);
-                return socket;
-            }
-            catch (Exception)
-            {
-                socket.Shutdown(SocketShutdown.Both);
-                socket.Dispose();
-
-                throw;
-            }
-        }
-
-        private void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
+        protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
         {
             var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
             var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");

+ 5 - 0
src/Renci.SshNet/Connection/IConnector.cs

@@ -1,9 +1,14 @@
 using System.Net.Sockets;
+using System.Threading;
 
 namespace Renci.SshNet.Connection
 {
     internal interface IConnector
     {
         Socket Connect(IConnectionInfo connectionInfo);
+
+#if FEATURE_TAP
+        System.Threading.Tasks.Task<Socket> ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken);
+#endif
     }
 }

+ 4 - 0
src/Renci.SshNet/Connection/IProtocolVersionExchange.cs

@@ -18,5 +18,9 @@ namespace Renci.SshNet.Connection
         /// The SSH identification of the server.
         /// </returns>
         SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout);
+
+#if FEATURE_TAP
+        System.Threading.Tasks.Task<SshIdentification> StartAsync(string clientVersion, Socket socket, System.Threading.CancellationToken cancellationToken);
+#endif
     }
 }

+ 103 - 0
src/Renci.SshNet/Connection/ProtocolVersionExchange.cs

@@ -7,6 +7,10 @@ using System.Globalization;
 using System.Net.Sockets;
 using System.Text;
 using System.Text.RegularExpressions;
+using System.Threading;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet.Connection
 {
@@ -78,6 +82,51 @@ namespace Renci.SshNet.Connection
             }
         }
 
+#if FEATURE_TAP
+        public async Task<SshIdentification> StartAsync(string clientVersion, Socket socket, CancellationToken cancellationToken)
+        {
+            // Immediately send the identification string since the spec states both sides MUST send an identification string
+            // when the connection has been established
+            SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A"));
+
+            var bytesReceived = new List<byte>();
+
+            // Get server version from the server,
+            // ignore text lines which are sent before if any
+            while (true)
+            {
+                var line = await SocketReadLineAsync(socket, cancellationToken, bytesReceived).ConfigureAwait(false);
+                if (line == null)
+                {
+                    if (bytesReceived.Count == 0)
+                    {
+                        throw new SshConnectionException(string.Format("The server response does not contain an SSH identification string.{0}" +
+                                                                       "The connection to the remote server was closed before any data was received.{0}{0}" +
+                                                                       "More information on the Protocol Version Exchange is available here:{0}" +
+                                                                       "https://tools.ietf.org/html/rfc4253#section-4.2",
+                                                                       Environment.NewLine),
+                                                         DisconnectReason.ConnectionLost);
+                    }
+
+                    throw new SshConnectionException(string.Format("The server response does not contain an SSH identification string:{0}{0}{1}{0}{0}" +
+                                                                   "More information on the Protocol Version Exchange is available here:{0}" +
+                                                                   "https://tools.ietf.org/html/rfc4253#section-4.2",
+                                                                   Environment.NewLine,
+                                                                   PacketDump.Create(bytesReceived, 2)),
+                                                     DisconnectReason.ProtocolError);
+                }
+
+                var identificationMatch = ServerVersionRe.Match(line);
+                if (identificationMatch.Success)
+                {
+                    return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"),
+                                                 GetGroupValue(identificationMatch, "softwareversion"),
+                                                 GetGroupValue(identificationMatch, "comments"));
+                }
+            }
+        }
+#endif
+
         private static string GetGroupValue(Match match, string groupName)
         {
             var commentsGroup = match.Groups[groupName];
@@ -153,5 +202,59 @@ namespace Renci.SshNet.Connection
 
             return null;
         }
+
+#if FEATURE_TAP
+        private static async Task<string> SocketReadLineAsync(Socket socket, CancellationToken cancellationToken, List<byte> buffer)
+        {
+            var data = new byte[1];
+
+            var startPosition = buffer.Count;
+
+            // Read data one byte at a time to find end of line and leave any unhandled information in the buffer
+            // to be processed by subsequent invocations.
+            while (true)
+            {
+                var bytesRead = await SocketAbstraction.ReadAsync(socket, data, 0, data.Length, cancellationToken).ConfigureAwait(false);
+                if (bytesRead == 0)
+                {
+                    throw new SshConnectionException("The connection was closed by the remote host.");
+                }
+
+                var byteRead = data[0];
+                buffer.Add(byteRead);
+
+                // The null character MUST NOT be sent
+                if (byteRead == Null)
+                {
+                    throw new SshConnectionException(string.Format(CultureInfo.InvariantCulture,
+                                                                   "The server response contains a null character at position 0x{0:X8}:{1}{1}{2}{1}{1}" +
+                                                                   "A server must not send a null character before the Protocol Version Exchange is complete.{1}{1}" +
+                                                                   "More information is available here:{1}" +
+                                                                   "https://tools.ietf.org/html/rfc4253#section-4.2",
+                                                                   buffer.Count,
+                                                                   Environment.NewLine,
+                                                                   PacketDump.Create(buffer.ToArray(), 2)));
+                }
+
+                if (byteRead == Session.LineFeed)
+                {
+                    if (buffer.Count > startPosition + 1 && buffer[buffer.Count - 2] == Session.CarriageReturn)
+                    {
+                        // Return current line without CRLF
+                        return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 2));
+                    }
+                    else
+                    {
+                        // Even though RFC4253 clearly indicates that the identification string should be terminated
+                        // by a CR LF we also support banners and identification strings that are terminated by a LF
+
+                        // Return current line without LF
+                        return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 1));
+                    }
+                }
+            }
+        }
+#endif
+
     }
 }

+ 74 - 0
src/Renci.SshNet/Connection/ProxyConnector.cs

@@ -0,0 +1,74 @@
+#if !FEATURE_SOCKET_DISPOSE
+using Renci.SshNet.Common;
+#endif
+using System;
+using System.Net.Sockets;
+#if FEATURE_TAP
+using System.Threading;
+using System.Threading.Tasks;
+#endif
+
+namespace Renci.SshNet.Connection
+{
+    internal abstract class ProxyConnector : ConnectorBase
+    {
+        public ProxyConnector(ISocketFactory socketFactory) :
+            base(socketFactory)
+        {
+        }
+
+        protected abstract void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket);
+
+#if FEATURE_TAP
+        // ToDo: Performs async/sync fallback, true async version should be implemented in derived classes
+        protected virtual Task HandleProxyConnectAsync(IConnectionInfo connectionInfo, Socket socket, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            using (cancellationToken.Register(o => ((Socket)o).Dispose(), socket, false))
+            {
+                HandleProxyConnect(connectionInfo, socket);
+            }
+            return Task.CompletedTask;
+        }
+#endif
+
+        public override Socket Connect(IConnectionInfo connectionInfo)
+        {
+            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
+
+            try
+            {
+                HandleProxyConnect(connectionInfo, socket);
+                return socket;
+            }
+            catch (Exception)
+            {
+                socket.Shutdown(SocketShutdown.Both);
+                socket.Dispose();
+
+                throw;
+            }
+        }
+
+#if FEATURE_TAP
+        public override async Task<Socket> ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken)
+        {
+            var socket = await SocketConnectAsync(connectionInfo.ProxyHost, connectionInfo.ProxyPort, cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                await HandleProxyConnectAsync(connectionInfo, socket, cancellationToken).ConfigureAwait(false);
+                return socket;
+            }
+            catch (Exception)
+            {
+                socket.Shutdown(SocketShutdown.Both);
+                socket.Dispose();
+
+                throw;
+            }
+        }
+#endif
+    }
+}

+ 2 - 20
src/Renci.SshNet/Connection/Socks4Connector.cs

@@ -12,36 +12,18 @@ namespace Renci.SshNet.Connection
     /// <remarks>
     /// https://www.openssh.com/txt/socks4.protocol
     /// </remarks>
-    internal class Socks4Connector : ConnectorBase
+    internal sealed class Socks4Connector : ProxyConnector
     {
         public Socks4Connector(ISocketFactory socketFactory) : base(socketFactory)
         {
         }
 
-        public override Socket Connect(IConnectionInfo connectionInfo)
-        {
-            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
-
-            try
-            {
-                HandleProxyConnect(connectionInfo, socket);
-                return socket;
-            }
-            catch (Exception)
-            {
-                socket.Shutdown(SocketShutdown.Both);
-                socket.Dispose();
-
-                throw;
-            }
-        }
-
         /// <summary>
         /// Establishes a connection to the server via a SOCKS5 proxy.
         /// </summary>
         /// <param name="connectionInfo">The connection information.</param>
         /// <param name="socket">The <see cref="Socket"/>.</param>
-        private void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
+        protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
         {
             var connectionRequest = CreateSocks4ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port, connectionInfo.ProxyUsername);
             SocketAbstraction.Send(socket, connectionRequest);

+ 2 - 20
src/Renci.SshNet/Connection/Socks5Connector.cs

@@ -11,36 +11,18 @@ namespace Renci.SshNet.Connection
     /// <remarks>
     /// https://en.wikipedia.org/wiki/SOCKS#SOCKS5
     /// </remarks>
-    internal class Socks5Connector : ConnectorBase
+    internal sealed class Socks5Connector : ProxyConnector
     {
         public Socks5Connector(ISocketFactory socketFactory) : base(socketFactory)
         {
         }
 
-        public override Socket Connect(IConnectionInfo connectionInfo)
-        {
-            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
-
-            try
-            {
-                HandleProxyConnect(connectionInfo, socket);
-                return socket;
-            }
-            catch (Exception)
-            {
-                socket.Shutdown(SocketShutdown.Both);
-                socket.Dispose();
-
-                throw;
-            }
-        }
-
         /// <summary>
         /// Establishes a connection to the server via a SOCKS5 proxy.
         /// </summary>
         /// <param name="connectionInfo">The connection information.</param>
         /// <param name="socket">The <see cref="Socket"/>.</param>
-        private void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
+        protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
         {
             var greeting = new byte[]
                 {

+ 16 - 0
src/Renci.SshNet/ISession.cs

@@ -6,6 +6,9 @@ using Renci.SshNet.Common;
 using Renci.SshNet.Messages;
 using Renci.SshNet.Messages.Authentication;
 using Renci.SshNet.Messages.Connection;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet
 {
@@ -54,6 +57,19 @@ namespace Renci.SshNet
         /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
         void Connect();
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously connects to the server.
+        /// </summary>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous connect operation.</returns>
+        /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
+        /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
+        /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
+        /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
+        Task ConnectAsync(CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Create a new SSH session channel.
         /// </summary>

+ 89 - 0
src/Renci.SshNet/ISftpClient.cs

@@ -4,6 +4,10 @@ using System.IO;
 using System.Text;
 using Renci.SshNet.Sftp;
 using Renci.SshNet.Common;
+#if FEATURE_TAP
+using System.Threading;
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet
 {
@@ -488,6 +492,22 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         void DeleteFile(string path);
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously deletes remote file specified by path.
+        /// </summary>
+        /// <param name="path">File to be deleted path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is <b>null</b> or contains only whitespace characters.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to delete the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        Task DeleteFileAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Downloads remote file specified by the path into the stream.
         /// </summary>
@@ -653,6 +673,22 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         SftpFileSytemInformation GetStatus(string path);
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously gets status using statvfs@openssh.com request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{SftpFileSytemInformation}"/> that represents the status operation.
+        /// The task result contains the <see cref="SftpFileSytemInformation"/> instance that contains file status information.
+        /// </returns>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        Task<SftpFileSytemInformation> GetStatusAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Retrieves list of files in remote directory.
         /// </summary>
@@ -668,6 +704,25 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         IEnumerable<ISftpFile> ListDirectory(string path, Action<int> listCallback = null);
 
+#if FEATURE_TAP
+
+        /// <summary>
+        /// Asynchronously retrieves list of files in remote directory.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
+        /// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        Task<IEnumerable<SftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Opens a <see cref="SftpFileStream"/> on the specified path with read/write access.
         /// </summary>
@@ -695,6 +750,24 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         SftpFileStream Open(string path, FileMode mode, FileAccess access);
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously opens a <see cref="SftpFileStream"/> on the specified path, with the specified mode and access.
+        /// </summary>
+        /// <param name="path">The file to open.</param>
+        /// <param name="mode">A <see cref="FileMode"/> value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten.</param>
+        /// <param name="access">A <see cref="FileAccess"/> value that specifies the operations that can be performed on the file.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{SftpFileStream}"/> that represents the asynchronous open operation.
+        /// The task result contains the <see cref="SftpFileStream"/> that provides access to the specified file, with the specified mode and access.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        Task<SftpFileStream> OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Opens an existing file for reading.
         /// </summary>
@@ -833,6 +906,22 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         void RenameFile(string oldPath, string newPath);
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously renames remote file from old path to new path.
+        /// </summary>
+        /// <param name="oldPath">Path to the old file location.</param>
+        /// <param name="newPath">Path to the new file location.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous rename operation.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="oldPath"/> is <b>null</b>. <para>-or-</para> or <paramref name="newPath"/> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        Task RenameFileAsync(string oldPath, string newPath, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Renames remote file from old path to new path.
         /// </summary>

+ 7 - 4
src/Renci.SshNet/Renci.SshNet.csproj

@@ -5,9 +5,9 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <AssemblyName>Renci.SshNet</AssemblyName>
     <AssemblyOriginatorKeyFile>../Renci.SshNet.snk</AssemblyOriginatorKeyFile>
-    <LangVersion>5</LangVersion>
+    <LangVersion>6</LangVersion>
     <SignAssembly>true</SignAssembly>
-    <TargetFrameworks>net35;net40;netstandard1.3;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>net35;net40;net472;netstandard1.3;netstandard2.0</TargetFrameworks>
   </PropertyGroup>
 
   <!--
@@ -38,10 +38,13 @@
   <PropertyGroup Condition=" '$(TargetFramework)' == 'net40' ">
     <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII;FEATURE_ECDSA</DefineConstants>
   </PropertyGroup>
+  <PropertyGroup Condition=" '$(TargetFramework)' == 'net472' ">
+    <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_REGEX_COMPILE;FEATURE_BINARY_SERIALIZATION;FEATURE_RNG_CREATE;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_EAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_STREAM_APM;FEATURE_DNS_SYNC;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HASH_RIPEMD160_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_HMAC_RIPEMD160;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_ENCODING_ASCII;FEATURE_ECDSA;FEATURE_TAP</DefineConstants>
+  </PropertyGroup>
   <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
-    <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_REFLECTION_TYPEINFO;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_TAP;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512</DefineConstants>
+    <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_REFLECTION_TYPEINFO;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_TAP;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_TAP</DefineConstants>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' ">
-    <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP;FEATURE_STREAM_APM;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_ECDSA</DefineConstants>
+    <DefineConstants>FEATURE_STRINGBUILDER_CLEAR;FEATURE_HASHALGORITHM_DISPOSE;FEATURE_ENCODING_ASCII;FEATURE_DIAGNOSTICS_TRACESOURCE;FEATURE_DIRECTORYINFO_ENUMERATEFILES;FEATURE_MEMORYSTREAM_GETBUFFER;FEATURE_MEMORYSTREAM_TRYGETBUFFER;FEATURE_RNG_CREATE;FEATURE_SOCKET_TAP;FEATURE_SOCKET_APM;FEATURE_SOCKET_EAP;FEATURE_SOCKET_SYNC;FEATURE_SOCKET_SELECT;FEATURE_SOCKET_POLL;FEATURE_SOCKET_DISPOSE;FEATURE_DNS_SYNC;FEATURE_DNS_APM;FEATURE_DNS_TAP;FEATURE_STREAM_APM;FEATURE_STREAM_TAP;FEATURE_THREAD_COUNTDOWNEVENT;FEATURE_THREAD_TAP;FEATURE_THREAD_THREADPOOL;FEATURE_THREAD_SLEEP;FEATURE_WAITHANDLE_DISPOSE;FEATURE_HASH_MD5;FEATURE_HASH_SHA1_CREATE;FEATURE_HASH_SHA256_CREATE;FEATURE_HASH_SHA384_CREATE;FEATURE_HASH_SHA512_CREATE;FEATURE_HMAC_MD5;FEATURE_HMAC_SHA1;FEATURE_HMAC_SHA256;FEATURE_HMAC_SHA384;FEATURE_HMAC_SHA512;FEATURE_ECDSA;FEATURE_TAP</DefineConstants>
   </PropertyGroup>
 </Project>

+ 109 - 0
src/Renci.SshNet/Session.cs

@@ -16,6 +16,9 @@ using System.Globalization;
 using System.Linq;
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Security.Cryptography;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet
 {
@@ -670,6 +673,112 @@ namespace Renci.SshNet
             }
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously connects to the server.
+        /// </summary>
+        /// <remarks>
+        /// Please note this function is NOT thread safe.<br/>
+        /// The caller SHOULD limit the number of simultaneous connection attempts to a server to a single connection attempt.</remarks>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous connect operation.</returns>
+        /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
+        /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
+        /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
+        /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
+        public async Task ConnectAsync(CancellationToken cancellationToken)
+        {
+            // If connected don't connect again
+            if (IsConnected)
+                return;
+
+            // Reset connection specific information
+            Reset();
+
+            // Build list of available messages while connecting
+            _sshMessageFactory = new SshMessageFactory();
+
+            _socket = await _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
+                                        .ConnectAsync(ConnectionInfo, cancellationToken).ConfigureAwait(false);
+
+            var serverIdentification = await _serviceFactory.CreateProtocolVersionExchange()
+                                                        .StartAsync(ClientVersion, _socket, cancellationToken).ConfigureAwait(false);
+
+            // Set connection versions
+            ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
+            ConnectionInfo.ClientVersion = ClientVersion;
+
+            DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", serverIdentification.ProtocolVersion, serverIdentification.SoftwareVersion));
+
+            if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
+            {
+                throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
+                                                    DisconnectReason.ProtocolVersionNotSupported);
+            }
+
+            // Register Transport response messages
+            RegisterMessage("SSH_MSG_DISCONNECT");
+            RegisterMessage("SSH_MSG_IGNORE");
+            RegisterMessage("SSH_MSG_UNIMPLEMENTED");
+            RegisterMessage("SSH_MSG_DEBUG");
+            RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
+            RegisterMessage("SSH_MSG_KEXINIT");
+            RegisterMessage("SSH_MSG_NEWKEYS");
+
+            // Some server implementations might sent this message first, prior to establishing encryption algorithm
+            RegisterMessage("SSH_MSG_USERAUTH_BANNER");
+
+            // Mark the message listener threads as started
+            _messageListenerCompleted.Reset();
+
+            // Start incoming request listener
+            // ToDo: Make message pump async, to not consume a thread for every session
+            ThreadAbstraction.ExecuteThreadLongRunning(() => MessageListener());
+
+            // Wait for key exchange to be completed
+            WaitOnHandle(_keyExchangeCompletedWaitHandle);
+
+            // If sessionId is not set then its not connected
+            if (SessionId == null)
+            {
+                Disconnect();
+                return;
+            }
+
+            // Request user authorization service
+            SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
+
+            // Wait for service to be accepted
+            WaitOnHandle(_serviceAccepted);
+
+            if (string.IsNullOrEmpty(ConnectionInfo.Username))
+            {
+                throw new SshException("Username is not specified.");
+            }
+
+            // Some servers send a global request immediately after successful authentication
+            // Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
+            RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
+
+            ConnectionInfo.Authenticate(this, _serviceFactory);
+            _isAuthenticated = true;
+
+            // Register Connection messages
+            RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
+            RegisterMessage("SSH_MSG_REQUEST_FAILURE");
+            RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
+            RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
+            RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
+            RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
+            RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
+            RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
+            RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
+            RegisterMessage("SSH_MSG_CHANNEL_DATA");
+            RegisterMessage("SSH_MSG_CHANNEL_EOF");
+            RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
+        }
+#endif
+
         /// <summary>
         /// Disconnects from the server.
         /// </summary>

+ 47 - 0
src/Renci.SshNet/Sftp/ISftpSession.cs

@@ -2,6 +2,9 @@
 using System.Collections.Generic;
 using System.Threading;
 using Renci.SshNet.Sftp.Responses;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet.Sftp
 {
@@ -38,6 +41,10 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         string GetCanonicalPath(string path);
 
+#if FEATURE_TAP
+        Task<string> GetCanonicalPathAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_FSTAT request.
         /// </summary>
@@ -48,6 +55,10 @@ namespace Renci.SshNet.Sftp
         /// </returns>
         SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError);
 
+#if FEATURE_TAP
+        Task<SftpFileAttributes> RequestFStatAsync(byte[] handle, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_STAT request.
         /// </summary>
@@ -124,6 +135,10 @@ namespace Renci.SshNet.Sftp
         /// <returns>File handle.</returns>
         byte[] RequestOpen(string path, Flags flags, bool nullOnError = false);
 
+#if FEATURE_TAP
+        Task<byte[]> RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_OPEN request
         /// </summary>
@@ -158,6 +173,10 @@ namespace Renci.SshNet.Sftp
         /// <returns>File handle.</returns>
         byte[] RequestOpenDir(string path, bool nullOnError = false);
 
+#if FEATURE_TAP
+        Task<byte[]> RequestOpenDirAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs posix-rename@openssh.com extended request.
         /// </summary>
@@ -201,6 +220,10 @@ namespace Renci.SshNet.Sftp
         /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <c>null</c>.</exception>
         byte[] EndRead(SftpReadAsyncResult asyncResult);
 
+#if FEATURE_TAP
+        Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_READDIR request
         /// </summary>
@@ -208,6 +231,10 @@ namespace Renci.SshNet.Sftp
         /// <returns></returns>
         KeyValuePair<string, SftpFileAttributes>[] RequestReadDir(byte[] handle);
 
+#if FEATURE_TAP
+        Task<KeyValuePair<string, SftpFileAttributes>[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_REALPATH request.
         /// </summary>
@@ -235,6 +262,10 @@ namespace Renci.SshNet.Sftp
         /// <param name="path">The path.</param>
         void RequestRemove(string path);
 
+#if FEATURE_TAP
+        Task RequestRemoveAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_RENAME request.
         /// </summary>
@@ -242,6 +273,10 @@ namespace Renci.SshNet.Sftp
         /// <param name="newPath">The new path.</param>
         void RequestRename(string oldPath, string newPath);
 
+#if FEATURE_TAP
+        Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_RMDIR request.
         /// </summary>
@@ -263,6 +298,10 @@ namespace Renci.SshNet.Sftp
         /// <returns></returns>
         SftpFileSytemInformation RequestStatVfs(string path, bool nullOnError = false);
 
+#if FEATURE_TAP
+        Task<SftpFileSytemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_SYMLINK request.
         /// </summary>
@@ -295,12 +334,20 @@ namespace Renci.SshNet.Sftp
                           AutoResetEvent wait,
                           Action<SftpStatusResponse> writeCompleted = null);
 
+#if FEATURE_TAP
+        Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_CLOSE request.
         /// </summary>
         /// <param name="handle">The handle.</param>
         void RequestClose(byte[] handle);
 
+#if FEATURE_TAP
+        Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken);
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_CLOSE request.
         /// </summary>

+ 354 - 1
src/Renci.SshNet/Sftp/SftpFileStream.cs

@@ -3,12 +3,16 @@ using System.IO;
 using System.Threading;
 using System.Diagnostics.CodeAnalysis;
 using Renci.SshNet.Common;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet.Sftp
 {
     /// <summary>
     /// Exposes a <see cref="Stream"/> around a remote SFTP file, supporting both synchronous and asynchronous read and write operations.
     /// </summary>
+    /// <threadsafety static="true" instance="false"/>
     public class SftpFileStream : Stream
     {
         //  TODO:   Add security method to set userid, groupid and other permission settings
@@ -166,6 +170,28 @@ namespace Renci.SshNet.Sftp
         /// </value>
         public TimeSpan Timeout { get; set; }
 
+        private SftpFileStream(ISftpSession session, string path, FileAccess access, int bufferSize, byte[] handle, long position)
+        {
+            Timeout = TimeSpan.FromSeconds(30);
+            Name = path;
+
+            _session = session;
+            _canRead = (access & FileAccess.Read) != 0;
+            _canSeek = true;
+            _canWrite = (access & FileAccess.Write) != 0;
+
+            _handle = handle;
+
+            // instead of using the specified buffer size as is, we use it to calculate a buffer size
+            // that ensures we always receive or send the max. number of bytes in a single SSH_FXP_READ
+            // or SSH_FXP_WRITE message
+
+            _readBufferSize = (int)session.CalculateOptimalReadLength((uint)bufferSize);
+            _writeBufferSize = (int)session.CalculateOptimalWriteLength((uint)bufferSize, _handle);
+
+            _position = position;
+        }
+
         internal SftpFileStream(ISftpSession session, string path, FileMode mode, FileAccess access, int bufferSize)
         {
             if (session == null)
@@ -173,7 +199,7 @@ namespace Renci.SshNet.Sftp
             if (path == null)
                 throw new ArgumentNullException("path");
             if (bufferSize <= 0)
-                throw new ArgumentOutOfRangeException("bufferSize");
+                throw new ArgumentOutOfRangeException("bufferSize", "Cannot be less than or equal to zero.");
 
             Timeout = TimeSpan.FromSeconds(30);
             Name = path;
@@ -267,6 +293,105 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        internal static async Task<SftpFileStream> OpenAsync(ISftpSession session, string path, FileMode mode, FileAccess access, int bufferSize, CancellationToken cancellationToken)
+        {
+            if (session == null)
+                throw new SshConnectionException("Client not connected.");
+            if (path == null)
+                throw new ArgumentNullException("path");
+            if (bufferSize <= 0)
+                throw new ArgumentOutOfRangeException("bufferSize", "Cannot be less than or equal to zero.");
+
+            var flags = Flags.None;
+
+            switch (access)
+            {
+                case FileAccess.Read:
+                    flags |= Flags.Read;
+                    break;
+                case FileAccess.Write:
+                    flags |= Flags.Write;
+                    break;
+                case FileAccess.ReadWrite:
+                    flags |= Flags.Read;
+                    flags |= Flags.Write;
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException("access");
+            }
+
+            if ((access & FileAccess.Read) != 0 && mode == FileMode.Append)
+            {
+                throw new ArgumentException(string.Format("{0} mode can be requested only when combined with write-only access.", mode.ToString("G")));
+            }
+
+            if ((access & FileAccess.Write) == 0)
+            {
+                if (mode == FileMode.Create || mode == FileMode.CreateNew || mode == FileMode.Truncate || mode == FileMode.Append)
+                {
+                    throw new ArgumentException(string.Format("Combining {0}: {1} with {2}: {3} is invalid.",
+                        typeof(FileMode).Name,
+                        mode,
+                        typeof(FileAccess).Name,
+                        access));
+                }
+            }
+
+            byte[] handle = null;
+
+            switch (mode)
+            {
+                case FileMode.Append:
+                    flags |= Flags.Append | Flags.CreateNewOrOpen;
+                    break;
+                case FileMode.Create:
+                    flags |= Flags.CreateNewOrOpen | Flags.Truncate;
+                    break;
+                case FileMode.CreateNew:
+                    flags |= Flags.CreateNew;
+                    break;
+                case FileMode.Open:
+                    break;
+                case FileMode.OpenOrCreate:
+                    flags |= Flags.CreateNewOrOpen;
+                    break;
+                case FileMode.Truncate:
+                    flags |= Flags.Truncate;
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException("mode");
+            }
+
+            if (handle == null)
+                handle = await session.RequestOpenAsync(path, flags, cancellationToken).ConfigureAwait(false);
+
+            long position = 0;
+            if (mode == FileMode.Append)
+            {
+                try
+                {
+                    var attributes = await session.RequestFStatAsync(handle, cancellationToken).ConfigureAwait(false);
+                    position = attributes.Size;
+                }
+                catch
+                {
+                    try
+                    {
+                        await session.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
+                    }
+                    catch
+                    { 
+                        // The original exception is presumably more informative, so we just ignore this one.
+                    }
+                    throw;
+                }
+            }
+
+            return new SftpFileStream(session, path, access, bufferSize, handle, position);
+        }
+#endif
+
         /// <summary>
         /// Releases unmanaged resources and performs other cleanup operations before the
         /// <see cref="SftpFileStream"/> is reclaimed by garbage collection.
@@ -298,6 +423,31 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously clears all buffers for this stream and causes any buffered data to be written to the file.
+        /// </summary>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
+        /// <exception cref="IOException">An I/O error occurs. </exception>
+        /// <exception cref="ObjectDisposedException">Stream is closed.</exception>
+        public override Task FlushAsync(CancellationToken cancellationToken)
+        {
+            CheckSessionIsOpen();
+
+            if (_bufferOwnedByWrite)
+            {
+                return FlushWriteBufferAsync(cancellationToken);
+            }
+            else
+            {
+                FlushReadBuffer();
+            }
+
+            return Task.CompletedTask;
+        }
+#endif
+
         /// <summary>
         /// Reads a sequence of bytes from the current stream and advances the position within the stream by the
         /// number of bytes read.
@@ -438,6 +588,120 @@ namespace Renci.SshNet.Sftp
             return readLen;
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously reads a sequence of bytes from the current stream and advances the position within the stream by the
+        /// number of bytes read.
+        /// </summary>
+        /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
+        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
+        /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken" /> to observe.</param>
+        /// <returns>A <see cref="Task" /> that represents the asynchronous read operation.</returns>
+        public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            var readLen = 0;
+
+            if (buffer == null)
+                throw new ArgumentNullException("buffer");
+            if (offset < 0)
+                throw new ArgumentOutOfRangeException("offset");
+            if (count < 0)
+                throw new ArgumentOutOfRangeException("count");
+            if ((buffer.Length - offset) < count)
+                throw new ArgumentException("Invalid array range.");
+
+            CheckSessionIsOpen();
+
+            // Set up for the read operation.
+            SetupRead();
+
+            // Read data into the caller's buffer.
+            while (count > 0)
+            {
+                // How much data do we have available in the buffer?
+                var bytesAvailableInBuffer = _bufferLen - _bufferPosition;
+                if (bytesAvailableInBuffer <= 0)
+                {
+                    var data = await _session.RequestReadAsync(_handle, (ulong)_position, (uint)_readBufferSize, cancellationToken).ConfigureAwait(false);
+
+                    if (data.Length == 0)
+                    {
+                        _bufferPosition = 0;
+                        _bufferLen = 0;
+
+                        break;
+                    }
+
+                    var bytesToWriteToCallerBuffer = count;
+                    if (bytesToWriteToCallerBuffer >= data.Length)
+                    {
+                        // write all data read to caller-provided buffer
+                        bytesToWriteToCallerBuffer = data.Length;
+                        // reset buffer since we will skip buffering
+                        _bufferPosition = 0;
+                        _bufferLen = 0;
+                    }
+                    else
+                    {
+                        // determine number of bytes that we should write into read buffer
+                        var bytesToWriteToReadBuffer = data.Length - bytesToWriteToCallerBuffer;
+                        // write remaining bytes to read buffer
+                        Buffer.BlockCopy(data, count, GetOrCreateReadBuffer(), 0, bytesToWriteToReadBuffer);
+                        // update position in read buffer
+                        _bufferPosition = 0;
+                        // update number of bytes in read buffer
+                        _bufferLen = bytesToWriteToReadBuffer;
+                    }
+
+                    // write bytes to caller-provided buffer
+                    Buffer.BlockCopy(data, 0, buffer, offset, bytesToWriteToCallerBuffer);
+                    // update stream position
+                    _position += bytesToWriteToCallerBuffer;
+                    // record total number of bytes read into caller-provided buffer
+                    readLen += bytesToWriteToCallerBuffer;
+
+                    // break out of the read loop when the server returned less than the request number of bytes
+                    // as that *may* indicate that we've reached EOF
+                    //
+                    // doing this avoids reading from server twice to determine EOF: once in the read loop, and
+                    // once upon the next Read or ReadByte invocation by the caller
+                    if (data.Length < _readBufferSize)
+                    {
+                        break;
+                    }
+
+                    // advance offset to start writing bytes into caller-provided buffer
+                    offset += bytesToWriteToCallerBuffer;
+                    // update number of bytes left to read into caller-provided buffer
+                    count -= bytesToWriteToCallerBuffer;
+                }
+                else
+                {
+                    // limit the number of bytes to use from read buffer to the caller-request number of bytes
+                    if (bytesAvailableInBuffer > count)
+                        bytesAvailableInBuffer = count;
+
+                    // copy data from read buffer to the caller-provided buffer
+                    Buffer.BlockCopy(GetOrCreateReadBuffer(), _bufferPosition, buffer, offset, bytesAvailableInBuffer);
+                    // update position in read buffer
+                    _bufferPosition += bytesAvailableInBuffer;
+                    // update stream position
+                    _position += bytesAvailableInBuffer;
+                    // record total number of bytes read into caller-provided buffer
+                    readLen += bytesAvailableInBuffer;
+                    // advance offset to start writing bytes into caller-provided buffer
+                    offset += bytesAvailableInBuffer;
+                    // update number of bytes left to read
+                    count -= bytesAvailableInBuffer;
+                }
+            }
+
+            // return the number of bytes that were read to the caller.
+            return readLen;
+        }
+#endif
+
         /// <summary>
         /// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.
         /// </summary>
@@ -740,6 +1004,84 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
+        /// </summary>
+        /// <param name="buffer">An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.</param>
+        /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param>
+        /// <param name="count">The number of bytes to be written to the current stream.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous write operation.</returns>
+        /// <exception cref="ArgumentException">The sum of <paramref name="offset"/> and <paramref name="count"/> is greater than the buffer length.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <c>null</c>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> or <paramref name="count"/> is negative.</exception>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        /// <exception cref="NotSupportedException">The stream does not support writing.</exception>
+        /// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
+        public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            if (buffer == null)
+                throw new ArgumentNullException("buffer");
+            if (offset < 0)
+                throw new ArgumentOutOfRangeException("offset");
+            if (count < 0)
+                throw new ArgumentOutOfRangeException("count");
+            if ((buffer.Length - offset) < count)
+                throw new ArgumentException("Invalid array range.");
+
+            CheckSessionIsOpen();
+
+            // Setup this object for writing.
+            SetupWrite();
+
+            // Write data to the file stream.
+            while (count > 0)
+            {
+                // Determine how many bytes we can write to the buffer.
+                var tempLen = _writeBufferSize - _bufferPosition;
+                if (tempLen <= 0)
+                {
+                    // flush write buffer, and mark it empty
+                    await FlushWriteBufferAsync(cancellationToken).ConfigureAwait(false);
+                    // we can now write or buffer the full buffer size
+                    tempLen = _writeBufferSize;
+                }
+
+                // limit the number of bytes to write to the actual number of bytes requested
+                if (tempLen > count)
+                {
+                    tempLen = count;
+                }
+
+                // Can we short-cut the internal buffer?
+                if (_bufferPosition == 0 && tempLen == _writeBufferSize)
+                {
+                    await _session.RequestWriteAsync(_handle, (ulong)_position, buffer, offset, tempLen, cancellationToken).ConfigureAwait(false);
+                }
+                else
+                {
+                    // No: copy the data to the write buffer first.
+                    Buffer.BlockCopy(buffer, offset, GetOrCreateWriteBuffer(), _bufferPosition, tempLen);
+                    _bufferPosition += tempLen;
+                }
+
+                // Advance the buffer and stream positions.
+                _position += tempLen;
+                offset += tempLen;
+                count -= tempLen;
+            }
+
+            // If the buffer is full, then do a speculative flush now,
+            // rather than waiting for the next call to this method.
+            if (_bufferPosition >= _writeBufferSize)
+            {
+                await _session.RequestWriteAsync(_handle, (ulong)(_position - _bufferPosition), GetOrCreateWriteBuffer(), 0, _bufferPosition, cancellationToken).ConfigureAwait(false);
+                _bufferPosition = 0;
+            }
+        }
+#endif
+
         /// <summary>
         /// Writes a byte to the current position in the stream and advances the position within the stream by one byte.
         /// </summary>
@@ -857,6 +1199,17 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        private async Task FlushWriteBufferAsync(CancellationToken cancellationToken)
+        {
+            if (_bufferPosition > 0)
+            {
+                await _session.RequestWriteAsync(_handle, (ulong)(_position - _bufferPosition), _writeBuffer, 0, _bufferPosition, cancellationToken);
+                _bufferPosition = 0;
+            }
+        }
+#endif
+
         /// <summary>
         /// Setups the read.
         /// </summary>

+ 329 - 4
src/Renci.SshNet/Sftp/SftpSession.cs

@@ -6,6 +6,9 @@ using System.Collections.Generic;
 using System.Globalization;
 using Renci.SshNet.Sftp.Responses;
 using Renci.SshNet.Sftp.Requests;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet.Sftp
 {
@@ -136,6 +139,54 @@ namespace Renci.SshNet.Sftp
             return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", canonizedPath, slash, pathParts[pathParts.Length - 1]);
         }
 
+#if FEATURE_TAP
+        public async Task<string> GetCanonicalPathAsync(string path, CancellationToken cancellationToken)
+        {
+            var fullPath = GetFullRemotePath(path);
+
+            var canonizedPath = string.Empty;
+            var realPathFiles = await RequestRealPathAsync(fullPath, 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) ||
+                fullPath.IndexOf('/') < 0)
+                return fullPath;
+
+            var pathParts = fullPath.Split('/');
+
+            var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1);
+
+            if (string.IsNullOrEmpty(partialFullPath))
+                partialFullPath = "/";
+
+            realPathFiles = await RequestRealPathAsync(partialFullPath, 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];
+        }
+#endif
+
         public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
         {
             return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
@@ -149,11 +200,11 @@ namespace Renci.SshNet.Sftp
             {
                 if (WorkingDirectory[WorkingDirectory.Length - 1] == '/')
                 {
-                    fullPath = string.Format(CultureInfo.InvariantCulture, "{0}{1}", WorkingDirectory, path);
+                    fullPath = WorkingDirectory + path;
                 }
                 else
                 {
-                    fullPath = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", WorkingDirectory, path);
+                    fullPath = WorkingDirectory + '/' + path;
                 }
             }
             return fullPath;
@@ -385,6 +436,24 @@ namespace Renci.SshNet.Sftp
             return handle;
         }
 
+#if FEATURE_TAP
+        public async Task<byte[]> RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpOpenRequest(ProtocolVersion, NextRequestId, path, Encoding, flags,
+                    response => tcs.TrySetResult(response.Handle),
+                    response => tcs.TrySetException(GetSftpException(response))));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_OPEN request
         /// </summary>
@@ -472,6 +541,34 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        public async Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken)
+        {
+            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            SendRequest(new SftpCloseRequest(ProtocolVersion, NextRequestId, handle,
+                response =>
+                {
+                    if (response.StatusCode == StatusCodes.Ok)
+                    {
+                        tcs.TrySetResult(true);
+                    }
+                    else
+                    {
+                        tcs.TrySetException(GetSftpException(response));
+                    }
+                }));
+
+            // Only check for cancellation after the SftpCloseRequest was sent
+            cancellationToken.ThrowIfCancellationRequested();
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<bool>)s).TrySetCanceled(), tcs, false))
+            {
+                await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
+
         /// <summary>
         /// Performs SSH_FXP_CLOSE request.
         /// </summary>
@@ -635,6 +732,34 @@ namespace Renci.SshNet.Sftp
             return data;
         }
 
+#if FEATURE_TAP
+        public async Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpReadRequest(ProtocolVersion, NextRequestId, handle, offset, length,
+                    response => tcs.TrySetResult(response.Data),
+                    response =>
+                    {
+                        if (response.StatusCode == StatusCodes.Eof)
+                        {
+                            tcs.TrySetResult(Array<byte>.Empty);
+                        }
+                        else
+                        {
+                            tcs.TrySetException(GetSftpException(response));
+                        }
+                    }));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_WRITE request.
         /// </summary>
@@ -679,6 +804,33 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        public async Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<bool>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpWriteRequest(ProtocolVersion, NextRequestId, handle, serverOffset, data, offset, length,
+                    response =>
+                    {
+                        if (response.StatusCode == StatusCodes.Ok)
+                        {
+                            tcs.TrySetResult(true);
+                        }
+                        else
+                        {
+                            tcs.TrySetException(GetSftpException(response));
+                        }
+                    }));
+
+                await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_LSTAT request.
         /// </summary>
@@ -811,6 +963,24 @@ namespace Renci.SshNet.Sftp
             return attributes;
         }
 
+#if FEATURE_TAP
+        public async Task<SftpFileAttributes> RequestFStatAsync(byte[] handle, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<SftpFileAttributes> tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpFStatRequest(ProtocolVersion, NextRequestId, handle,
+                    response => tcs.TrySetResult(response.Attributes),
+                    response => tcs.TrySetException(GetSftpException(response))));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_SETSTAT request.
         /// </summary>
@@ -908,6 +1078,24 @@ namespace Renci.SshNet.Sftp
             return handle;
         }
 
+#if FEATURE_TAP
+        public async Task<byte[]> RequestOpenDirAsync(string path, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpOpenDirRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                    response => tcs.TrySetResult(response.Handle),
+                    response => tcs.TrySetException(GetSftpException(response))));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_READDIR request
         /// </summary>
@@ -949,6 +1137,35 @@ namespace Renci.SshNet.Sftp
             return result;
         }
 
+#if FEATURE_TAP
+        public async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]> tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(), tcs, false))
+            {
+                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 await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
+
         /// <summary>
         /// Performs SSH_FXP_REMOVE request.
         /// </summary>
@@ -977,6 +1194,34 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+#if FEATURE_TAP
+        public async Task RequestRemoveAsync(string path, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<bool>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpRemoveRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                    response =>
+                    {
+                        if (response.StatusCode == StatusCodes.Ok)
+                        {
+                            tcs.TrySetResult(true);
+                        }
+                        else
+                        {
+                            tcs.TrySetException(GetSftpException(response));
+                        }
+                    }));
+
+                await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
+
         /// <summary>
         /// Performs SSH_FXP_MKDIR request.
         /// </summary>
@@ -1070,10 +1315,38 @@ namespace Renci.SshNet.Sftp
             {
                 throw exception;
             }
-            
+
             return result;
         }
 
+#if FEATURE_TAP
+        internal async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]> tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpRealPathRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                    response => tcs.TrySetResult(response.Files),
+                    response =>
+                    {
+                        if (nullOnError)
+                        {
+                            tcs.TrySetResult(null);
+                        }
+                        else
+                        {
+                            tcs.TrySetException(GetSftpException(response));
+                        }
+                    }));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_REALPATH request.
         /// </summary>
@@ -1255,6 +1528,34 @@ namespace Renci.SshNet.Sftp
             }
         }
 
+
+#if FEATURE_TAP
+        public async Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<bool>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new SftpRenameRequest(ProtocolVersion, NextRequestId, oldPath, newPath, Encoding,
+                    response =>
+                    {
+                        if (response.StatusCode == StatusCodes.Ok)
+                        {
+                            tcs.TrySetResult(true);
+                        }
+                        else
+                        {
+                            tcs.TrySetException(GetSftpException(response));
+                        }
+                    }));
+
+                await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs SSH_FXP_READLINK request.
         /// </summary>
@@ -1421,6 +1722,30 @@ namespace Renci.SshNet.Sftp
             return information;
         }
 
+
+#if FEATURE_TAP
+        public async Task<SftpFileSytemInformation> 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));
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            TaskCompletionSource<SftpFileSytemInformation> tcs = new TaskCompletionSource<SftpFileSytemInformation>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            using (cancellationToken.Register((s) => ((TaskCompletionSource<SftpFileSytemInformation>)s).TrySetCanceled(), tcs, false))
+            {
+                SendRequest(new StatVfsRequest(ProtocolVersion, NextRequestId, path, Encoding,
+                    response => tcs.TrySetResult(response.GetReply<StatVfsReplyInfo>().Information),
+                    response => tcs.TrySetException(GetSftpException(response))));
+
+                return await tcs.Task.ConfigureAwait(false);
+            }
+        }
+#endif
+
         /// <summary>
         /// Performs fstatvfs@openssh.com extended request.
         /// </summary>
@@ -1460,7 +1785,7 @@ namespace Renci.SshNet.Sftp
 
                 WaitOnHandle(wait, OperationTimeout);
             }
-            
+
             if (!nullOnError && exception != null)
             {
                 throw exception;

+ 175 - 0
src/Renci.SshNet/SftpClient.cs

@@ -10,6 +10,9 @@ using System.Threading;
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Sftp;
+#if FEATURE_TAP
+using System.Threading.Tasks;
+#endif
 
 namespace Renci.SshNet
 {
@@ -373,6 +376,33 @@ namespace Renci.SshNet
             _sftpSession.RequestRemove(fullPath);
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously deletes remote file specified by path.
+        /// </summary>
+        /// <param name="path">File to be deleted path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
+        /// <exception cref="ArgumentException"><paramref name="path"/> is <b>null</b> or contains only whitespace characters.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to delete the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task DeleteFileAsync(string path, CancellationToken cancellationToken)
+        {
+            base.CheckDisposed();
+            if (path.IsNullOrWhiteSpace())
+                throw new ArgumentException("path");
+            if (_sftpSession == null)
+                throw new SshConnectionException("Client not connected.");
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+            await _sftpSession.RequestRemoveAsync(fullPath, cancellationToken).ConfigureAwait(false);
+        }
+#endif
+
         /// <summary>
         /// Renames remote file from old path to new path.
         /// </summary>
@@ -388,6 +418,36 @@ namespace Renci.SshNet
             RenameFile(oldPath, newPath, false);
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously renames remote file from old path to new path.
+        /// </summary>
+        /// <param name="oldPath">Path to the old file location.</param>
+        /// <param name="newPath">Path to the new file location.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>A <see cref="Task"/> that represents the asynchronous rename operation.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="oldPath"/> is <b>null</b>. <para>-or-</para> or <paramref name="newPath"/> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task RenameFileAsync(string oldPath, string newPath, CancellationToken cancellationToken)
+        {
+            base.CheckDisposed();
+            if (oldPath == null)
+                throw new ArgumentNullException("oldPath");
+            if (newPath == null)
+                throw new ArgumentNullException("newPath");
+            if (_sftpSession == null)
+                throw new SshConnectionException("Client not connected.");
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var oldFullPath = await _sftpSession.GetCanonicalPathAsync(oldPath, cancellationToken).ConfigureAwait(false);
+            var newFullPath = await _sftpSession.GetCanonicalPathAsync(newPath, cancellationToken).ConfigureAwait(false);
+            await _sftpSession.RequestRenameAsync(oldFullPath, newFullPath, cancellationToken).ConfigureAwait(false);
+        }
+#endif
+
         /// <summary>
         /// Renames remote file from old path to new path.
         /// </summary>
@@ -476,6 +536,66 @@ namespace Renci.SshNet
             return InternalListDirectory(path, listCallback);
         }
 
+#if FEATURE_TAP
+
+        /// <summary>
+        /// Asynchronously retrieves list of files in remote directory.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{IEnumerable}"/> that represents the asynchronous list operation.
+        /// The task result contains an enumerable collection of <see cref="SftpFile"/> for the files in the directory specified by <paramref name="path" />.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
+        /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task<IEnumerable<SftpFile>> ListDirectoryAsync(string path, CancellationToken cancellationToken)
+        {
+            base.CheckDisposed();
+            if (path == null)
+                throw new ArgumentNullException("path");
+            if (_sftpSession == null)
+                throw new SshConnectionException("Client not connected.");
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+
+            var result = new List<SftpFile>();
+            var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
+            try
+            {
+                var basePath = (fullPath[fullPath.Length - 1] == '/') ?
+                    fullPath :
+                    fullPath + '/';
+
+                while (true)
+                {
+                    var files = await _sftpSession.RequestReadDirAsync(handle, cancellationToken).ConfigureAwait(false);
+                    if (files == null)
+                    {
+                        break;
+                    }
+
+                    foreach (var file in files)
+                    {
+                        result.Add(new SftpFile(_sftpSession, basePath + file.Key, file.Value));
+                    }
+                }
+
+            }
+            finally
+            {
+                await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
+            }
+
+            return result;
+        }
+
+#endif
+
         /// <summary>
         /// Begins an asynchronous operation of retrieving list of files in remote directory.
         /// </summary>
@@ -1009,6 +1129,33 @@ namespace Renci.SshNet
             return _sftpSession.RequestStatVfs(fullPath);
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously gets status using statvfs@openssh.com request.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{SftpFileSytemInformation}"/> that represents the status operation.
+        /// The task result contains the <see cref="SftpFileSytemInformation"/> instance that contains file status information.
+        /// </returns>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="path" /> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public async Task<SftpFileSytemInformation> GetStatusAsync(string path, CancellationToken cancellationToken)
+        {
+            base.CheckDisposed();
+            if (path == null)
+                throw new ArgumentNullException("path");
+            if (_sftpSession == null)
+                throw new SshConnectionException("Client not connected.");
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
+            return await _sftpSession.RequestStatVfsAsync(fullPath, cancellationToken).ConfigureAwait(false);
+        }
+#endif
+
         #region File Methods
 
         /// <summary>
@@ -1356,6 +1503,34 @@ namespace Renci.SshNet
             return new SftpFileStream(_sftpSession, path, mode, access, (int) _bufferSize);
         }
 
+#if FEATURE_TAP
+        /// <summary>
+        /// Asynchronously opens a <see cref="SftpFileStream"/> on the specified path, with the specified mode and access.
+        /// </summary>
+        /// <param name="path">The file to open.</param>
+        /// <param name="mode">A <see cref="FileMode"/> value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten.</param>
+        /// <param name="access">A <see cref="FileAccess"/> value that specifies the operations that can be performed on the file.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
+        /// <returns>
+        /// A <see cref="Task{SftpFileStream}"/> that represents the asynchronous open operation.
+        /// The task result contains the <see cref="SftpFileStream"/> that provides access to the specified file, with the specified mode and access.
+        /// </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>.</exception>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public Task<SftpFileStream> OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken)
+        {
+            base.CheckDisposed();
+            if (path == null)
+                throw new ArgumentNullException("path");
+            if (_sftpSession == null)
+                throw new SshConnectionException("Client not connected.");
+            cancellationToken.ThrowIfCancellationRequested();
+
+            return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken);
+        }
+#endif
+
         /// <summary>
         /// Opens an existing file for reading.
         /// </summary>