diff --git a/Renci.SshClient/Build/nuget/SSH.NET.nuspec b/Renci.SshClient/Build/nuget/SSH.NET.nuspec index dd1c4cdf..137bd380 100644 --- a/Renci.SshClient/Build/nuget/SSH.NET.nuspec +++ b/Renci.SshClient/Build/nuget/SSH.NET.nuspec @@ -16,11 +16,11 @@ * Added callbacks to UploadFile, DownloadFile and ListDirectory in SftpClient (issue #1324) * Allow a given private key file to be used concurrently * Performance improvements: - - optimization of payload size for both read and write operations (SftpClient only) - - increase window size from 1MB to 2MB - - increase buffer size from 16KB to 64KB for SftpClient - - take into account the maximum remote packet size of the channel for write operations - - increase maximum size of packets that we can receive from 32 KB to 64 KB + - optimization of payload size for both read and write operations (SftpClient only) + - increase window size from 1MB to 2MB + - increase buffer size from 16KB to 64KB for SftpClient + - take into account the maximum remote packet size of the channel for write operations + - increase maximum size of packets that we can receive from 32 KB to 64 KB * Improve exception message for authentication failures Breaking changes: @@ -30,6 +30,7 @@ Fixes: + * Timeout sending data to server with low window size (issue #1706) * No connection possible with the same auth method requested multiple times (issue #1930) * Unobserved exception rethrown by finalizer thread (issue #1298 and #1587) * Client cipher is used to decrypt server messages (issue #1917) diff --git a/Renci.SshClient/Renci.SshNet/Channels/Channel.cs b/Renci.SshClient/Renci.SshNet/Channels/Channel.cs index 62227e00..d2833ea2 100644 --- a/Renci.SshClient/Renci.SshNet/Channels/Channel.cs +++ b/Renci.SshClient/Renci.SshNet/Channels/Channel.cs @@ -1,4 +1,4 @@ -using System; + using System; using System.Threading; using Renci.SshNet.Common; using Renci.SshNet.Messages; @@ -360,7 +360,7 @@ namespace Renci.SshNet.Channels /// The message. protected void SendMessage(Message message) { - // Send channel messages only while channel is open + // send channel messages only while channel is open if (!this.IsOpen) return; @@ -373,99 +373,109 @@ namespace Renci.SshNet.Channels /// The message to send. private void SendMessage(ChannelCloseMessage message) { - // Send channel messages only while channel is open + // send channel messages only while channel is open if (!this.IsOpen) return; this._session.SendMessage(message); - // When channel close message is sent channel considered to be closed + // when channel close message is sent channel considered to be closed this.IsOpen = false; } /// /// Sends channel data message to the servers. /// - /// This method takes care of managing the window size. /// Channel data message. - /// The data of exceeds the maximum packet size of the channel. + /// + /// + /// When the data of the message exceeds the maximum packet size or the remote window + /// size does not allow the full message to be sent, then this method will send the + /// data in multiple chunks and will only wait for the remote window size to be adjusted + /// when its zero. + /// + /// + /// This is done to support SSH servers will a small window size that do not agressively + /// increase their window size. We need to take into account that there may be SSH + /// servers that only increase their window size when it has reached zero. + /// + /// protected void SendMessage(ChannelDataMessage message) { - // Send channel messages only while channel is open + // send channel messages only while channel is open if (!this.IsOpen) return; - var messageLength = message.Data.Length; + var totalDataLength = (uint) message.Data.Length; + var totalDataSent = 0u; - // RFC4254: - // The maximum amount of data allowed is determined by the maximum packet size - // for the channel - // - // there's some ambiguity in the RFC, but most ssh implementations take only the - // data into account for determining the size of a packet; the 4 bytes for the - // packet length and the 9 bytes of the header are not considered part of the - // data - if (messageLength > RemotePacketSize) - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, - "The payload of the data message is {0} bytes while the maximum packet size of the channel is {1} bytes.", - messageLength, RemotePacketSize)); - - do + var totalBytesToSend = totalDataLength; + while (totalBytesToSend > 0) { - lock (this._serverWindowSizeLock) + var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend); + if (dataThatCanBeSentInMessage == totalDataLength) { - var serverWindowSize = this.RemoteWindowSize; - if (serverWindowSize < messageLength) - { - // Wait for window to be big enough for this message - this._channelServerWindowAdjustWaitHandle.Reset(); - } - else - { - this.RemoteWindowSize -= (uint)messageLength; - break; - } + // we can send the message in one chunk + this._session.SendMessage(message); } - // Wait for window to change - this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle); - } while (true); - - this._session.SendMessage(message); + else + { + // we need to send the message in multiple chunks + var dataToSend = new byte[dataThatCanBeSentInMessage]; + Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage); + this._session.SendMessage(new ChannelDataMessage(message.LocalChannelNumber, dataToSend)); + } + totalDataSent += dataThatCanBeSentInMessage; + totalBytesToSend -= dataThatCanBeSentInMessage; + } } /// /// Sends channel extended data message to the servers. /// - /// This method takes care of managing the window size. /// Channel data message. + /// + /// + /// When the data of the message exceeds the maximum packet size or the remote window + /// size does not allow the full message to be sent, then this method will send the + /// data in multiple chunks and will only wait for the remote window size to be adjusted + /// when its zero. + /// + /// + /// This is done to support SSH servers will a small window size that do not agressively + /// increase their window size. We need to take into account that there may be SSH + /// servers that only increase their window size when it has reached zero. + /// + /// protected void SendMessage(ChannelExtendedDataMessage message) { - // Send channel messages only while channel is open + // end channel messages only while channel is open if (!this.IsOpen) return; - var messageLength = message.Data.Length; - do - { - lock (this._serverWindowSizeLock) - { - var serverWindowSize = this.RemoteWindowSize; - if (serverWindowSize < messageLength) - { - // Wait for window to be big enough for this message - this._channelServerWindowAdjustWaitHandle.Reset(); - } - else - { - this.RemoteWindowSize -= (uint)messageLength; - break; - } - } - // Wait for window to change - this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle); - } while (true); + var totalDataLength = (uint) message.Data.Length; + var totalDataSent = 0u; - this._session.SendMessage(message); + var totalBytesToSend = totalDataLength; + while (totalBytesToSend > 0) + { + var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend); + if (dataThatCanBeSentInMessage == totalDataLength) + { + // we can send the message in one chunk + this._session.SendMessage(message); + } + else + { + // we need to send the message in multiple chunks + var dataToSend = new byte[dataThatCanBeSentInMessage]; + Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage); + this._session.SendMessage(new ChannelExtendedDataMessage(message.LocalChannelNumber, + message.DataTypeCode, dataToSend)); + } + totalDataSent += dataThatCanBeSentInMessage; + totalBytesToSend -= dataThatCanBeSentInMessage; + } } /// @@ -479,8 +489,8 @@ namespace Renci.SshNet.Channels protected virtual void Close(bool wait) { - // Send message to close the channel on the server - // Ignore sending close message when client not connected + // send message to close the channel on the server + // ignore sending close message when client not connected if (!_closeMessageSent && this.IsConnected) { lock (this) @@ -498,7 +508,7 @@ namespace Renci.SshNet.Channels IsOpen = false; } - // Wait for channel to be closed + // wait for channel to be closed if (wait) { WaitOnHandle(this._channelClosedWaitHandle); @@ -637,6 +647,38 @@ namespace Renci.SshNet.Channels } } + /// + /// Determines the length of data that currently can be sent in a single message. + /// + /// The length of the message that must be sent. + /// + /// The actual data length that currently can be sent. + /// + private uint GetDataLengthThatCanBeSentInMessage(uint messageLength) + { + do + { + lock (this._serverWindowSizeLock) + { + var serverWindowSize = RemoteWindowSize; + if (serverWindowSize == 0) + { + // allow us to be signal when remote window size is adjusted + this._channelServerWindowAdjustWaitHandle.Reset(); + } + else + { + var bytesThatCanBeSent = Math.Min(Math.Min(RemotePacketSize, messageLength), + serverWindowSize); + this.RemoteWindowSize -= bytesThatCanBeSent; + return bytesThatCanBeSent; + } + } + // wait for remote window size to change + this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle); + } while (true); + } + private InvalidOperationException CreateRemoteChannelInfoNotAvailableException() { throw new InvalidOperationException("The channel has not been opened, or the open has not yet been confirmed."); diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs index a38164cc..60c7e55a 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs @@ -1101,6 +1101,9 @@ namespace Renci.SshNet.Sftp /// /// The optimal size of the buffer to write data on the channel. /// + /// + /// Currently, we do not take the remote window size into account. + /// internal uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle) { // 1-4: package length of SSH_FXP_WRITE message