using System; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; namespace Renci.SshNet { /// /// Serves as base class for client implementations, provides common client functionality. /// public abstract class BaseClient : IBaseClient, IDisposable { /// /// Holds value indicating whether the connection info is owned by this client. /// private readonly bool _ownsConnectionInfo; private readonly IServiceFactory _serviceFactory; private readonly object _keepAliveLock = new object(); private TimeSpan _keepAliveInterval; private Timer _keepAliveTimer; private ConnectionInfo _connectionInfo; private bool _isDisposed; /// /// Gets the current session. /// /// /// The current session. /// internal ISession Session { get; private set; } /// /// Gets the factory for creating new services. /// /// /// The factory for creating new services. /// internal IServiceFactory ServiceFactory { get { return _serviceFactory; } } /// /// Gets the connection info. /// /// /// The connection info. /// /// The method was called after the client was disposed. public ConnectionInfo ConnectionInfo { get { CheckDisposed(); return _connectionInfo; } private set { _connectionInfo = value; } } /// /// Gets a value indicating whether this client is connected to the server. /// /// /// true if this client is connected; otherwise, false. /// /// The method was called after the client was disposed. public bool IsConnected { get { CheckDisposed(); return IsSessionConnected(); } } /// /// Gets or sets the keep-alive interval. /// /// /// The keep-alive interval. Specify negative one (-1) milliseconds to disable the /// keep-alive. This is the default value. /// /// The method was called after the client was disposed. public TimeSpan KeepAliveInterval { get { CheckDisposed(); return _keepAliveInterval; } set { CheckDisposed(); if (value == _keepAliveInterval) { return; } if (value == SshNet.Session.InfiniteTimeSpan) { // stop the timer when the value is -1 milliseconds StopKeepAliveTimer(); } else { if (_keepAliveTimer != null) { // change the due time and interval of the timer if has already // been created (which means the client is connected) _ = _keepAliveTimer.Change(value, value); } else if (IsSessionConnected()) { // if timer has not yet been created and the client is already connected, // then we need to create the timer now // // this means that - before connecting - the keep-alive interval was set to // negative one (-1) and as such we did not create the timer _keepAliveTimer = CreateKeepAliveTimer(value, value); } // note that if the client is not yet connected, then the timer will be created with the // new interval when Connect() is invoked } _keepAliveInterval = value; } } /// /// Occurs when an error occurred. /// /// /// /// public event EventHandler ErrorOccurred; /// /// Occurs when host key received. /// /// /// /// public event EventHandler HostKeyReceived; /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// is null. /// /// If is true, then the /// connection info will be disposed when this instance is disposed. /// protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo) : this(connectionInfo, ownsConnectionInfo, new ServiceFactory()) { } /// /// Initializes a new instance of the class. /// /// The connection info. /// Specified whether this instance owns the connection info. /// The factory to use for creating new services. /// is null. /// is null. /// /// If is true, then the /// connection info will be disposed when this instance is disposed. /// private protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory) { if (connectionInfo is null) { throw new ArgumentNullException(nameof(connectionInfo)); } if (serviceFactory is null) { throw new ArgumentNullException(nameof(serviceFactory)); } ConnectionInfo = connectionInfo; _ownsConnectionInfo = ownsConnectionInfo; _serviceFactory = serviceFactory; _keepAliveInterval = SshNet.Session.InfiniteTimeSpan; } /// /// Connects client to the server. /// /// 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 void Connect() { CheckDisposed(); // 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 = CreateAndConnectSession(); 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(); } /// /// 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(); } /// /// Disconnects client from the server. /// /// The method was called after the client was disposed. public void Disconnect() { DiagnosticAbstraction.Log("Disconnecting client."); CheckDisposed(); OnDisconnecting(); // stop sending keep-alive messages before we close the session StopKeepAliveTimer(); // dispose the SSH session DisposeSession(); OnDisconnected(); } /// /// Sends a keep-alive message to the server. /// /// /// Use to configure the client to send a keep-alive at regular /// intervals. /// /// The method was called after the client was disposed. [Obsolete("Use KeepAliveInterval to send a keep-alive message at regular intervals.")] public void SendKeepAlive() { CheckDisposed(); SendKeepAliveMessage(); } /// /// Called when client is connecting to the server. /// protected virtual void OnConnecting() { } /// /// Called when client is connected to the server. /// protected virtual void OnConnected() { } /// /// Called when client is disconnecting from the server. /// protected virtual void OnDisconnecting() { Session?.OnDisconnecting(); } /// /// Called when client is disconnected from the server. /// protected virtual void OnDisconnected() { } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { ErrorOccurred?.Invoke(this, e); } private void Session_HostKeyReceived(object sender, HostKeyEventArgs e) { HostKeyReceived?.Invoke(this, e); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { DiagnosticAbstraction.Log("Disposing client."); Dispose(disposing: true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_isDisposed) { return; } if (disposing) { Disconnect(); if (_ownsConnectionInfo && _connectionInfo != null) { if (_connectionInfo is IDisposable connectionInfoDisposable) { connectionInfoDisposable.Dispose(); } _connectionInfo = null; } _isDisposed = true; } } /// /// Check if the current instance is disposed. /// /// THe current instance is disposed. protected void CheckDisposed() { if (_isDisposed) { throw new ObjectDisposedException(GetType().FullName); } } /// /// Finalizes an instance of the class. /// ~BaseClient() { Dispose(disposing: false); } /// /// Stops the keep-alive timer, and waits until all timer callbacks have been /// executed. /// private void StopKeepAliveTimer() { if (_keepAliveTimer is null) { return; } _keepAliveTimer.Dispose(); _keepAliveTimer = null; } private void SendKeepAliveMessage() { var session = Session; // do nothing if we have disposed or disconnected if (session is null) { return; } // do not send multiple keep-alive messages concurrently if (Monitor.TryEnter(_keepAliveLock)) { try { _ = session.TrySendMessage(new IgnoreMessage()); } finally { Monitor.Exit(_keepAliveLock); } } } /// /// Starts the keep-alive timer. /// /// /// When is negative one (-1) milliseconds, then /// the timer will not be started. /// private void StartKeepAliveTimer() { if (_keepAliveInterval == SshNet.Session.InfiniteTimeSpan) { return; } if (_keepAliveTimer != null) { // timer is already started return; } _keepAliveTimer = CreateKeepAliveTimer(_keepAliveInterval, _keepAliveInterval); } /// /// Creates a with the specified due time and interval. /// /// The amount of time to delay before the keep-alive message is first sent. Specify negative one (-1) milliseconds to prevent the timer from starting. Specify zero (0) to start the timer immediately. /// The time interval between attempts to send a keep-alive message. Specify negative one (-1) milliseconds to disable periodic signaling. /// /// A with the specified due time and interval. /// private Timer CreateKeepAliveTimer(TimeSpan dueTime, TimeSpan period) { return new Timer(state => SendKeepAliveMessage(), Session, dueTime, period); } private ISession CreateAndConnectSession() { var session = _serviceFactory.CreateSession(ConnectionInfo, _serviceFactory.CreateSocketFactory()); session.HostKeyReceived += Session_HostKeyReceived; session.ErrorOccured += Session_ErrorOccured; try { session.Connect(); return session; } catch { DisposeSession(session); throw; } } 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; } } private void DisposeSession(ISession session) { session.ErrorOccured -= Session_ErrorOccured; session.HostKeyReceived -= Session_HostKeyReceived; session.Dispose(); } /// /// Disposes the SSH session, and assigns null to . /// private void DisposeSession() { var session = Session; if (session != null) { Session = null; DisposeSession(session); } } /// /// Returns a value indicating whether the SSH session is established. /// /// /// true if the SSH session is established; otherwise, false. /// private bool IsSessionConnected() { var session = Session; return session != null && session.IsConnected; } } }