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 62f432491..dc2d2c899 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 : IDisposable /// 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 : IDisposable /// 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 : IDisposable /// 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 : IDisposable /// 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 : IDisposable /// 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