浏览代码

BaseClient:
* Added bool argument to BaseClient ctor to indicate whether the specified ConnectionInfo
instance is owned by the client.
* In Dispose, also dispose ConnectionInfo if ownsConnectionInfo is true.
* Modified Connect() to throw InvalidOperationException when the client is already connected.
* Postpone creation of keep-alive timer until client has actually connected, and stop timer
when disconnecting the client.
* Document default value of KeepAliveInterval.
* Modified all public members to throw ObjectDisposedException when client is disposed.
* Modified Dispose to stop the keep-alive timer (and wait until all timer callbacks have
been executed) before the session is disposed.

NetConfClient:
* Moved dispose of ConnectionInfo to BaseClient.
* Document default value of OperationTimeout and AutomaticMessageIdHandling.

ScpClient:
* Moved dispose of ConnectionInfo to BaseClient.
* Document default value of OperationTimeout and BufferSize.

SftpClient:
* Moved dispose of ConnectionInfo to BaseClient.
* Document default value of OperationTimeout and BufferSize.
* Modified all public members to throw ObjectDisposedException when client is disposed.
* Modified the WorkingDirectory and ProtocolVersion properties to throw
SshConnectionException when the client is not connected.
* When disconnecting the client, also dispose the SftpSession to avoid leaks and set it
to null as this is used to check whether the SftpClient is connected.

SshClient:
* Moved dispose of ConnectionInfo to BaseClient.

Session:

* include waithandle that signals when message listener has completed to avoid waiting for a response that can never be received
* throw SshConnectionException when message listener has completed (and no exception has been raised) if session is not disconnecting
* Modified IsConnected to also return false when:
- disconnect message has been sent to server
- the message listener completed waithandle is null or has been set (=listener thread has completed)
* Improved documentation of IsConnected property.
* Modified Disconnect() to also disconnect and dispose the underlying socket, and wait for the listener thread to complete.
Only send disconnect message if session is still connected.
* Modified Connect() to to reset connection-specific information to ensure state of a
previous connection does not affect new connections.
* Do not raise errors for SshConnectionException or socket timeout when session is disconnecting.
* Move any checks that are not socket related from IsSocketConnected to the IsConnected property.
* Modified IsSocketConnected on .NET to first check Connected property on underlying socket,
and when true use Socket.Poll with SelectRead in combination with Socket.Available to detect
if connection has been closed. Note that this does not detect loss of network connectivity.

Gert Driesen 11 年之前
父节点
当前提交
34405caa24

+ 22 - 26
Renci.SshClient/Renci.SshNet.Silverlight/Session.SilverlightShared.cs

@@ -5,7 +5,6 @@ using System.Net.Sockets;
 using System.Threading;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
-using System.Text;
 using System.Collections.Generic;
 
 namespace Renci.SshNet
@@ -15,12 +14,17 @@ namespace Renci.SshNet
         private readonly AutoResetEvent _autoEvent = new AutoResetEvent(false);
         private readonly AutoResetEvent _sendEvent = new AutoResetEvent(false);
         private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
-
-        private bool _isConnected = false;
-
+        private bool _isConnected;
+
+        /// <summary>
+        /// Gets a value indicating whether the socket is connected.
+        /// </summary>
+        /// <value>
+        /// <c>true</c> if the socket is connected; otherwise, <c>false</c>.
+        /// </value>
         partial void IsSocketConnected(ref bool isConnected)
         {
-            isConnected = (!this._isDisconnecting && this._socket != null && this._socket.Connected && this._isAuthenticated && this._messageListenerCompleted != null && this._isConnected);
+            isConnected = (this._socket != null && this._socket.Connected && this._isConnected);
         }
 
         partial void SocketConnect(string host, int port)
@@ -29,10 +33,9 @@ namespace Renci.SshNet
             this._socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 
             var args = new SocketAsyncEventArgs();
-
             args.UserToken = this._socket;
             args.RemoteEndPoint = ep;
-            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnConnect);
+            args.Completed += OnConnect;
 
             this._socket.ConnectAsync(args);
             this._autoEvent.WaitOne(this.ConnectionInfo.Timeout);
@@ -48,20 +51,19 @@ namespace Renci.SshNet
 
         partial void SocketReadLine(ref string response)
         {
-            var encoding = new Renci.SshNet.Common.ASCIIEncoding();
+            var encoding = new ASCIIEncoding();
 
-            var line = new StringBuilder();
             //  Read data one byte at a time to find end of line and leave any unhandled information in the buffer to be processed later
             var buffer = new List<byte>();
 
             var data = new byte[1];
             do
             {
-                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
+                var args = new SocketAsyncEventArgs();
                 args.SetBuffer(data, 0, data.Length);
                 args.UserToken = this._socket;
                 args.RemoteEndPoint = this._socket.RemoteEndPoint;
-                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceive);
+                args.Completed += OnReceive;
                 this._socket.ReceiveAsync(args);
 
                 if (!this._receiveEvent.WaitOne(this.ConnectionInfo.Timeout))
@@ -90,16 +92,15 @@ namespace Renci.SshNet
 
         partial void SocketRead(int length, ref byte[] buffer)
         {
-            var offset = 0;
-            int receivedTotal = 0;  // how many bytes is already received
+            var receivedTotal = 0;  // how many bytes is already received
 
             do
             {
-                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
-                args.SetBuffer(buffer, offset + receivedTotal, length - receivedTotal);
+                var args = new SocketAsyncEventArgs();
+                args.SetBuffer(buffer, receivedTotal, length - receivedTotal);
                 args.UserToken = this._socket;
                 args.RemoteEndPoint = this._socket.RemoteEndPoint;
-                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceive);
+                args.Completed += OnReceive;
                 this._socket.ReceiveAsync(args);
 
                 this._receiveEvent.WaitOne(this.ConnectionInfo.Timeout);
@@ -112,22 +113,18 @@ namespace Renci.SshNet
                     Thread.Sleep(30);
                     continue;
                 }
-                else if (args.SocketError != SocketError.Success)
+                if (args.SocketError != SocketError.Success)
                 {
                     throw new SocketException((int)args.SocketError);
                 }
 
                 var receivedBytes = args.BytesTransferred;
-
                 if (receivedBytes > 0)
                 {
                     receivedTotal += receivedBytes;
                     continue;
                 }
-                else
-                {
-                    throw new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
-                }
+                throw new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
             } while (receivedTotal < length);
         }
 
@@ -135,11 +132,11 @@ namespace Renci.SshNet
         {
             if (this._isConnected)
             {
-                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
+                var args = new SocketAsyncEventArgs();
                 args.SetBuffer(data, 0, data.Length);
                 args.UserToken = this._socket;
                 args.RemoteEndPoint = this._socket.RemoteEndPoint;
-                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnSend);
+                args.Completed += OnSend;
 
                 this._socket.SendAsync(args);
             }
@@ -166,7 +163,7 @@ namespace Renci.SshNet
 
         partial void ExecuteThread(Action action)
         {
-            ThreadPool.QueueUserWorkItem((o) => { action(); });
+            ThreadPool.QueueUserWorkItem(o => action());
         }
 
         partial void InternalRegisterMessage(string messageName)
@@ -192,6 +189,5 @@ namespace Renci.SshNet
                 }
             }
         }
-
     }
 }

+ 140 - 54
Renci.SshClient/Renci.SshNet/BaseClient.cs

@@ -9,9 +9,15 @@ namespace Renci.SshNet
     /// </summary>
     public abstract class BaseClient : IDisposable
     {
-        private TimeSpan _keepAliveInterval;
+        private static readonly TimeSpan Infinite = new TimeSpan(0, 0, 0, 0, -1);
 
+        /// <summary>
+        /// Holds value indicating whether the connection info is owned by this client.
+        /// </summary>
+        private readonly bool _ownsConnectionInfo;
+        private TimeSpan _keepAliveInterval;
         private Timer _keepAliveTimer;
+        private ConnectionInfo _connectionInfo;
 
         /// <summary>
         /// Gets current session.
@@ -21,46 +27,77 @@ namespace Renci.SshNet
         /// <summary>
         /// Gets the connection info.
         /// </summary>
-        public ConnectionInfo ConnectionInfo { get; private set; }
+        /// <value>
+        /// The connection info.
+        /// </value>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public ConnectionInfo ConnectionInfo
+        {
+            get
+            {
+                CheckDisposed();
+                return _connectionInfo;
+            }
+            private set
+            {
+                _connectionInfo = value;
+            }
+        }
 
         /// <summary>
         /// Gets a value indicating whether this client is connected to the server.
         /// </summary>
         /// <value>
-        /// 	<c>true</c> if this client is connected; otherwise, <c>false</c>.
+        /// <c>true</c> if this client is connected; otherwise, <c>false</c>.
         /// </value>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public bool IsConnected
         {
             get
             {
-                if (this.Session == null)
-                    return false;
-                return this.Session.IsConnected;
+                CheckDisposed();
+                return this.Session != null && this.Session.IsConnected;
             }
         }
 
         /// <summary>
-        /// Gets or sets the keep alive interval in seconds.
+        /// Gets or sets the keep-alive interval.
         /// </summary>
         /// <value>
-        /// The keep alive interval in seconds.
+        /// The keep-alive interval. Specify negative one (-1) milliseconds to disable the
+        /// keep-alive. This is the default value.
         /// </value>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public TimeSpan KeepAliveInterval
         {
             get
             {
+                CheckDisposed();
                 return this._keepAliveInterval;
             }
             set
             {
-                this._keepAliveInterval = value;
+                CheckDisposed();
 
-                if (this._keepAliveTimer == null)
+                if (value == _keepAliveInterval)
+                    return;
+
+                if (value == Infinite)
                 {
-                    this._keepAliveTimer = new Timer(state => this.SendKeepAlive());
+                    // stop the timer when the value is -1 milliseconds
+                    StopKeepAliveTimer();
                 }
-
-                this._keepAliveTimer.Change(this._keepAliveInterval, this._keepAliveInterval);
+                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);
+                }
+                this._keepAliveInterval = value;
             }
         }
 
@@ -84,63 +121,68 @@ namespace Renci.SshNet
         /// Initializes a new instance of the <see cref="BaseClient"/> class.
         /// </summary>
         /// <param name="connectionInfo">The connection info.</param>
+        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
-        public BaseClient(ConnectionInfo connectionInfo)
+        /// <remarks>
+        /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
+        /// connection info will be disposed when this instance is disposed.
+        /// </remarks>
+        protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
         {
             if (connectionInfo == null)
                 throw new ArgumentNullException("connectionInfo");
 
-            this.ConnectionInfo = connectionInfo;
-            this.Session = new Session(connectionInfo);
+            ConnectionInfo = connectionInfo;
+            _ownsConnectionInfo = ownsConnectionInfo;
+            _keepAliveInterval = Infinite;
         }
 
         /// <summary>
         /// Connects client to the server.
         /// </summary>
+        /// <exception cref="InvalidOperationException">The client is already connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void Connect()
         {
-            this.OnConnecting();
-
-            if (this.IsConnected)
-            {
-                this.Session.Disconnect();
-            }
-
-            this.Session = new Session(this.ConnectionInfo);
-            this.Session.HostKeyReceived += Session_HostKeyReceived;
-            this.Session.ErrorOccured += Session_ErrorOccured;
-            this.Session.Connect();
-
-            this.OnConnected();
+            CheckDisposed();
+
+            if (Session != null && Session.IsConnected)
+                throw new InvalidOperationException("The client is already connected.");
+
+            OnConnecting();
+            Session = new Session(ConnectionInfo);
+            Session.HostKeyReceived += Session_HostKeyReceived;
+            Session.ErrorOccured += Session_ErrorOccured;
+            Session.Connect();
+            StartKeepAliveTimer();
+            OnConnected();
         }
 
         /// <summary>
         /// Disconnects client from the server.
         /// </summary>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void Disconnect()
         {
-            if (!this.IsConnected)
-                return;
-
-            this.OnDisconnecting();
-
-            this.Session.Disconnect();
+            CheckDisposed();
 
-            this.OnDisconnected();
+            OnDisconnecting();
+            StopKeepAliveTimer();
+            if (Session != null)
+                Session.Disconnect();
+            OnDisconnected();
         }
 
         /// <summary>
         /// Sends keep-alive message to the server.
         /// </summary>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void SendKeepAlive()
         {
-            if (this.Session == null)
-                return;
-
-            if (!this.Session.IsConnected)
-                return;
+            CheckDisposed();
 
-            this.Session.SendKeepAlive();
+            if (Session != null && Session.IsConnected)
+                Session.SendKeepAlive();
         }
 
         /// <summary>
@@ -148,7 +190,6 @@ namespace Renci.SshNet
         /// </summary>
         protected virtual void OnConnecting()
         {
-
         }
 
         /// <summary>
@@ -156,7 +197,6 @@ namespace Renci.SshNet
         /// </summary>
         protected virtual void OnConnected()
         {
-
         }
 
         /// <summary>
@@ -164,7 +204,8 @@ namespace Renci.SshNet
         /// </summary>
         protected virtual void OnDisconnecting()
         {
-
+            if (Session != null)
+                Session.OnDisconnecting();
         }
 
         /// <summary>
@@ -172,10 +213,9 @@ namespace Renci.SshNet
         /// </summary>
         protected virtual void OnDisconnected()
         {
-
         }
 
-         private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
+        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
         {
             var handler = this.ErrorOccurred;
             if (handler != null)
@@ -203,7 +243,6 @@ namespace Renci.SshNet
         public void Dispose()
         {
             Dispose(true);
-
             GC.SuppressFinalize(this);
         }
 
@@ -220,20 +259,24 @@ namespace Renci.SshNet
                 // and unmanaged ResourceMessages.
                 if (disposing)
                 {
-                    // Dispose managed ResourceMessages.
-                    this.Session.ErrorOccured -= Session_ErrorOccured;
-                    this.Session.HostKeyReceived -= Session_HostKeyReceived;
+                    // stop sending keep-alive messages before we close the
+                    // session
+                    StopKeepAliveTimer();
 
                     if (this.Session != null)
                     {
+                        this.Session.ErrorOccured -= Session_ErrorOccured;
+                        this.Session.HostKeyReceived -= Session_HostKeyReceived;
                         this.Session.Dispose();
                         this.Session = null;
                     }
 
-                    if (this._keepAliveTimer != null)
+                    if (_ownsConnectionInfo && _connectionInfo != null)
                     {
-                        this._keepAliveTimer.Dispose();
-                        this._keepAliveTimer = null;
+                        var connectionInfoDisposable = _connectionInfo as IDisposable;
+                        if (connectionInfoDisposable != null)
+                            connectionInfoDisposable.Dispose();
+                        _connectionInfo = null;
                     }
                 }
 
@@ -242,6 +285,16 @@ namespace Renci.SshNet
             }
         }
 
+        /// <summary>
+        /// Check if the current instance is disposed.
+        /// </summary>
+        /// <exception cref="ObjectDisposedException">THe current instance is disposed.</exception>
+        protected void CheckDisposed()
+        {
+            if (_isDisposed)
+                throw new ObjectDisposedException(GetType().FullName);
+        }
+
         /// <summary>
         /// Releases unmanaged resources and performs other cleanup operations before the
         /// <see cref="BaseClient"/> is reclaimed by garbage collection.
@@ -255,5 +308,38 @@ namespace Renci.SshNet
         }
 
         #endregion
+
+        /// <summary>
+        /// Stops the keep-alive timer, and waits until all timer callbacks have been
+        /// executed.
+        /// </summary>
+        private void StopKeepAliveTimer()
+        {
+            if (_keepAliveTimer == null)
+                return;
+
+            var timerDisposed = new ManualResetEvent(false);
+            _keepAliveTimer.Dispose(timerDisposed);
+            timerDisposed.WaitOne();
+            timerDisposed.Dispose();
+            _keepAliveTimer = null;
+        }
+
+        /// <summary>
+        /// Starts the keep-alive timer.
+        /// </summary>
+        /// <remarks>
+        /// When <see cref="KeepAliveInterval"/> is negative one (-1) milliseconds, then
+        /// the timer will not be started.
+        /// </remarks>
+        private void StartKeepAliveTimer()
+        {
+            if (_keepAliveInterval == Infinite)
+                return;
+
+            if (_keepAliveTimer == null)
+                _keepAliveTimer = new Timer(state => this.SendKeepAlive());
+            _keepAliveTimer.Change(_keepAliveInterval, _keepAliveInterval);
+        }
     }
 }

+ 33 - 30
Renci.SshClient/Renci.SshNet/NetConfClient.cs

@@ -13,16 +13,17 @@ namespace Renci.SshNet
     public partial class NetConfClient : BaseClient
     {
         /// <summary>
-        /// Holds SftpSession instance that used to communicate to the SFTP server
+        /// Holds <see cref="NetConfSession"/> instance that used to communicate to the server
         /// </summary>
         private NetConfSession _netConfSession;
 
-        private readonly bool _disposeConnectionInfo;
-
         /// <summary>
         /// Gets or sets the operation timeout.
         /// </summary>
-        /// <value>The operation timeout.</value>
+        /// <value>
+        /// The timeout to wait until an operation completes. The default value is negative
+        /// one (-1) milliseconds, which indicates an infinite time-out period.
+        /// </value>
         public TimeSpan OperationTimeout { get; set; }
 
         #region Constructors
@@ -33,10 +34,8 @@ namespace Renci.SshNet
         /// <param name="connectionInfo">The connection info.</param>
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
         public NetConfClient(ConnectionInfo connectionInfo)
-            : base(connectionInfo)
+            : this(connectionInfo, false)
         {
-            this.AutomaticMessageIdHandling = true;
-            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
         }
 
         /// <summary>
@@ -51,9 +50,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public NetConfClient(string host, int port, string username, string password)
-            : this(new PasswordConnectionInfo(host, port, username, password))
+            : this(new PasswordConnectionInfo(host, port, username, password), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -81,9 +79,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public NetConfClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
-            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
+            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -99,6 +96,23 @@ namespace Renci.SshNet
         {
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NetConfClient"/> class.
+        /// </summary>
+        /// <param name="connectionInfo">The connection info.</param>
+        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
+        /// <remarks>
+        /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
+        /// connection info will be disposed when this instance is disposed.
+        /// </remarks>
+        private NetConfClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
+            : base(connectionInfo, ownsConnectionInfo)
+        {
+            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
+            this.AutomaticMessageIdHandling = true;
+        }
+
         #endregion
 
         /// <summary>
@@ -107,10 +121,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         public XmlDocument ServerCapabilities 
         {
-            get
-            {
-                return this._netConfSession.ServerCapabilities;
-            }
+            get { return this._netConfSession.ServerCapabilities; }
         }
 
         /// <summary>
@@ -119,17 +130,16 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         public XmlDocument ClientCapabilities
         {
-            get
-            {
-                return this._netConfSession.ClientCapabilities;
-            }
+            get { return this._netConfSession.ClientCapabilities; }
         }
 
         /// <summary>
-        /// Gets or sets a value indicating whether [automatic message id handling].
+        /// Gets or sets a value indicating whether automatic message id handling is
+        /// enabled.
         /// </summary>
         /// <value>
-        /// <c>true</c> if [automatic message id handling]; otherwise, <c>false</c>.
+        /// <c>true</c> if automatic message id handling is enabled; otherwise, <c>false</c>.
+        /// The default value is <c>true</c>.
         /// </value>
         public bool AutomaticMessageIdHandling { get; set; }
 
@@ -163,10 +173,8 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         public XmlDocument SendCloseRpc()
         {
-            XmlDocument rpc = new XmlDocument();
-
+            var rpc = new XmlDocument();
             rpc.LoadXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc message-id=\"6666\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"><close-session/></rpc>");
-
             return this._netConfSession.SendReceiveRpc(rpc, this.AutomaticMessageIdHandling);
         }
 
@@ -178,8 +186,7 @@ namespace Renci.SshNet
             base.OnConnected();
 
             this._netConfSession = new NetConfSession(this.Session, this.OperationTimeout);
-
-            this._netConfSession.Connect();            
+            this._netConfSession.Connect();
         }
 
         /// <summary>
@@ -205,10 +212,6 @@ namespace Renci.SshNet
             }
 
             base.Dispose(disposing);
-
-            if (this._disposeConnectionInfo)
-                ((IDisposable)this.ConnectionInfo).Dispose();
-
         }
     }
 }

+ 37 - 35
Renci.SshClient/Renci.SshNet/ScpClient.cs

@@ -16,21 +16,23 @@ namespace Renci.SshNet
     public partial class ScpClient : BaseClient
     {
         private static readonly Regex _fileInfoRe = new Regex(@"C(?<mode>\d{4}) (?<length>\d+) (?<filename>.+)");
-
         private static char[] _byteToChar;
 
-        private readonly bool _disposeConnectionInfo;
-
         /// <summary>
         /// Gets or sets the operation timeout.
         /// </summary>
-        /// <value>The operation timeout.</value>
+        /// <value>
+        /// The timeout to wait until an operation completes. The default value is negative
+        /// one (-1) milliseconds, which indicates an infinite time-out period.
+        /// </value>
         public TimeSpan OperationTimeout { get; set; }
 
         /// <summary>
         /// Gets or sets the size of the buffer.
         /// </summary>
-        /// <value>The size of the buffer.</value>
+        /// <value>
+        /// The size of the buffer. The default buffer size is 16384 bytes.
+        /// </value>
         public uint BufferSize { get; set; }
 
         /// <summary>
@@ -51,21 +53,8 @@ namespace Renci.SshNet
         /// <param name="connectionInfo">The connection info.</param>
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
         public ScpClient(ConnectionInfo connectionInfo)
-            : base(connectionInfo)
+            : this(connectionInfo, false)
         {
-            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
-            this.BufferSize = 1024 * 16;
-
-            if (_byteToChar == null)
-            {
-                _byteToChar = new char[128];
-                var ch = '\0';
-                for (int i = 0; i < 128; i++)
-                {
-                    _byteToChar[i] = ch++;
-                }
-            }
-
         }
 
         /// <summary>
@@ -80,9 +69,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public ScpClient(string host, int port, string username, string password)
-            : this(new PasswordConnectionInfo(host, port, username, password))
+            : this(new PasswordConnectionInfo(host, port, username, password), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -110,9 +98,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
-            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
+            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -128,6 +115,33 @@ namespace Renci.SshNet
         {
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ScpClient"/> class.
+        /// </summary>
+        /// <param name="connectionInfo">The connection info.</param>
+        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
+        /// <remarks>
+        /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
+        /// connection info will be disposed when this instance is disposed.
+        /// </remarks>
+        private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
+            : base(connectionInfo, ownsConnectionInfo)
+        {
+            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
+            this.BufferSize = 1024 * 16;
+
+            if (_byteToChar == null)
+            {
+                _byteToChar = new char[128];
+                var ch = '\0';
+                for (int i = 0; i < 128; i++)
+                {
+                    _byteToChar[i] = ch++;
+                }
+            }
+        }
+
         #endregion
 
         /// <summary>
@@ -390,17 +404,5 @@ namespace Renci.SshNet
 
             return sb.ToString();
         }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources
-        /// </summary>
-        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
-        protected override void Dispose(bool disposing)
-        {
-            base.Dispose(disposing);
-
-            if (this._disposeConnectionInfo)
-                ((IDisposable)this.ConnectionInfo).Dispose();
-        }
     }
 }

+ 20 - 6
Renci.SshClient/Renci.SshNet/Session.NET.cs

@@ -19,15 +19,25 @@ namespace Renci.SshNet
             new TraceSource("SshNet.Logging");
 #endif
 
+        /// <summary>
+        /// Gets a value indicating whether the socket is connected.
+        /// </summary>
+        /// <value>
+        /// <c>true</c> if the socket is connected; otherwise, <c>false</c>.
+        /// </value>
         partial void IsSocketConnected(ref bool isConnected)
         {
-                isConnected = (!this._isDisconnecting && this._socket != null && this._socket.Connected && this._isAuthenticated && this._messageListenerCompleted != null)
-                    && this._socket.Poll(-1, SelectMode.SelectWrite);
+            isConnected = (_socket != null && _socket.Connected);
+            if (isConnected)
+            {
+                var connectionClosedOrDataAvailable = _socket.Poll(1000, SelectMode.SelectRead);
+                isConnected = !(connectionClosedOrDataAvailable && _socket.Available == 0);
+            }
         }
 
         partial void SocketConnect(string host, int port)
         {
-            const int socketBufferSize = 2 * MAXIMUM_PACKET_SIZE;
+            const int socketBufferSize = 2 * MaximumPacketSize;
 
             var addr = host.GetIPAddress();
 
@@ -53,12 +63,12 @@ namespace Renci.SshNet
 
         partial void SocketDisconnect()
         {
-            this._socket.Disconnect(true);
+            _socket.Disconnect(true);
         }
 
         partial void SocketReadLine(ref string response)
         {
-            var encoding = new Renci.SshNet.Common.ASCIIEncoding();
+            var encoding = new ASCIIEncoding();
 
             //  Read data one byte at a time to find end of line and leave any unhandled information in the buffer to be processed later
             var buffer = new List<byte>();
@@ -113,6 +123,7 @@ namespace Renci.SshNet
                         receivedTotal += receivedBytes;
                         continue;
                     }
+
                     // 2012-09-11: Kenneth_aa
                     // When Disconnect or Dispose is called, this throws SshConnectionException(), which...
                     // 1 - goes up to ReceiveMessage() 
@@ -123,7 +134,10 @@ namespace Renci.SshNet
                     //
                     // Adding a check for this._isDisconnecting causes ReceiveMessage() to throw SshConnectionException: "Bad packet length {0}".
                     //
-                    throw new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
+
+                    if (_isDisconnecting)
+                        throw new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
+                    throw new SshConnectionException("An established connection was aborted by the server.", DisconnectReason.ConnectionLost);
                 }
                 catch (SocketException exp)
                 {

+ 249 - 130
Renci.SshClient/Renci.SshNet/Session.cs

@@ -28,19 +28,19 @@ namespace Renci.SshNet
         /// <summary>
         /// Specifies maximum packet size defined by the protocol.
         /// </summary>
-        protected const int MAXIMUM_PACKET_SIZE = 35000;
+        protected const int MaximumPacketSize = 35000;
 
         /// <summary>
         /// Specifies maximum payload size defined by the protocol.
         /// </summary>
-        protected const int MAXIMUM_PAYLOAD_SIZE = 1024 * 32;
+        protected const int MaximumPayloadSize = 1024 * 32;
 
-        private static readonly RNGCryptoServiceProvider _randomizer = new RNGCryptoServiceProvider();
+        private static readonly RNGCryptoServiceProvider Randomizer = new RNGCryptoServiceProvider();
 
 #if SILVERLIGHT
-        private static readonly Regex _serverVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$");
+        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$");
 #else
-        private static readonly Regex _serverVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$", RegexOptions.Compiled);
+        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$", RegexOptions.Compiled);
 #endif
 
         /// <summary>
@@ -49,7 +49,7 @@ namespace Renci.SshNet
         /// <remarks>
         /// Some server may restrict number to prevent authentication attacks
         /// </remarks>
-        private static readonly SemaphoreLight _authenticationConnection = new SemaphoreLight(3);
+        private static readonly SemaphoreLight AuthenticationConnection = new SemaphoreLight(3);
 
         /// <summary>
         /// Holds metada about session messages
@@ -131,6 +131,7 @@ namespace Renci.SshNet
         private Compressor _clientCompression;
 
         private SemaphoreLight _sessionSemaphore;
+
         /// <summary>
         /// Gets the session semaphore that controls session channels.
         /// </summary>
@@ -177,15 +178,37 @@ namespace Renci.SshNet
         }
 
         /// <summary>
-        /// Gets a value indicating whether socket connected.
+        /// Gets a value indicating whether the session is connected.
         /// </summary>
         /// <value>
-        /// 	<c>true</c> if socket connected; otherwise, <c>false</c>.
+        /// <c>true</c> if the session is connected; otherwise, <c>false</c>.
         /// </value>
+        /// <remarks>
+        /// This methods returns true in all but the following cases:
+        /// <list type="bullet">
+        ///     <item>
+        ///         <description>The SSH_MSG_DISCONNECT message - which is used to disconnect from the server - has been sent.</description>
+        ///     </item>
+        ///     <item>
+        ///         <description>The user has not been authenticated successfully.</description>
+        ///     </item>
+        ///     <item>
+        ///         <description>The listener thread - which is used to receive messages from the server - has stopped.</description>
+        ///     </item>
+        ///     <item>
+        ///         <description>The socket used to communicate with the server is no longer connected.</description>
+        ///     </item>
+        /// </list>
+        /// </remarks>
         public bool IsConnected
         {
             get
             {
+                if (_isDisconnectMessageSent || !this._isAuthenticated)
+                    return false;
+                if (_messageListenerCompleted == null || _messageListenerCompleted.WaitOne(0))
+                    return false;
+
                 var isSocketConnected = false;
                 IsSocketConnected(ref isSocketConnected);
                 return isSocketConnected;
@@ -219,10 +242,10 @@ namespace Renci.SshNet
                         MacAlgorithmsServerToClient = this.ConnectionInfo.HmacAlgorithms.Keys.ToArray(),
                         CompressionAlgorithmsClientToServer = this.ConnectionInfo.CompressionAlgorithms.Keys.ToArray(),
                         CompressionAlgorithmsServerToClient = this.ConnectionInfo.CompressionAlgorithms.Keys.ToArray(),
-                        LanguagesClientToServer = new string[] { string.Empty },
-                        LanguagesServerToClient = new string[] { string.Empty },
+                        LanguagesClientToServer = new[] {string.Empty},
+                        LanguagesServerToClient = new[] {string.Empty},
                         FirstKexPacketFollows = false,
-                        Reserved = 0,
+                        Reserved = 0
                     };
                 }
                 return this._clientInitMessage;
@@ -426,7 +449,7 @@ namespace Renci.SshNet
 
             try
             {
-                _authenticationConnection.Wait();
+                AuthenticationConnection.Wait();
 
                 if (this.IsConnected)
                     return;
@@ -437,6 +460,9 @@ namespace Renci.SshNet
                     if (this.IsConnected)
                         return;
 
+                    // reset connection specific information
+                    Reset();
+
                     //  Build list of available messages while connecting
                     this._messagesMetadata = GetMessagesMetadata();
 
@@ -459,7 +485,6 @@ namespace Renci.SshNet
                             break;
                     }
 
-
                     Match versionMatch;
 
                     //  Get server version from the server,
@@ -475,7 +500,7 @@ namespace Renci.SshNet
                             throw new InvalidOperationException("Server string is null or empty.");
                         }
 
-                        versionMatch = _serverVersionRe.Match(this.ServerVersion);
+                        versionMatch = ServerVersionRe.Match(this.ServerVersion);
 
                         if (versionMatch.Success)
                         {
@@ -576,21 +601,44 @@ namespace Renci.SshNet
             }
             finally
             {
-                _authenticationConnection.Release();
+                AuthenticationConnection.Release();
             }
         }
 
         /// <summary>
-        /// Disconnects from the server
+        /// Disconnects from the server.
         /// </summary>
+        /// <remarks>
+        /// This sends a <b>SSH_MSG_DISCONNECT</b> message to the server, waits for the
+        /// server to close the socket on its end and subsequently closes the client socket.
+        /// </remarks>
         public void Disconnect()
+        {
+            Disconnect(DisconnectReason.ByApplication, "Connection terminated by the client.");
+        }
+
+        private void Disconnect(DisconnectReason reason, string message)
         {
             this._isDisconnecting = true;
 
-            //  If socket still open try to send disconnect message to the server
-            this.SendDisconnect(DisconnectReason.ByApplication, "Connection terminated by the client.");
+            //  send disconnect message to the server if the connection is still open
+            // and the disconnect message has not yet been sent
+            //
+            // note that this should also cause the listener thread to be stopped as
+            // the server should respond by closing the socket
+            SendDisconnect(reason, message);
+
+            // disconnect socket, and dispose it
+            SocketDisconnectAndDispose();
 
-            //this.Dispose();
+            if (_messageListenerCompleted != null)
+            {
+                // at this point, we are sure that the listener thread will stop
+                // as we've disconnected the socket
+                _messageListenerCompleted.WaitOne();
+                _messageListenerCompleted.Dispose();
+                _messageListenerCompleted = null;
+            }
         }
 
         internal T CreateChannel<T>() where T : Channel, new()
@@ -600,7 +648,7 @@ namespace Renci.SshNet
 
         internal T CreateChannel<T>(uint serverChannelNumber, uint windowSize, uint packetSize) where T : Channel, new()
         {
-            T channel = new T();
+            var channel = new T();
             lock (this)
             {
                 channel.Initialize(this, serverChannelNumber, windowSize, packetSize);
@@ -617,24 +665,62 @@ namespace Renci.SshNet
         }
 
         /// <summary>
-        /// Waits for handle to signal while checking other handles as well including timeout check to prevent waiting for ever
+        /// Waits for the specified handle or the exception handle for the receive thread
+        /// to signal within the connection timeout.
         /// </summary>
         /// <param name="waitHandle">The wait handle.</param>
+        /// <exception cref="SshConnectionException">A received package was invalid or failed the message integrity check.</exception>
+        /// <exception cref="SshOperationTimeoutException">None of the handles are signaled in time and the session is not disconnecting.</exception>
+        /// <exception cref="SocketException">A socket error was signaled while receiving messages from the server.</exception>
+        /// <remarks>
+        /// When neither handles are signaled in time and the session is not closing, then the
+        /// session is disconnected.
+        /// 
+        /// </remarks>
         internal void WaitOnHandle(WaitHandle waitHandle)
         {
             var waitHandles = new[]
                 {
                     this._exceptionWaitHandle,
+                    this._messageListenerCompleted,
                     waitHandle
                 };
 
-            switch (WaitHandle.WaitAny(waitHandles, this.ConnectionInfo.Timeout))
+            switch (WaitHandle.WaitAny(waitHandles, ConnectionInfo.Timeout))
             {
                 case 0:
                     throw this._exception;
+                case 1:
+                    // when the session is NOT disconnecting, the listener should actually
+                    // never complete without setting the exception wait handle and should
+                    // end up in case 0... 
+                    //
+                    // when the session is disconnecting, the completion of the listener
+                    // should not be considered an error (quite the oppposite actually)
+                    if (!_isDisconnecting)
+                    {
+                        throw new SshConnectionException("Client not connected.");
+                    }
+                    break;
                 case WaitHandle.WaitTimeout:
-                    this.SendDisconnect(DisconnectReason.ByApplication, "Operation timeout");
-                    throw new SshOperationTimeoutException("Session operation has timed out");
+                    // when the session is NOT disconnecting, then we want to disconnect
+                    // the session altogether in case of a timeout, and throw a
+                    // SshOperationTimeoutException
+                    //
+                    // when the session is disconnecting, a timeout is likely when no
+                    // network connectivity is available; depending on the configured
+                    // timeout either the WaitAny times out first or a SocketException
+                    // detailing a timeout thrown hereby completing the listener thread
+                    // (which makes us end up in case 1). Either way, we do not want to
+                    // disconnect while we're already disconnecting and we do not want
+                    // to report an exception to the client when we're disconnecting
+                    // anyway
+                    if (!_isDisconnecting)
+                    {
+                        this.Disconnect(DisconnectReason.ByApplication, "Operation timeout");
+                        throw new SshOperationTimeoutException("Session operation has timed out");
+                    }
+                    break;
             }
         }
 
@@ -655,14 +741,14 @@ namespace Renci.SshNet
 
             this.Log(string.Format("SendMessage to server '{0}': '{1}'.", message.GetType().Name, message));
 
-            //  Messages can be sent by different thread so we need to synchronize it            
+            //  Messages can be sent by different thread so we need to synchronize it
             var paddingMultiplier = this._clientCipher == null ? (byte)8 : Math.Max((byte)8, this._serverCipher.MinimumSize);    //    Should be recalculate base on cipher min length if cipher specified
 
             var messageData = message.GetBytes();
 
-            if (messageData.Length > Session.MAXIMUM_PAYLOAD_SIZE)
+            if (messageData.Length > MaximumPayloadSize)
             {
-                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Payload cannot be more than {0} bytes.", Session.MAXIMUM_PAYLOAD_SIZE));
+                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Payload cannot be more than {0} bytes.", MaximumPayloadSize));
             }
 
             if (this._clientCompression != null)
@@ -671,7 +757,7 @@ namespace Renci.SshNet
             }
 
             var packetLength = messageData.Length + 4 + 1; //  add length bytes and padding byte
-            byte paddingLength = (byte)((-packetLength) & (paddingMultiplier - 1));
+            var paddingLength = (byte)((-packetLength) & (paddingMultiplier - 1));
             if (paddingLength < paddingMultiplier)
             {
                 paddingLength += paddingMultiplier;
@@ -691,7 +777,7 @@ namespace Renci.SshNet
 
             //  Add random padding
             var paddingBytes = new byte[paddingLength];
-            _randomizer.GetBytes(paddingBytes);
+            Randomizer.GetBytes(paddingBytes);
             paddingBytes.CopyTo(packetData, 4 + 1 + messageData.Length);
 
             //  Lock handling of _outboundPacketSequence since it must be sent sequently to server
@@ -711,9 +797,9 @@ namespace Renci.SshNet
                     packetData = this._clientCipher.Encrypt(packetData);
                 }
 
-                if (packetData.Length > Session.MAXIMUM_PACKET_SIZE)
+                if (packetData.Length > MaximumPacketSize)
                 {
-                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Packet is too big. Maximum packet size is {0} bytes.", Session.MAXIMUM_PACKET_SIZE));
+                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Packet is too big. Maximum packet size is {0} bytes.", MaximumPacketSize));
                 }
 
                 if (this._clientMac == null)
@@ -739,44 +825,44 @@ namespace Renci.SshNet
 
         private static IEnumerable<MessageMetadata> GetMessagesMetadata()
         {
-            return new MessageMetadata[] 
-                { 
-                    new MessageMetadata { Name = "SSH_MSG_NEWKEYS", Number = 21, Enabled = false, Activated = false, Type = typeof(NewKeysMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_REQUEST_FAILURE", Number = 82, Enabled = false, Activated = false, Type = typeof(RequestFailureMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_KEXINIT", Number = 20, Enabled = false, Activated = false, Type = typeof(KeyExchangeInitMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN_FAILURE", Number = 92, Enabled = false, Activated = false, Type = typeof(ChannelOpenFailureMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_FAILURE", Number = 100, Enabled = false, Activated = false, Type = typeof(ChannelFailureMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_EXTENDED_DATA", Number = 95, Enabled = false, Activated = false, Type = typeof(ChannelExtendedDataMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_DATA", Number = 94, Enabled = false, Activated = false, Type = typeof(ChannelDataMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_REQUEST", Number = 50, Enabled = false, Activated = false, Type = typeof(RequestMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_REQUEST", Number = 98, Enabled = false, Activated = false, Type = typeof(ChannelRequestMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_BANNER", Number = 53, Enabled = false, Activated = false, Type = typeof(BannerMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_INFO_RESPONSE", Number = 61, Enabled = false, Activated = false, Type = typeof(InformationResponseMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_FAILURE", Number = 51, Enabled = false, Activated = false, Type = typeof(FailureMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_DEBUG", Number = 4, Enabled = false, Activated = false, Type = typeof(DebugMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_KEXDH_INIT", Number = 30, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhInitMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_GLOBAL_REQUEST", Number = 80, Enabled = false, Activated = false, Type = typeof(GlobalRequestMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN", Number = 90, Enabled = false, Activated = false, Type = typeof(ChannelOpenMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", Number = 91, Enabled = false, Activated = false, Type = typeof(ChannelOpenConfirmationMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_INFO_REQUEST", Number = 60, Enabled = false, Activated = false, Type = typeof(InformationRequestMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_UNIMPLEMENTED", Number = 3, Enabled = false, Activated = false, Type = typeof(UnimplementedMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_REQUEST_SUCCESS", Number = 81, Enabled = false, Activated = false, Type = typeof(RequestSuccessMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_SUCCESS", Number = 99, Enabled = false, Activated = false, Type = typeof(ChannelSuccessMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_PASSWD_CHANGEREQ", Number = 60, Enabled = false, Activated = false, Type = typeof(PasswordChangeRequiredMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_DISCONNECT", Number = 1, Enabled = false, Activated = false, Type = typeof(DisconnectMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_SERVICE_REQUEST", Number = 5, Enabled = false, Activated = false, Type = typeof(ServiceRequestMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_REQUEST", Number = 34, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhGroupExchangeRequest), },
-                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_GROUP", Number = 31, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhGroupExchangeGroup), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_SUCCESS", Number = 52, Enabled = false, Activated = false, Type = typeof(SuccessMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_PK_OK", Number = 60, Enabled = false, Activated = false, Type = typeof(PublicKeyMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_IGNORE", Number = 2, Enabled = false, Activated = false, Type = typeof(IgnoreMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_WINDOW_ADJUST", Number = 93, Enabled = false, Activated = false, Type = typeof(ChannelWindowAdjustMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_EOF", Number = 96, Enabled = false, Activated = false, Type = typeof(ChannelEofMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_CLOSE", Number = 97, Enabled = false, Activated = false, Type = typeof(ChannelCloseMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_SERVICE_ACCEPT", Number = 6, Enabled = false, Activated = false, Type = typeof(ServiceAcceptMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_KEXDH_REPLY", Number = 31, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhReplyMessage), },
-                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_INIT", Number = 32, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhGroupExchangeInit), },
-                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_REPLY", Number = 33, Enabled = false, Activated = false, Type = typeof(KeyExchangeDhGroupExchangeReply), }
+            return new []
+                {
+                    new MessageMetadata { Name = "SSH_MSG_NEWKEYS", Number = 21, Type = typeof(NewKeysMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_REQUEST_FAILURE", Number = 82, Type = typeof(RequestFailureMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_KEXINIT", Number = 20, Type = typeof(KeyExchangeInitMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN_FAILURE", Number = 92, Type = typeof(ChannelOpenFailureMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_FAILURE", Number = 100, Type = typeof(ChannelFailureMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_EXTENDED_DATA", Number = 95, Type = typeof(ChannelExtendedDataMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_DATA", Number = 94, Type = typeof(ChannelDataMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_REQUEST", Number = 50, Type = typeof(RequestMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_REQUEST", Number = 98, Type = typeof(ChannelRequestMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_BANNER", Number = 53, Type = typeof(BannerMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_INFO_RESPONSE", Number = 61, Type = typeof(InformationResponseMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_FAILURE", Number = 51, Type = typeof(FailureMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_DEBUG", Number = 4, Type = typeof(DebugMessage), },
+                    new MessageMetadata { Name = "SSH_MSG_KEXDH_INIT", Number = 30, Type = typeof(KeyExchangeDhInitMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_GLOBAL_REQUEST", Number = 80, Type = typeof(GlobalRequestMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN", Number = 90, Type = typeof(ChannelOpenMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", Number = 91, Type = typeof(ChannelOpenConfirmationMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_INFO_REQUEST", Number = 60, Type = typeof(InformationRequestMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_UNIMPLEMENTED", Number = 3, Type = typeof(UnimplementedMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_REQUEST_SUCCESS", Number = 81, Type = typeof(RequestSuccessMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_SUCCESS", Number = 99, Type = typeof(ChannelSuccessMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_PASSWD_CHANGEREQ", Number = 60, Type = typeof(PasswordChangeRequiredMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_DISCONNECT", Number = 1, Type = typeof(DisconnectMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_SERVICE_REQUEST", Number = 5, Type = typeof(ServiceRequestMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_REQUEST", Number = 34, Type = typeof(KeyExchangeDhGroupExchangeRequest) },
+                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_GROUP", Number = 31, Type = typeof(KeyExchangeDhGroupExchangeGroup) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_SUCCESS", Number = 52, Type = typeof(SuccessMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_USERAUTH_PK_OK", Number = 60, Type = typeof(PublicKeyMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_IGNORE", Number = 2, Type = typeof(IgnoreMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_WINDOW_ADJUST", Number = 93, Type = typeof(ChannelWindowAdjustMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_EOF", Number = 96, Type = typeof(ChannelEofMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_CHANNEL_CLOSE", Number = 97, Type = typeof(ChannelCloseMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_SERVICE_ACCEPT", Number = 6, Type = typeof(ServiceAcceptMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_KEXDH_REPLY", Number = 31, Type = typeof(KeyExchangeDhReplyMessage) },
+                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_INIT", Number = 32, Type = typeof(KeyExchangeDhGroupExchangeInit) },
+                    new MessageMetadata { Name = "SSH_MSG_KEX_DH_GEX_REPLY", Number = 33, Type = typeof(KeyExchangeDhGroupExchangeReply) }
                 };
         }
 
@@ -787,9 +873,6 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException"></exception>
         private Message ReceiveMessage()
         {
-            if (!this._socket.Connected)
-                return null;
-
             //  No lock needed since all messages read by only one thread
             var blockSize = this._serverCipher == null ? (byte)8 : Math.Max((byte)8, this._serverCipher.MinimumSize);
 
@@ -804,11 +887,11 @@ namespace Renci.SshNet
             var packetLength = (uint)(firstBlock[0] << 24 | firstBlock[1] << 16 | firstBlock[2] << 8 | firstBlock[3]);
 
             //  Test packet minimum and maximum boundaries
-            if (packetLength < Math.Max((byte)16, blockSize) - 4 || packetLength > Session.MAXIMUM_PACKET_SIZE - 4)
+            if (packetLength < Math.Max((byte)16, blockSize) - 4 || packetLength > MaximumPacketSize - 4)
                 throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length {0}", packetLength), DisconnectReason.ProtocolError);
 
             //  Read rest of the packet data
-            int bytesToRead = (int)(packetLength - (blockSize - 4));
+            var bytesToRead = (int)(packetLength - (blockSize - 4));
 
             var data = new byte[bytesToRead + blockSize];
 
@@ -851,7 +934,7 @@ namespace Renci.SshNet
                 messagePayload = this._serverDecompression.Decompress(messagePayload);
             }
 
-            //  Validate message against MAC            
+            //  Validate message against MAC
             if (this._serverMac != null)
             {
                 var clientHashData = new byte[4 + data.Length];
@@ -876,8 +959,9 @@ namespace Renci.SshNet
 
         private void SendDisconnect(DisconnectReason reasonCode, string message)
         {
-            //  If disconnect message was sent already dont send it again
-            if (this._isDisconnectMessageSent)
+            // only send a disconnect message if it wasn't already sent, and we're
+            // still connected
+            if (this._isDisconnectMessageSent || !IsConnected)
                 return;
 
             var disconnectMessage = new DisconnectMessage(reasonCode, message);
@@ -905,19 +989,8 @@ namespace Renci.SshNet
         {
             this.OnDisconnectReceived(message);
 
-            //  Shutdown and disconnect from the socket
-            if (this._socket != null)
-            {
-                lock (this._socketLock)
-                {
-                    if (this._socket != null)
-                    {
-                        this.SocketDisconnect();
-                        this._socket.Dispose();
-                        this._socket = null;
-                    }
-                }
-            }
+            //  disconnect from the socket, and dispose it
+            SocketDisconnectAndDispose();
         }
 
         private void HandleMessage(IgnoreMessage message)
@@ -1152,7 +1225,7 @@ namespace Renci.SshNet
             //  Disable all registered messages except key exchange related
             foreach (var messageMetadata in this._messagesMetadata)
             {
-                if (messageMetadata.Activated == true && messageMetadata.Number > 2 && (messageMetadata.Number < 20 || messageMetadata.Number > 30))
+                if (messageMetadata.Activated && messageMetadata.Number > 2 && (messageMetadata.Number < 20 || messageMetadata.Number > 30))
                     messageMetadata.Enabled = false;
             }
 
@@ -1226,7 +1299,7 @@ namespace Renci.SshNet
             //  Enable all active registered messages
             foreach (var messageMetadata in this._messagesMetadata)
             {
-                if (messageMetadata.Activated == true)
+                if (messageMetadata.Activated)
                     messageMetadata.Enabled = true;
             }
 
@@ -1241,6 +1314,14 @@ namespace Renci.SshNet
             this._keyExchangeInProgress = false;
         }
 
+        /// <summary>
+        /// Called when client is disconnecting from the server.
+        /// </summary>
+        internal void OnDisconnecting()
+        {
+            _isDisconnecting = true;
+        }
+
         /// <summary>
         /// Called when <see cref="RequestMessage"/> message received.
         /// </summary>
@@ -1481,13 +1562,15 @@ namespace Renci.SshNet
         }
 
         /// <summary>
-        /// Reads the specified length of bytes from the server
+        /// Reads the specified length of bytes from the server.
         /// </summary>
         /// <param name="length">The length.</param>
-        /// <returns></returns>
+        /// <returns>
+        /// The bytes read from the server.
+        /// </returns>
         private byte[] Read(int length)
         {
-            byte[] buffer = new byte[length];
+            var buffer = new byte[length];
 
             this.SocketRead(length, ref buffer);
 
@@ -1523,7 +1606,6 @@ namespace Renci.SshNet
         {
             var messageType = data[0];
             var messageMetadata = (from m in this._messagesMetadata where m.Number == messageType && m.Enabled && m.Activated select m).SingleOrDefault();
-
             if (messageMetadata == null)
                 throw new SshException(string.Format(CultureInfo.CurrentCulture, "Message type {0} is not valid.", messageType));
 
@@ -1544,6 +1626,12 @@ namespace Renci.SshNet
 
         partial void ExecuteThread(Action action);
 
+        /// <summary>
+        /// Gets a value indicating whether the socket is connected.
+        /// </summary>
+        /// <value>
+        /// <c>true</c> if the socket is connected; otherwise, <c>false</c>.
+        /// </value>
         partial void IsSocketConnected(ref bool isConnected);
 
         partial void SocketConnect(string host, int port);
@@ -1562,6 +1650,23 @@ namespace Renci.SshNet
         /// <param name="data">The data.</param>
         partial void SocketWrite(byte[] data);
 
+        private void SocketDisconnectAndDispose()
+        {
+            if (_socket != null)
+            {
+                lock (_socketLock)
+                {
+                    if (_socket != null)
+                    {
+                        SocketDisconnect();
+                        _socket.Dispose();
+                        _socket = null;
+                    }
+                }
+            }
+
+        }
+
         /// <summary>
         /// Listens for incoming message from the server and handles them. This method run as a task on separate thread.
         /// </summary>
@@ -1572,12 +1677,6 @@ namespace Renci.SshNet
                 while (this._socket != null && this._socket.Connected)
                 {
                     var message = this.ReceiveMessage();
-
-                    if (message == null)
-                    {
-                        throw new NullReferenceException("The 'message' variable cannot be null");
-                    }
-
                     this.HandleMessageCore(message);
                 }
             }
@@ -1598,7 +1697,7 @@ namespace Renci.SshNet
 
         private void SocketWriteByte(byte data)
         {
-            this.SocketWrite(new byte[] { data });
+            this.SocketWrite(new[] {data});
         }
 
         private void ConnectSocks4()
@@ -1618,7 +1717,7 @@ namespace Renci.SshNet
             this.SocketWrite(ipAddress.GetAddressBytes());
 
             //  Send username
-            var username = new Renci.SshNet.Common.ASCIIEncoding().GetBytes(this.ConnectionInfo.ProxyUsername);
+            var username = new Common.ASCIIEncoding().GetBytes(this.ConnectionInfo.ProxyUsername);
             this.SocketWrite(username);
             this.SocketWriteByte(0x00);
 
@@ -1645,7 +1744,7 @@ namespace Renci.SshNet
                     throw new ProxyException("SOCKS4: Not valid response.");
             }
 
-            byte[] dummyBuffer = new byte[4];
+            var dummyBuffer = new byte[4];
 
             //  Read 2 bytes to be ignored
             this.SocketRead(2, ref dummyBuffer);
@@ -1680,7 +1779,7 @@ namespace Renci.SshNet
                     //  Send version
                     this.SocketWriteByte(0x01);
 
-                    var encoding = new Renci.SshNet.Common.ASCIIEncoding();
+                    var encoding = new Common.ASCIIEncoding();
 
                     var username = encoding.GetBytes(this.ConnectionInfo.ProxyUsername);
 
@@ -1816,7 +1915,7 @@ namespace Renci.SshNet
             var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
             var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");
 
-            var encoding = new Renci.SshNet.Common.ASCIIEncoding();
+            var encoding = new Common.ASCIIEncoding();
 
             this.SocketWrite(encoding.GetBytes(string.Format("CONNECT {0}:{1} HTTP/1.0\r\n", this.ConnectionInfo.Host, this.ConnectionInfo.Port)));
 
@@ -1890,23 +1989,57 @@ namespace Renci.SshNet
         {
             var connectionException = exp as SshConnectionException;
 
-            //  If connection exception was raised while isDisconnecting is true then this is expected
-            //  case and should be ignore
-            if (connectionException != null && this._isDisconnecting)
-                return;
+            if (_isDisconnecting)
+            {
+                //  a connection exception which is raised while isDisconnecting is normal and
+                //  should be ignored
+                if (connectionException != null)
+                    return;
+
+                // any timeout while disconnecting can be caused by loss of connectivity
+                // altogether and should be ignored
+                var socketException = exp as SocketException;
+                if (socketException != null && socketException.SocketErrorCode == SocketError.TimedOut)
+                    return;
+            }
 
             this._exception = exp;
 
             this._exceptionWaitHandle.Set();
 
-            if (this.ErrorOccured != null)
+            SignalErrorOccurred(exp);
+
+            if (connectionException != null && connectionException.DisconnectReason != DisconnectReason.ConnectionLost)
             {
-                this.ErrorOccured(this, new ExceptionEventArgs(exp));
+                this.Disconnect(connectionException.DisconnectReason, exp.ToString());
             }
+        }
 
-            if (connectionException != null && connectionException.DisconnectReason != DisconnectReason.ConnectionLost)
+        /// <summary>
+        /// Resets connection-specific information to ensure state of a previous connection
+        /// does not affect new connections.
+        /// </summary>
+        private void Reset()
+        {
+            if (_exceptionWaitHandle != null)
+                _exceptionWaitHandle.Reset();
+            if (_keyExchangeCompletedWaitHandle != null)
+                _keyExchangeCompletedWaitHandle.Reset();
+
+            SessionId = null;
+            _isDisconnectMessageSent = false;
+            _isDisconnecting = false;
+            _isAuthenticated = false;
+            _exception = null;
+            _keyExchangeInProgress = false;
+        }
+
+        private void SignalErrorOccurred(Exception error)
+        {
+            var errorOccurred = ErrorOccured;
+            if (errorOccurred != null)
             {
-                this.SendDisconnect(connectionException.DisconnectReason, exp.ToString());
+                errorOccurred(this, new ExceptionEventArgs(error));
             }
         }
 
@@ -1920,7 +2053,6 @@ namespace Renci.SshNet
         public void Dispose()
         {
             Dispose(true);
-
             GC.SuppressFinalize(this);
         }
 
@@ -1937,20 +2069,7 @@ namespace Renci.SshNet
                 // and unmanaged ResourceMessages.
                 if (disposing)
                 {
-
-                    if (this._socket != null)
-                    {
-                        this._socket.Dispose();
-                        this._socket = null;
-                    }
-
-                    if (this._messageListenerCompleted != null)
-                    {
-                        //  Wait for socket to be closed and for task to complete before disposing a task
-                        this._messageListenerCompleted.WaitOne();
-                        this._messageListenerCompleted.Dispose();
-                        this._messageListenerCompleted = null;
-                    }
+                    Disconnect();
 
                     if (this._serviceAccepted != null)
                     {

+ 209 - 49
Renci.SshClient/Renci.SshNet/SftpClient.cs

@@ -17,41 +17,95 @@ namespace Renci.SshNet
     public partial class SftpClient : BaseClient
     {
         /// <summary>
-        /// Holds SftpSession instance that used to communicate to the SFTP server
+        /// Holds the <see cref="SftpSession"/> instance that used to communicate to the
+        /// SFTP server.
         /// </summary>
         private SftpSession _sftpSession;
 
-        private readonly bool _disposeConnectionInfo;
+        /// <summary>
+        /// Holds the operation timeout.
+        /// </summary>
+        private TimeSpan _operationTimeout;
+
+        /// <summary>
+        /// Holds the size of the buffer.
+        /// </summary>
+        private uint _bufferSize;
 
         /// <summary>
         /// Gets or sets the operation timeout.
         /// </summary>
-        /// <value>The operation timeout.</value>
-        public TimeSpan OperationTimeout { get; set; }
+        /// <value>
+        /// The timeout to wait until an operation completes. The default value is negative
+        /// one (-1) milliseconds, which indicates an infinite time-out period.
+        /// </value>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public TimeSpan OperationTimeout
+        {
+            get
+            {
+                CheckDisposed();
+                return _operationTimeout;
+            }
+            set
+            {
+                CheckDisposed();
+                _operationTimeout = value;
+            }
+        }
 
         /// <summary>
         /// Gets or sets the size of the buffer.
         /// </summary>
-        /// <value>The size of the buffer.</value>
-        public uint BufferSize { get; set; }
+        /// <value>
+        /// The size of the buffer. The default buffer size is 16384 bytes.
+        /// </value>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public uint BufferSize
+        {
+            get
+            {
+                CheckDisposed();
+                return _bufferSize;
+            }
+            set
+            {
+                CheckDisposed();
+                _bufferSize = value;
+            }
+        }
 
         /// <summary>
         /// Gets remote working directory.
         /// </summary>
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public string WorkingDirectory
         {
             get
             {
-                if (this._sftpSession == null)
-                    return null;
-                return this._sftpSession.WorkingDirectory;
+                CheckDisposed();
+                if (_sftpSession == null)
+                    throw new SshConnectionException("Client not connected.");
+                return _sftpSession.WorkingDirectory;
             }
         }
 
         /// <summary>
         /// Gets sftp protocol version.
         /// </summary>
-        public int ProtocolVersion { get; private set; }
+        /// <exception cref="SshConnectionException">Client is not connected.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
+        public int ProtocolVersion
+        {
+            get
+            {
+                CheckDisposed();
+                if (_sftpSession == null)
+                    throw new SshConnectionException("Client not connected.");
+                return (int) _sftpSession.ProtocolVersion;
+            }
+        }
 
         #region Constructors
 
@@ -61,10 +115,8 @@ namespace Renci.SshNet
         /// <param name="connectionInfo">The connection info.</param>
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <b>null</b>.</exception>
         public SftpClient(ConnectionInfo connectionInfo)
-            : base(connectionInfo)
+            : this(connectionInfo, false)
         {
-            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
-            this.BufferSize = 1024 * 16;
         }
 
         /// <summary>
@@ -79,9 +131,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public SftpClient(string host, int port, string username, string password)
-            : this(new PasswordConnectionInfo(host, port, username, password))
+            : this(new PasswordConnectionInfo(host, port, username, password), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -109,9 +160,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public SftpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
-            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
+            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -127,6 +177,23 @@ namespace Renci.SshNet
         {
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SftpClient"/> class.
+        /// </summary>
+        /// <param name="connectionInfo">The connection info.</param>
+        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
+        /// <remarks>
+        /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
+        /// connection info will be disposed when this instance is disposed.
+        /// </remarks>
+        private SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
+            : base(connectionInfo, ownsConnectionInfo)
+        {
+            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
+            this.BufferSize = 1024 * 16;
+        }
+
         #endregion
 
         /// <summary>
@@ -138,8 +205,11 @@ namespace Renci.SshNet
         /// <exception cref="SftpPermissionDeniedException">Permission to change directory denied by remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException">The path in <paramref name="path"/> was not found on the remote host.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void ChangeDirectory(string path)
         {
+            CheckDisposed();
+
             if (path == null)
                 throw new ArgumentNullException("path");
 
@@ -159,10 +229,10 @@ namespace Renci.SshNet
         /// <exception cref="SftpPermissionDeniedException">Permission to change permission on the path(s) was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="SftpPathNotFoundException">The path in <paramref name="path"/> was not found on the remote host.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void ChangePermissions(string path, short mode)
         {
             var file = this.Get(path);
-
             file.SetPermissions(mode);
         }
 
@@ -174,8 +244,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void CreateDirectory(string path)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException(path);
 
@@ -195,8 +268,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to delete the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void DeleteDirectory(string path)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException("path");
 
@@ -216,8 +292,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void DeleteFile(string path)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException("path");
 
@@ -238,6 +317,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void RenameFile(string oldPath, string newPath)
         {
             this.RenameFile(oldPath, newPath, false);
@@ -249,13 +329,15 @@ namespace Renci.SshNet
         /// <param name="oldPath">Path to the old file location.</param>
         /// <param name="newPath">Path to the new file location.</param>
         /// <param name="isPosix">if set to <c>true</c> then perform a posix rename.</param>
-        /// <exception cref="System.ArgumentNullException">oldPath</exception>
         /// <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="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void RenameFile(string oldPath, string newPath, bool isPosix)
         {
+            CheckDisposed();
+
             if (oldPath == null)
                 throw new ArgumentNullException("oldPath");
 
@@ -288,8 +370,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to create the symbolic link was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void SymbolicLink(string path, string linkPath)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException("path");
 
@@ -318,8 +403,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public IEnumerable<SftpFile> ListDirectory(string path, Action<int> listCallback = null)
         {
+            CheckDisposed();
+
             return InternalListDirectory(path, listCallback);
         }
 
@@ -333,8 +421,11 @@ namespace Renci.SshNet
         /// <returns>
         /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
         /// </returns>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public IAsyncResult BeginListDirectory(string path, AsyncCallback asyncCallback, object state, Action<int> listCallback = null)
         {
+            CheckDisposed();
+
             var asyncResult = new SftpListDirectoryAsyncResult(asyncCallback, state);
 
             this.ExecuteThread(() =>
@@ -386,11 +477,13 @@ namespace Renci.SshNet
         /// </summary>
         /// <param name="path">The path.</param>
         /// <returns>Reference to <see cref="Renci.SshNet.Sftp.SftpFile"/> file object.</returns>
-        /// <exception cref="System.ArgumentNullException">path</exception>
         /// <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 SftpFile Get(string path)
         {
+            CheckDisposed();
+
             if (path == null)
                 throw new ArgumentNullException("path");
 
@@ -413,8 +506,11 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message"/> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public bool Exists(string path)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException("path");
 
@@ -441,11 +537,14 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
         public void DownloadFile(string path, Stream output, Action<ulong> downloadCallback = null)
         {
+            CheckDisposed();
+
             this.InternalDownloadFile(path, output, null, downloadCallback);
         }
 
@@ -457,13 +556,12 @@ namespace Renci.SshNet
         /// <returns>
         /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
         /// </returns>
-        /// <exception cref="System.ArgumentException">path</exception>
-        /// <exception cref="System.ArgumentNullException">output</exception>
         /// <exception cref="ArgumentNullException"><paramref name="output" /> is <b>null</b>.</exception>
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -481,13 +579,12 @@ namespace Renci.SshNet
         /// <returns>
         /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
         /// </returns>
-        /// <exception cref="System.ArgumentException">path</exception>
-        /// <exception cref="System.ArgumentNullException">output</exception>
         /// <exception cref="ArgumentNullException"><paramref name="output" /> is <b>null</b>.</exception>
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -507,18 +604,19 @@ namespace Renci.SshNet
         /// <returns>
         /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
         /// </returns>
-        /// <exception cref="System.ArgumentException">path</exception>
-        /// <exception cref="System.ArgumentNullException">output</exception>
         /// <exception cref="ArgumentNullException"><paramref name="output" /> is <b>null</b>.</exception>
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
         public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback asyncCallback, object state, Action<ulong> downloadCallback = null)
         {
+            CheckDisposed();
+
             if (path.IsNullOrWhiteSpace())
                 throw new ArgumentException("path");
 
@@ -579,6 +677,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -599,11 +698,14 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
         /// <exception cref="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
         public void UploadFile(Stream input, string path, bool canOverride, Action<ulong> uploadCallback = null)
         {
+            CheckDisposed();
+
             var flags = Flags.Write | Flags.Truncate;
 
             if (canOverride)
@@ -627,6 +729,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -649,6 +752,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -673,6 +777,7 @@ namespace Renci.SshNet
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
@@ -693,18 +798,19 @@ namespace Renci.SshNet
         /// <returns>
         /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
         /// </returns>
-        /// <exception cref="System.ArgumentNullException">input</exception>
-        /// <exception cref="System.ArgumentException">path</exception>
         /// <exception cref="ArgumentNullException"><paramref name="input" /> is <b>null</b>.</exception>
         /// <exception cref="ArgumentException"><paramref name="path" /> is <b>null</b> or contains whitespace characters.</exception>
         /// <exception cref="SshConnectionException">Client is not connected.</exception>
         /// <exception cref="Renci.SshNet.Common.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="T:Renci.SshNet.Common.SshException">A SSH error where <see cref="P:System.Exception.Message" /> is the message from the remote host.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         /// <remarks>
         /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
         /// </remarks>
         public IAsyncResult BeginUploadFile(Stream input, string path, bool canOverride, AsyncCallback asyncCallback, object state, Action<ulong> uploadCallback = null)
         {
+            CheckDisposed();
+
             if (input == null)
                 throw new ArgumentNullException("input");
 
@@ -768,11 +874,13 @@ namespace Renci.SshNet
         /// </summary>
         /// <param name="path">The path.</param>
         /// <returns>Reference to <see cref="Renci.SshNet.Sftp.SftpFileSytemInformation"/> object that contains file status information.</returns>
-        /// <exception cref="System.ArgumentNullException">path</exception>
         /// <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 SftpFileSytemInformation GetStatus(string path)
         {
+            CheckDisposed();
+
             if (path == null)
                 throw new ArgumentNullException("path");
 
@@ -792,8 +900,11 @@ namespace Renci.SshNet
         /// <param name="path">The file to append the lines to. The file is created if it does not already exist.</param>
         /// <param name="contents">The lines to append to the file.</param>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is<b>null</b> <para>-or-</para> <paramref name="contents"/> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void AppendAllLines(string path, IEnumerable<string> contents)
         {
+            CheckDisposed();
+
             if (contents == null)
                 throw new ArgumentNullException("contents");
 
@@ -813,8 +924,11 @@ namespace Renci.SshNet
         /// <param name="contents">The lines to append to the file.</param>
         /// <param name="encoding">The character encoding to use.</param>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>. <para>-or-</para> <paramref name="contents"/> is <b>null</b>. <para>-or-</para> <paramref name="encoding"/> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding)
         {
+            CheckDisposed();
+
             if (contents == null)
                 throw new ArgumentNullException("contents");
 
@@ -834,6 +948,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to append the specified string to.</param>
         /// <param name="contents">The string to append to the file.</param>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>. <para>-or-</para> <paramref name="contents"/> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void AppendAllText(string path, string contents)
         {
             using (var stream = this.AppendText(path))
@@ -850,6 +965,7 @@ namespace Renci.SshNet
         /// <param name="contents">The string to append to the file.</param>
         /// <param name="encoding">The character encoding to use.</param>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>. <para>-or-</para> <paramref name="contents"/> is <b>null</b>. <para>-or-</para> <paramref name="encoding"/> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void AppendAllText(string path, string contents, Encoding encoding)
         {
             using (var stream = this.AppendText(path, encoding))
@@ -864,6 +980,7 @@ namespace Renci.SshNet
         /// <param name="path">The path to the file to append to.</param>
         /// <returns>A StreamWriter that appends UTF-8 encoded text to an existing file.</returns>
         /// <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 StreamWriter AppendText(string path)
         {
             return this.AppendText(path, Encoding.UTF8);
@@ -878,8 +995,11 @@ namespace Renci.SshNet
         /// A StreamWriter that appends UTF-8 encoded text to an existing file.
         /// </returns>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is <b>null</b>. <para>-or-</para> <paramref name="encoding"/> is <b>null</b>.</exception>
+        /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public StreamWriter AppendText(string path, Encoding encoding)
         {
+            CheckDisposed();
+
             if (encoding == null)
                 throw new ArgumentNullException("encoding");
 
@@ -892,8 +1012,11 @@ namespace Renci.SshNet
         /// <param name="path">The path and name of the file to create.</param>
         /// <returns>A <see cref="SftpFileStream"/> that provides read/write access to the file specified in path</returns>
         /// <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 SftpFileStream Create(string path)
         {
+            CheckDisposed();
+
             return new SftpFileStream(this._sftpSession, path, FileMode.Create, FileAccess.ReadWrite);
         }
 
@@ -904,8 +1027,11 @@ namespace Renci.SshNet
         /// <param name="bufferSize">The number of bytes buffered for reads and writes to the file.</param>
         /// <returns>A <see cref="SftpFileStream"/> that provides read/write access to the file specified in path</returns>
         /// <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 SftpFileStream Create(string path, int bufferSize)
         {
+            CheckDisposed();
+
             return new SftpFileStream(this._sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize);
         }
 
@@ -915,9 +1041,10 @@ namespace Renci.SshNet
         /// <param name="path">The file to be opened for writing.</param>
         /// <returns>A <see cref="System.IO.StreamWriter"/> that writes to the specified file using UTF-8 encoding.</returns>
         /// <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 StreamWriter CreateText(string path)
         {
-            return new StreamWriter(this.OpenWrite(path), Encoding.UTF8);
+            return CreateText(path, Encoding.UTF8);
         }
 
         /// <summary>
@@ -927,8 +1054,11 @@ namespace Renci.SshNet
         /// <param name="encoding">The character encoding to use.</param>
         /// <returns> A <see cref="System.IO.StreamWriter"/> that writes to the specified file using UTF-8 encoding. </returns>
         /// <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 StreamWriter CreateText(string path, Encoding encoding)
         {
+            CheckDisposed();
+
             return new StreamWriter(this.OpenWrite(path), encoding);
         }
 
@@ -938,10 +1068,10 @@ namespace Renci.SshNet
         /// <param name="path">The name of the file or directory to be deleted. Wildcard characters are not supported.</param>
         /// <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 void Delete(string path)
         {
             var file = this.Get(path);
-
             file.Delete();
         }
 
@@ -952,10 +1082,10 @@ namespace Renci.SshNet
         /// <returns>A <see cref="System.DateTime"/> structure set to the date and time that the specified file or directory was last accessed. This value is expressed in local time.</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 DateTime GetLastAccessTime(string path)
         {
             var file = this.Get(path);
-
             return file.LastAccessTime;
         }
 
@@ -966,11 +1096,11 @@ namespace Renci.SshNet
         /// <returns>A <see cref="System.DateTime"/> structure set to the date and time that the specified file or directory was last accessed. This value is expressed in UTC time.</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 DateTime GetLastAccessTimeUtc(string path)
         {
-            var file = this.Get(path);
-
-            return file.LastAccessTime.ToUniversalTime();
+            var lastAccessTime = GetLastAccessTime(path);
+            return lastAccessTime.ToUniversalTime();
         }
 
         /// <summary>
@@ -980,10 +1110,10 @@ namespace Renci.SshNet
         /// <returns>A <see cref="System.DateTime"/> structure set to the date and time that the specified file or directory was last written to. This value is expressed in local time.</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 DateTime GetLastWriteTime(string path)
         {
             var file = this.Get(path);
-
             return file.LastWriteTime;
         }
 
@@ -994,11 +1124,11 @@ namespace Renci.SshNet
         /// <returns>A <see cref="System.DateTime"/> structure set to the date and time that the specified file or directory was last written to. This value is expressed in UTC time.</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 DateTime GetLastWriteTimeUtc(string path)
         {
-            var file = this.Get(path);
-
-            return file.LastWriteTime.ToUniversalTime();
+            var lastWriteTime = GetLastWriteTime(path);
+            return lastWriteTime.ToUniversalTime();
         }
 
         /// <summary>
@@ -1008,9 +1138,10 @@ namespace Renci.SshNet
         /// <param name="mode">A <see cref="System.IO.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>
         /// <returns>An unshared <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="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public SftpFileStream Open(string path, FileMode mode)
         {
-            return new SftpFileStream(this._sftpSession, path, mode, FileAccess.ReadWrite);
+            return Open(path, mode, FileAccess.ReadWrite);
         }
 
         /// <summary>
@@ -1021,8 +1152,11 @@ namespace Renci.SshNet
         /// <param name="access">A <see cref="System.IO.FileAccess"/> value that specifies the operations that can be performed on the file.</param>
         /// <returns>An unshared <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="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public SftpFileStream Open(string path, FileMode mode, FileAccess access)
         {
+            CheckDisposed();
+
             return new SftpFileStream(this._sftpSession, path, mode, access);
         }
 
@@ -1032,9 +1166,10 @@ namespace Renci.SshNet
         /// <param name="path">The file to be opened for reading.</param>
         /// <returns>A read-only System.IO.FileStream on the specified path.</returns>
         /// <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 SftpFileStream OpenRead(string path)
         {
-            return new SftpFileStream(this._sftpSession, path, FileMode.Open, FileAccess.Read);
+            return Open(path, FileMode.Open, FileAccess.Read);
         }
 
         /// <summary>
@@ -1043,6 +1178,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to be opened for reading.</param>
         /// <returns>A <see cref="System.IO.StreamReader"/> on the specified path.</returns>
         /// <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 StreamReader OpenText(string path)
         {
             return new StreamReader(this.OpenRead(path), Encoding.UTF8);
@@ -1054,8 +1190,11 @@ namespace Renci.SshNet
         /// <param name="path">The file to be opened for writing.</param>
         /// <returns>An unshared <see cref="SftpFileStream"/> object on the specified path with <see cref="System.IO.FileAccess.Write"/> access.</returns>
         /// <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 SftpFileStream OpenWrite(string path)
         {
+            CheckDisposed();
+
             return new SftpFileStream(this._sftpSession, path, FileMode.OpenOrCreate, FileAccess.Write);
         }
 
@@ -1065,6 +1204,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to open for reading.</param>
         /// <returns>A byte array containing the contents of the file.</returns>
         /// <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 byte[] ReadAllBytes(string path)
         {
             using (var stream = this.OpenRead(path))
@@ -1081,6 +1221,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to open for reading.</param>
         /// <returns>A string array containing all lines of the file.</returns>
         /// <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 string[] ReadAllLines(string path)
         {
             return this.ReadAllLines(path, Encoding.UTF8);
@@ -1093,6 +1234,7 @@ namespace Renci.SshNet
         /// <param name="encoding">The encoding applied to the contents of the file.</param>
         /// <returns>A string array containing all lines of the file.</returns>
         /// <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 string[] ReadAllLines(string path, Encoding encoding)
         {
             var lines = new List<string>();
@@ -1112,6 +1254,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to open for reading.</param>
         /// <returns>A string containing all lines of the file.</returns>
         /// <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 string ReadAllText(string path)
         {
             return this.ReadAllText(path, Encoding.UTF8);
@@ -1124,6 +1267,7 @@ namespace Renci.SshNet
         /// <param name="encoding">The encoding applied to the contents of the file.</param>
         /// <returns>A string containing all lines of the file.</returns>
         /// <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 string ReadAllText(string path, Encoding encoding)
         {
             using (var stream = new StreamReader(this.OpenRead(path), encoding))
@@ -1138,6 +1282,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to read.</param>
         /// <returns>The lines of the file.</returns>
         /// <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 IEnumerable<string> ReadLines(string path)
         {
             return this.ReadAllLines(path);
@@ -1150,6 +1295,7 @@ namespace Renci.SshNet
         /// <param name="encoding">The encoding that is applied to the contents of the file.</param>
         /// <returns>The lines of the file.</returns>
         /// <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 IEnumerable<string> ReadLines(string path, Encoding encoding)
         {
             return this.ReadAllLines(path, encoding);
@@ -1205,6 +1351,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to write to.</param>
         /// <param name="bytes">The bytes to write to the file.</param>
         /// <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 void WriteAllBytes(string path, byte[] bytes)
         {
             using (var stream = this.OpenWrite(path))
@@ -1219,6 +1366,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to write to.</param>
         /// <param name="contents">The lines to write to the file.</param>
         /// <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 void WriteAllLines(string path, IEnumerable<string> contents)
         {
             this.WriteAllLines(path, contents, Encoding.UTF8);
@@ -1230,6 +1378,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to write to.</param>
         /// <param name="contents">The string array to write to the file.</param>
         /// <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 void WriteAllLines(string path, string[] contents)
         {
             this.WriteAllLines(path, contents, Encoding.UTF8);
@@ -1242,6 +1391,7 @@ namespace Renci.SshNet
         /// <param name="contents">The lines to write to the file.</param>
         /// <param name="encoding">The character encoding to use.</param>
         /// <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 void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding)
         {
             using (var stream = this.CreateText(path, encoding))
@@ -1260,6 +1410,7 @@ namespace Renci.SshNet
         /// <param name="contents">The string array to write to the file.</param>
         /// <param name="encoding">An <see cref="System.Text.Encoding"/> object that represents the character encoding applied to the string array.</param>
         /// <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 void WriteAllLines(string path, string[] contents, Encoding encoding)
         {
             using (var stream = this.CreateText(path, encoding))
@@ -1277,6 +1428,7 @@ namespace Renci.SshNet
         /// <param name="path">The file to write to.</param>
         /// <param name="contents">The string to write to the file.</param>
         /// <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 void WriteAllText(string path, string contents)
         {
             using (var stream = this.CreateText(path))
@@ -1292,6 +1444,7 @@ namespace Renci.SshNet
         /// <param name="contents">The string to write to the file.</param>
         /// <param name="encoding">The encoding to apply to the string.</param>
         /// <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 void WriteAllText(string path, string contents, Encoding encoding)
         {
             using (var stream = this.CreateText(path, encoding))
@@ -1306,8 +1459,11 @@ namespace Renci.SshNet
         /// <param name="path">The path to the file.</param>
         /// <returns>The <see cref="SftpFileAttributes"/> of the file on the path.</returns>
         /// <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 SftpFileAttributes GetAttributes(string path)
         {
+            CheckDisposed();
+
             if (this._sftpSession == null)
                 throw new SshConnectionException("Client not connected.");
 
@@ -1322,8 +1478,11 @@ namespace Renci.SshNet
         /// <param name="path">The path to the file.</param>
         /// <param name="fileAttributes">The desired <see cref="SftpFileAttributes"/>.</param>
         /// <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 void SetAttributes(string path, SftpFileAttributes fileAttributes)
         {
+            CheckDisposed();
+
             if (this._sftpSession == null)
                 throw new SshConnectionException("Client not connected.");
 
@@ -1540,11 +1699,7 @@ namespace Renci.SshNet
             base.OnConnected();
 
             this._sftpSession = new SftpSession(this.Session, this.OperationTimeout, this.ConnectionInfo.Encoding);
-
             this._sftpSession.Connect();
-
-            //  Resolve current running version
-            this.ProtocolVersion = (int)this._sftpSession.ProtocolVersion;
         }
 
         /// <summary>
@@ -1554,7 +1709,15 @@ namespace Renci.SshNet
         {
             base.OnDisconnecting();
 
-            this._sftpSession.Disconnect();
+            // disconnect and dispose the SFTP session
+            // the dispose is necessary since we create a new SFTP session
+            // on each connect
+            if (_sftpSession != null)
+            {
+                this._sftpSession.Disconnect();
+                this._sftpSession.Dispose();
+                this._sftpSession = null;
+            }
         }
 
         /// <summary>
@@ -1569,9 +1732,6 @@ namespace Renci.SshNet
                 this._sftpSession = null;
             }
 
-            if (this._disposeConnectionInfo)
-                ((IDisposable)this.ConnectionInfo).Dispose();
-
             base.Dispose(disposing);
         }
     }

+ 20 - 14
Renci.SshClient/Renci.SshNet/SshClient.cs

@@ -15,12 +15,7 @@ namespace Renci.SshNet
         /// <summary>
         /// Holds the list of forwarded ports
         /// </summary>
-        private readonly List<ForwardedPort> _forwardedPorts = new List<ForwardedPort>();
-
-        /// <summary>
-        /// If true, causes the connectionInfo object to be disposed.
-        /// </summary>
-        private readonly bool _disposeConnectionInfo;
+        private readonly List<ForwardedPort> _forwardedPorts;
 
         private Stream _inputStream;
 
@@ -49,7 +44,7 @@ namespace Renci.SshNet
         /// </example>
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
         public SshClient(ConnectionInfo connectionInfo)
-            : base(connectionInfo)
+            : this(connectionInfo, false)
         {
         }
 
@@ -65,9 +60,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "C2A000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public SshClient(string host, int port, string username, string password)
-            : this(new PasswordConnectionInfo(host, port, username, password))
+            : this(new PasswordConnectionInfo(host, port, username, password), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -102,9 +96,8 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="F:System.Net.IPEndPoint.MinPort"/> and <see cref="System.Net.IPEndPoint.MaxPort"/>.</exception>
         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
         public SshClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
-            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles))
+            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
         {
-            this._disposeConnectionInfo = true;
         }
 
         /// <summary>
@@ -124,6 +117,22 @@ namespace Renci.SshNet
         {
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SshClient"/> class.
+        /// </summary>
+        /// <param name="connectionInfo">The connection info.</param>
+        /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
+        /// <remarks>
+        /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
+        /// connection info will be disposed when this instance is disposed.
+        /// </remarks>
+        private SshClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
+            : base(connectionInfo, ownsConnectionInfo)
+        {
+            _forwardedPorts = new List<ForwardedPort>();
+        }
+
         #endregion
 
         /// <summary>
@@ -405,9 +414,6 @@ namespace Renci.SshNet
         {
             base.Dispose(disposing);
 
-            if (this._disposeConnectionInfo)
-                ((IDisposable)this.ConnectionInfo).Dispose();
-
             if (this._inputStream != null)
             {
                 this._inputStream.Dispose();