diff --git a/src/Renci.SshNet/Abstractions/DnsAbstraction.cs b/src/Renci.SshNet/Abstractions/DnsAbstraction.cs
index 0af35aeed..a2ac9d58f 100644
--- a/src/Renci.SshNet/Abstractions/DnsAbstraction.cs
+++ b/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 @@ public static IPAddress[] GetHostAddresses(string hostNameOrAddress)
#endif // FEATURE_DEVICEINFORMATION_APM
#endif
}
+
+#if FEATURE_TAP
+ ///
+ /// Returns the Internet Protocol (IP) addresses for the specified host.
+ ///
+ /// The host name or IP address to resolve
+ ///
+ /// A task with result of an array of type that holds the IP addresses for the host that
+ /// is specified by the parameter.
+ ///
+ /// is null.
+ /// An error is encountered when resolving .
+ public static Task GetHostAddressesAsync(string hostNameOrAddress)
+ {
+ return Dns.GetHostAddressesAsync(hostNameOrAddress);
+ }
+#endif
+
}
}
diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs
index 287caea66..2029b0143 100644
--- a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs
+++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs
@@ -3,6 +3,9 @@
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 @@ public static void Connect(Socket socket, IPEndPoint remoteEndpoint, TimeSpan co
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 @@ public static byte[] Read(Socket socket, int size, TimeSpan timeout)
return buffer;
}
+#if FEATURE_TAP
+ public static Task ReadAsync(Socket socket, byte[] buffer, int offset, int length, CancellationToken cancellationToken)
+ {
+ return socket.ReceiveAsync(buffer, offset, length, cancellationToken);
+ }
+#endif
+
///
/// Receives data from a bound into a receive buffer.
///
diff --git a/src/Renci.SshNet/Abstractions/SocketExtensions.cs b/src/Renci.SshNet/Abstractions/SocketExtensions.cs
new file mode 100644
index 000000000..d763e1a34
--- /dev/null
+++ b/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 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 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
\ No newline at end of file
diff --git a/src/Renci.SshNet/BaseClient.cs b/src/Renci.SshNet/BaseClient.cs
index 5b0e01c90..4e0975b09 100644
--- a/src/Renci.SshNet/BaseClient.cs
+++ b/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 @@ public void Connect()
StartKeepAliveTimer();
}
+#if FEATURE_TAP
+ ///
+ /// Asynchronously connects client to the server.
+ ///
+ /// The to observe.
+ /// A that represents the asynchronous connect operation.
+ ///
+ /// The client is already connected.
+ /// The method was called after the client was disposed.
+ /// Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.
+ /// SSH session could not be established.
+ /// Authentication of SSH session failed.
+ /// Failed to establish proxy connection.
+ 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
+
///
/// Disconnects client from the server.
///
@@ -473,6 +533,26 @@ private ISession CreateAndConnectSession()
}
}
+#if FEATURE_TAP
+ private async Task 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;
diff --git a/src/Renci.SshNet/Connection/ConnectorBase.cs b/src/Renci.SshNet/Connection/ConnectorBase.cs
index 6e9bed7af..ffa026750 100644
--- a/src/Renci.SshNet/Connection/ConnectorBase.cs
+++ b/src/Renci.SshNet/Connection/ConnectorBase.cs
@@ -4,6 +4,11 @@
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 @@ protected ConnectorBase(ISocketFactory socketFactory)
public abstract Socket Connect(IConnectionInfo connectionInfo);
+#if FEATURE_TAP
+ public abstract Task ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken);
+#endif
+
///
/// Establishes a socket connection to the specified host and port.
///
@@ -54,6 +63,42 @@ protected Socket SocketConnect(string host, int port, TimeSpan timeout)
}
}
+#if FEATURE_TAP
+ ///
+ /// Establishes a socket connection to the specified host and port.
+ ///
+ /// The host name of the server to connect to.
+ /// The port to connect to.
+ /// The cancellation token to observe.
+ /// The connection failed to establish within the configured .
+ /// An error occurred trying to establish the connection.
+ protected async Task 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];
diff --git a/src/Renci.SshNet/Connection/DirectConnector.cs b/src/Renci.SshNet/Connection/DirectConnector.cs
index ec8464505..0d07bb936 100644
--- a/src/Renci.SshNet/Connection/DirectConnector.cs
+++ b/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 @@ public override Socket Connect(IConnectionInfo connectionInfo)
{
return SocketConnect(connectionInfo.Host, connectionInfo.Port, connectionInfo.Timeout);
}
+
+#if FEATURE_TAP
+ public override System.Threading.Tasks.Task ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken)
+ {
+ return SocketConnectAsync(connectionInfo.Host, connectionInfo.Port, cancellationToken);
+ }
+#endif
}
}
diff --git a/src/Renci.SshNet/Connection/HttpConnector.cs b/src/Renci.SshNet/Connection/HttpConnector.cs
index b77f07345..62c4ae279 100644
--- a/src/Renci.SshNet/Connection/HttpConnector.cs
+++ b/src/Renci.SshNet/Connection/HttpConnector.cs
@@ -5,6 +5,7 @@
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
///
///
///
- 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/(?\d[.]\d) (?\d{3}) (?.+)$");
var httpHeaderRe = new Regex(@"(?[^\[\]()<>@,;:\""/?={} \t]+):(?.+)?");
diff --git a/src/Renci.SshNet/Connection/IConnector.cs b/src/Renci.SshNet/Connection/IConnector.cs
index 7efc5c5fa..9eccabe62 100644
--- a/src/Renci.SshNet/Connection/IConnector.cs
+++ b/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 ConnectAsync(IConnectionInfo connectionInfo, CancellationToken cancellationToken);
+#endif
}
}
diff --git a/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs b/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs
index 77bcfbd33..c804c291f 100644
--- a/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs
+++ b/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs
@@ -18,5 +18,9 @@ internal interface IProtocolVersionExchange
/// The SSH identification of the server.
///
SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout);
+
+#if FEATURE_TAP
+ System.Threading.Tasks.Task StartAsync(string clientVersion, Socket socket, System.Threading.CancellationToken cancellationToken);
+#endif
}
}
diff --git a/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs
index 1d529a6d1..4e6957c10 100644
--- a/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs
+++ b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs
@@ -7,6 +7,10 @@
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 @@ public SshIdentification Start(string clientVersion, Socket socket, TimeSpan tim
}
}
+#if FEATURE_TAP
+ public async Task 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();
+
+ // 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 @@ private static string SocketReadLine(Socket socket, TimeSpan timeout, List
return null;
}
+
+#if FEATURE_TAP
+ private static async Task SocketReadLineAsync(Socket socket, CancellationToken cancellationToken, List 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
+
}
}
diff --git a/src/Renci.SshNet/Connection/ProxyConnector.cs b/src/Renci.SshNet/Connection/ProxyConnector.cs
new file mode 100644
index 000000000..6cc2ac9d4
--- /dev/null
+++ b/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 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
+ }
+}
diff --git a/src/Renci.SshNet/Connection/Socks4Connector.cs b/src/Renci.SshNet/Connection/Socks4Connector.cs
index 9154240f8..ccf4e1456 100644
--- a/src/Renci.SshNet/Connection/Socks4Connector.cs
+++ b/src/Renci.SshNet/Connection/Socks4Connector.cs
@@ -12,36 +12,18 @@ namespace Renci.SshNet.Connection
///
/// https://www.openssh.com/txt/socks4.protocol
///
- 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;
- }
- }
-
///
/// Establishes a connection to the server via a SOCKS5 proxy.
///
/// The connection information.
/// The .
- 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);
diff --git a/src/Renci.SshNet/Connection/Socks5Connector.cs b/src/Renci.SshNet/Connection/Socks5Connector.cs
index 720ac5004..e1b6859dc 100644
--- a/src/Renci.SshNet/Connection/Socks5Connector.cs
+++ b/src/Renci.SshNet/Connection/Socks5Connector.cs
@@ -11,36 +11,18 @@ namespace Renci.SshNet.Connection
///
/// https://en.wikipedia.org/wiki/SOCKS#SOCKS5
///
- 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;
- }
- }
-
///
/// Establishes a connection to the server via a SOCKS5 proxy.
///
/// The connection information.
/// The .
- private void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
+ protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
{
var greeting = new byte[]
{
diff --git a/src/Renci.SshNet/ISession.cs b/src/Renci.SshNet/ISession.cs
index cd950da52..cde647a46 100644
--- a/src/Renci.SshNet/ISession.cs
+++ b/src/Renci.SshNet/ISession.cs
@@ -6,6 +6,9 @@
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 @@ internal interface ISession : IDisposable
/// Failed to establish proxy connection.
void Connect();
+#if FEATURE_TAP
+ ///
+ /// Asynchronously connects to the server.
+ ///
+ /// The to observe.
+ /// A that represents the asynchronous connect operation.
+ /// Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.
+ /// SSH session could not be established.
+ /// Authentication of SSH session failed.
+ /// Failed to establish proxy connection.
+ Task ConnectAsync(CancellationToken cancellationToken);
+#endif
+
///
/// Create a new SSH session channel.
///
diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs
index b1212cfea..486344d18 100644
--- a/src/Renci.SshNet/ISftpClient.cs
+++ b/src/Renci.SshNet/ISftpClient.cs
@@ -4,6 +4,10 @@
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 @@ public interface ISftpClient
/// The method was called after the client was disposed.
void DeleteFile(string path);
+#if FEATURE_TAP
+ ///
+ /// Asynchronously deletes remote file specified by path.
+ ///
+ /// File to be deleted path.
+ /// The to observe.
+ /// A that represents the asynchronous delete operation.
+ /// is null or contains only whitespace characters.
+ /// Client is not connected.
+ /// was not found on the remote host.
+ /// Permission to delete the file was denied by the remote host. -or- A SSH command was denied by the server.
+ /// A SSH error where is the message from the remote host.
+ /// The method was called after the client was disposed.
+ Task DeleteFileAsync(string path, CancellationToken cancellationToken);
+#endif
+
///
/// Downloads remote file specified by the path into the stream.
///
@@ -653,6 +673,22 @@ public interface ISftpClient
/// The method was called after the client was disposed.
SftpFileSytemInformation GetStatus(string path);
+#if FEATURE_TAP
+ ///
+ /// Asynchronously gets status using statvfs@openssh.com request.
+ ///
+ /// The path.
+ /// The to observe.
+ ///
+ /// A that represents the status operation.
+ /// The task result contains the instance that contains file status information.
+ ///
+ /// Client is not connected.
+ /// is null.
+ /// The method was called after the client was disposed.
+ Task GetStatusAsync(string path, CancellationToken cancellationToken);
+#endif
+
///
/// Retrieves list of files in remote directory.
///
@@ -668,6 +704,25 @@ public interface ISftpClient
/// The method was called after the client was disposed.
IEnumerable ListDirectory(string path, Action listCallback = null);
+#if FEATURE_TAP
+
+ ///
+ /// Asynchronously retrieves list of files in remote directory.
+ ///
+ /// The path.
+ /// The to observe.
+ ///
+ /// A that represents the asynchronous list operation.
+ /// The task result contains an enumerable collection of for the files in the directory specified by .
+ ///
+ /// is null.
+ /// Client is not connected.
+ /// Permission to list the contents of the directory was denied by the remote host. -or- A SSH command was denied by the server.
+ /// A SSH error where is the message from the remote host.
+ /// The method was called after the client was disposed.
+ Task> ListDirectoryAsync(string path, CancellationToken cancellationToken);
+#endif
+
///
/// Opens a on the specified path with read/write access.
///
@@ -695,6 +750,24 @@ public interface ISftpClient
/// The method was called after the client was disposed.
SftpFileStream Open(string path, FileMode mode, FileAccess access);
+#if FEATURE_TAP
+ ///
+ /// Asynchronously opens a on the specified path, with the specified mode and access.
+ ///
+ /// The file to open.
+ /// A 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.
+ /// A value that specifies the operations that can be performed on the file.
+ /// The to observe.
+ ///
+ /// A that represents the asynchronous open operation.
+ /// The task result contains the that provides access to the specified file, with the specified mode and access.
+ ///
+ /// is null.
+ /// Client is not connected.
+ /// The method was called after the client was disposed.
+ Task OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken);
+#endif
+
///
/// Opens an existing file for reading.
///
@@ -833,6 +906,22 @@ public interface ISftpClient
/// The method was called after the client was disposed.
void RenameFile(string oldPath, string newPath);
+#if FEATURE_TAP
+ ///
+ /// Asynchronously renames remote file from old path to new path.
+ ///
+ /// Path to the old file location.
+ /// Path to the new file location.
+ /// The to observe.
+ /// A that represents the asynchronous rename operation.
+ /// is null. -or- or is null.
+ /// Client is not connected.
+ /// Permission to rename the file was denied by the remote host. -or- A SSH command was denied by the server.
+ /// A SSH error where is the message from the remote host.
+ /// The method was called after the client was disposed.
+ Task RenameFileAsync(string oldPath, string newPath, CancellationToken cancellationToken);
+#endif
+
///
/// Renames remote file from old path to new path.
///
diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj
index 124ce9d4b..740ec7ee1 100644
--- a/src/Renci.SshNet/Renci.SshNet.csproj
+++ b/src/Renci.SshNet/Renci.SshNet.csproj
@@ -5,9 +5,9 @@
false
Renci.SshNet
../Renci.SshNet.snk
- 5
+ 6
true
- net35;net40;netstandard1.3;netstandard2.0
+ net35;net40;net472;netstandard1.3;netstandard2.0