using System; using System.Net.Sockets; using System.Threading; 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 : 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; /// /// 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 Session != null && Session.IsConnected; } } /// /// 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 { // change the due time and interval of the timer if has already // been created (which means the client is connected) // // if the client is not yet connected, then the timer will be // created with the new interval when Connect() is invoked if (_keepAliveTimer != null) _keepAliveTimer.Change(value, value); } _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. /// internal BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory) { if (connectionInfo == null) throw new ArgumentNullException("connectionInfo"); if (serviceFactory == null) throw new ArgumentNullException("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 (Session != null && Session.IsConnected) throw new InvalidOperationException("The client is already connected."); OnConnecting(); Session = _serviceFactory.CreateSession(ConnectionInfo); Session.HostKeyReceived += Session_HostKeyReceived; Session.ErrorOccured += Session_ErrorOccured; Session.Connect(); StartKeepAliveTimer(); OnConnected(); } /// /// 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(); // disconnect and dispose the SSH session if (Session != null) { // a new session is created in Connect(), so we should dispose and // dereference the current session here Session.ErrorOccured -= Session_ErrorOccured; Session.HostKeyReceived -= Session_HostKeyReceived; Session.Dispose(); Session = null; } 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() { if (Session != null) Session.OnDisconnecting(); } /// /// Called when client is disconnected from the server. /// protected virtual void OnDisconnected() { } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { var handler = ErrorOccurred; if (handler != null) { handler(this, e); } } private void Session_HostKeyReceived(object sender, HostKeyEventArgs e) { var handler = HostKeyReceived; if (handler != null) { handler(this, e); } } #region IDisposable Members private bool _isDisposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { DiagnosticAbstraction.Log("Disposing client."); Dispose(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) { var connectionInfoDisposable = _connectionInfo as IDisposable; if (connectionInfoDisposable != null) 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); } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~BaseClient() { Dispose(false); } #endregion /// /// Stops the keep-alive timer, and waits until all timer callbacks have been /// executed. /// private void StopKeepAliveTimer() { if (_keepAliveTimer == null) return; _keepAliveTimer.Dispose(); _keepAliveTimer = null; } private void SendKeepAliveMessage() { // do nothing if we have disposed or disconnected if (Session == 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 = new Timer(state => SendKeepAliveMessage(), null, _keepAliveInterval, _keepAliveInterval); } } }