|
|
@@ -81,12 +81,6 @@ namespace Renci.SshNet
|
|
|
private readonly ISocketFactory _socketFactory;
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Holds an object that is used to ensure only a single thread can read from
|
|
|
- /// <see cref="_socket"/> at any given time.
|
|
|
- /// </summary>
|
|
|
- private readonly Lock _socketReadLock = new Lock();
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Holds an object that is used to ensure only a single thread can write to
|
|
|
/// <see cref="_socket"/> at any given time.
|
|
|
@@ -105,7 +99,7 @@ namespace Renci.SshNet
|
|
|
/// This is also used to ensure that <see cref="_socket"/> will not be disposed
|
|
|
/// while performing a given operation or set of operations on <see cref="_socket"/>.
|
|
|
/// </remarks>
|
|
|
- private readonly SemaphoreSlim _socketDisposeLock = new SemaphoreSlim(1, 1);
|
|
|
+ private readonly Lock _socketDisposeLock = new Lock();
|
|
|
|
|
|
/// <summary>
|
|
|
/// Holds an object that is used to ensure only a single thread can connect
|
|
|
@@ -279,17 +273,11 @@ namespace Renci.SshNet
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
- if (_disposed || _isDisconnectMessageSent || !_isAuthenticated)
|
|
|
- {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- if (_messageListenerCompleted is null || _messageListenerCompleted.WaitOne(0))
|
|
|
- {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- return IsSocketConnected();
|
|
|
+ return !_disposed &&
|
|
|
+ !_isDisconnectMessageSent &&
|
|
|
+ _isAuthenticated &&
|
|
|
+ _messageListenerCompleted?.WaitOne(0) == false &&
|
|
|
+ _socket.IsConnected();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1046,7 +1034,7 @@ namespace Renci.SshNet
|
|
|
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
|
|
|
internal void SendMessage(Message message)
|
|
|
{
|
|
|
- if (!_socket.CanWrite())
|
|
|
+ if (!_socket.IsConnected())
|
|
|
{
|
|
|
throw new SshConnectionException("Client not connected.");
|
|
|
}
|
|
|
@@ -1161,9 +1149,7 @@ namespace Renci.SshNet
|
|
|
/// </remarks>
|
|
|
private void SendPacket(byte[] packet, int offset, int length)
|
|
|
{
|
|
|
- _socketDisposeLock.Wait();
|
|
|
-
|
|
|
- try
|
|
|
+ lock (_socketDisposeLock)
|
|
|
{
|
|
|
if (!_socket.IsConnected())
|
|
|
{
|
|
|
@@ -1172,10 +1158,6 @@ namespace Renci.SshNet
|
|
|
|
|
|
SocketAbstraction.Send(_socket, packet, offset, length);
|
|
|
}
|
|
|
- finally
|
|
|
- {
|
|
|
- _ = _socketDisposeLock.Release();
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -1259,76 +1241,70 @@ namespace Renci.SshNet
|
|
|
byte[] data;
|
|
|
uint packetLength;
|
|
|
|
|
|
- // avoid reading from socket while IsSocketConnected is attempting to determine whether the
|
|
|
- // socket is still connected by invoking Socket.Poll(...) and subsequently verifying value of
|
|
|
- // Socket.Available
|
|
|
- lock (_socketReadLock)
|
|
|
+ // Read first block - which starts with the packet length
|
|
|
+ var firstBlock = new byte[blockSize];
|
|
|
+ if (TrySocketRead(socket, firstBlock, 0, blockSize) == 0)
|
|
|
{
|
|
|
- // Read first block - which starts with the packet length
|
|
|
- var firstBlock = new byte[blockSize];
|
|
|
- if (TrySocketRead(socket, firstBlock, 0, blockSize) == 0)
|
|
|
- {
|
|
|
- // connection with SSH server was closed
|
|
|
- return null;
|
|
|
- }
|
|
|
+ // connection with SSH server was closed
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- var plainFirstBlock = firstBlock;
|
|
|
+ var plainFirstBlock = firstBlock;
|
|
|
|
|
|
- // First block is not encrypted in AES GCM mode.
|
|
|
- if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher)
|
|
|
- {
|
|
|
- _serverCipher.SetSequenceNumber(_inboundPacketSequence);
|
|
|
+ // First block is not encrypted in AES GCM mode.
|
|
|
+ if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher)
|
|
|
+ {
|
|
|
+ _serverCipher.SetSequenceNumber(_inboundPacketSequence);
|
|
|
|
|
|
- // First block is not encrypted in ETM mode.
|
|
|
- if (_serverMac == null || !_serverEtm)
|
|
|
- {
|
|
|
- plainFirstBlock = _serverCipher.Decrypt(firstBlock);
|
|
|
- }
|
|
|
+ // First block is not encrypted in ETM mode.
|
|
|
+ if (_serverMac == null || !_serverEtm)
|
|
|
+ {
|
|
|
+ plainFirstBlock = _serverCipher.Decrypt(firstBlock);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock);
|
|
|
+ packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock);
|
|
|
|
|
|
- // Test packet minimum and maximum boundaries
|
|
|
- if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
|
|
|
- {
|
|
|
- throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength),
|
|
|
- DisconnectReason.ProtocolError);
|
|
|
- }
|
|
|
+ // Test packet minimum and maximum boundaries
|
|
|
+ if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
|
|
|
+ {
|
|
|
+ throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength),
|
|
|
+ DisconnectReason.ProtocolError);
|
|
|
+ }
|
|
|
|
|
|
- // Determine the number of bytes left to read; We've already read "blockSize" bytes, but the
|
|
|
- // "packet length" field itself - which is 4 bytes - is not included in the length of the packet
|
|
|
- var bytesToRead = (int)(packetLength - (blockSize - packetLengthFieldLength)) + serverMacLength;
|
|
|
-
|
|
|
- // Construct buffer for holding the payload and the inbound packet sequence as we need both in order
|
|
|
- // to generate the hash.
|
|
|
- //
|
|
|
- // The total length of the "data" buffer is an addition of:
|
|
|
- // - inboundPacketSequenceLength (4 bytes)
|
|
|
- // - packetLength
|
|
|
- // - serverMacLength
|
|
|
- //
|
|
|
- // We include the inbound packet sequence to allow us to have the the full SSH packet in a single
|
|
|
- // byte[] for the purpose of calculating the client hash. Room for the server MAC is foreseen
|
|
|
- // to read the packet including server MAC in a single pass (except for the initial block).
|
|
|
- data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength];
|
|
|
- BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence);
|
|
|
-
|
|
|
- // Use raw packet length field to calculate the mac in AEAD mode.
|
|
|
- if (_serverAead)
|
|
|
- {
|
|
|
- Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize);
|
|
|
- }
|
|
|
+ // Determine the number of bytes left to read; We've already read "blockSize" bytes, but the
|
|
|
+ // "packet length" field itself - which is 4 bytes - is not included in the length of the packet
|
|
|
+ var bytesToRead = (int)(packetLength - (blockSize - packetLengthFieldLength)) + serverMacLength;
|
|
|
+
|
|
|
+ // Construct buffer for holding the payload and the inbound packet sequence as we need both in order
|
|
|
+ // to generate the hash.
|
|
|
+ //
|
|
|
+ // The total length of the "data" buffer is an addition of:
|
|
|
+ // - inboundPacketSequenceLength (4 bytes)
|
|
|
+ // - packetLength
|
|
|
+ // - serverMacLength
|
|
|
+ //
|
|
|
+ // We include the inbound packet sequence to allow us to have the the full SSH packet in a single
|
|
|
+ // byte[] for the purpose of calculating the client hash. Room for the server MAC is foreseen
|
|
|
+ // to read the packet including server MAC in a single pass (except for the initial block).
|
|
|
+ data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength];
|
|
|
+ BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence);
|
|
|
+
|
|
|
+ // Use raw packet length field to calculate the mac in AEAD mode.
|
|
|
+ if (_serverAead)
|
|
|
+ {
|
|
|
+ Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize);
|
|
|
+ }
|
|
|
|
|
|
- if (bytesToRead > 0)
|
|
|
+ if (bytesToRead > 0)
|
|
|
+ {
|
|
|
+ if (TrySocketRead(socket, data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0)
|
|
|
{
|
|
|
- if (TrySocketRead(socket, data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0)
|
|
|
- {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1888,84 +1864,6 @@ namespace Renci.SshNet
|
|
|
#endif
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Gets a value indicating whether the socket is connected.
|
|
|
- /// </summary>
|
|
|
- /// <returns>
|
|
|
- /// <see langword="true"/> if the socket is connected; otherwise, <see langword="false"/>.
|
|
|
- /// </returns>
|
|
|
- /// <remarks>
|
|
|
- /// <para>
|
|
|
- /// As a first check we verify whether <see cref="Socket.Connected"/> is
|
|
|
- /// <see langword="true"/>. However, this only returns the state of the socket as of
|
|
|
- /// the last I/O operation.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// Therefore we use the combination of <see cref="Socket.Poll(int, SelectMode)"/> with mode <see cref="SelectMode.SelectRead"/>
|
|
|
- /// and <see cref="Socket.Available"/> to verify if the socket is still connected.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// The MSDN doc mention the following on the return value of <see cref="Socket.Poll(int, SelectMode)"/>
|
|
|
- /// with mode <see cref="SelectMode.SelectRead"/>:
|
|
|
- /// <list type="bullet">
|
|
|
- /// <item>
|
|
|
- /// <description><see langword="true"/> if data is available for reading;</description>
|
|
|
- /// </item>
|
|
|
- /// <item>
|
|
|
- /// <description><see langword="true"/> if the connection has been closed, reset, or terminated; otherwise, returns <see langword="false"/>.</description>
|
|
|
- /// </item>
|
|
|
- /// </list>
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// <c>Conclusion:</c> when the return value is <see langword="true"/> - but no data is available for reading - then
|
|
|
- /// the socket is no longer connected.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// When a <see cref="Socket"/> is used from multiple threads, there's a race condition
|
|
|
- /// between the invocation of <see cref="Socket.Poll(int, SelectMode)"/> and the moment
|
|
|
- /// when the value of <see cref="Socket.Available"/> is obtained. To workaround this issue
|
|
|
- /// we synchronize reads from the <see cref="Socket"/>.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// We assume the socket is still connected if the read lock cannot be acquired immediately.
|
|
|
- /// In this case, we just return <see langword="true"/> without actually waiting to acquire
|
|
|
- /// the lock. We don't want to wait for the read lock if another thread already has it because
|
|
|
- /// there are cases where the other thread holding the lock can be waiting indefinitely for
|
|
|
- /// a socket read operation to complete.
|
|
|
- /// </para>
|
|
|
- /// </remarks>
|
|
|
- private bool IsSocketConnected()
|
|
|
- {
|
|
|
- _socketDisposeLock.Wait();
|
|
|
-
|
|
|
- try
|
|
|
- {
|
|
|
- if (!_socket.IsConnected())
|
|
|
- {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- if (!_socketReadLock.TryEnter())
|
|
|
- {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- try
|
|
|
- {
|
|
|
- var connectionClosedOrDataAvailable = _socket.Poll(0, SelectMode.SelectRead);
|
|
|
- return !(connectionClosedOrDataAvailable && _socket.Available == 0);
|
|
|
- }
|
|
|
- finally
|
|
|
- {
|
|
|
- _socketReadLock.Exit();
|
|
|
- }
|
|
|
- }
|
|
|
- finally
|
|
|
- {
|
|
|
- _ = _socketDisposeLock.Release();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
|
|
|
/// </summary>
|
|
|
@@ -1988,46 +1886,37 @@ namespace Renci.SshNet
|
|
|
/// </summary>
|
|
|
private void SocketDisconnectAndDispose()
|
|
|
{
|
|
|
- if (_socket != null)
|
|
|
+ lock (_socketDisposeLock)
|
|
|
{
|
|
|
- _socketDisposeLock.Wait();
|
|
|
+ if (_socket is null)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- try
|
|
|
+ if (_socket.Connected)
|
|
|
{
|
|
|
-#pragma warning disable CA1508 // Avoid dead conditional code; Value could have been changed by another thread.
|
|
|
- if (_socket != null)
|
|
|
-#pragma warning restore CA1508 // Avoid dead conditional code
|
|
|
+ try
|
|
|
{
|
|
|
- if (_socket.Connected)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex);
|
|
|
-
|
|
|
- // Interrupt any pending reads; should be done outside of socket read lock as we
|
|
|
- // actually want shutdown the socket to make sure blocking reads are interrupted.
|
|
|
- //
|
|
|
- // This may result in a SocketException (eg. An existing connection was forcibly
|
|
|
- // closed by the remote host) which we'll log and ignore as it means the socket
|
|
|
- // was already shut down.
|
|
|
- _socket.Shutdown(SocketShutdown.Send);
|
|
|
- }
|
|
|
- catch (SocketException ex)
|
|
|
- {
|
|
|
- _logger.LogInformation(ex, "Failure shutting down socket");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex);
|
|
|
- _socket.Dispose();
|
|
|
- _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex);
|
|
|
- _socket = null;
|
|
|
+ _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex);
|
|
|
+
|
|
|
+ // Interrupt any pending reads; should be done outside of socket read lock as we
|
|
|
+ // actually want shutdown the socket to make sure blocking reads are interrupted.
|
|
|
+ //
|
|
|
+ // This may result in a SocketException (eg. An existing connection was forcibly
|
|
|
+ // closed by the remote host) which we'll log and ignore as it means the socket
|
|
|
+ // was already shut down.
|
|
|
+ _socket.Shutdown(SocketShutdown.Both);
|
|
|
+ }
|
|
|
+ catch (SocketException ex)
|
|
|
+ {
|
|
|
+ _logger.LogInformation(ex, "Failure shutting down socket");
|
|
|
}
|
|
|
}
|
|
|
- finally
|
|
|
- {
|
|
|
- _ = _socketDisposeLock.Release();
|
|
|
- }
|
|
|
+
|
|
|
+ _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex);
|
|
|
+ _socket.Dispose();
|
|
|
+ _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex);
|
|
|
+ _socket = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2048,25 +1937,6 @@ namespace Renci.SshNet
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- try
|
|
|
- {
|
|
|
- // Block until either data is available or the socket is closed
|
|
|
- var connectionClosedOrDataAvailable = socket.Poll(-1, SelectMode.SelectRead);
|
|
|
- if (connectionClosedOrDataAvailable && socket.Available == 0)
|
|
|
- {
|
|
|
- // connection with SSH server was closed or connection was reset
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- catch (ObjectDisposedException)
|
|
|
- {
|
|
|
- // The socket was disposed by either:
|
|
|
- // * a call to Disconnect()
|
|
|
- // * a call to Dispose()
|
|
|
- // * a SSH_MSG_DISCONNECT received from server
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
var message = ReceiveMessage(socket);
|
|
|
if (message is null)
|
|
|
{
|
|
|
@@ -2102,25 +1972,12 @@ namespace Renci.SshNet
|
|
|
/// <param name="exp">The <see cref="Exception"/>.</param>
|
|
|
private void RaiseError(Exception exp)
|
|
|
{
|
|
|
- var connectionException = exp as SshConnectionException;
|
|
|
-
|
|
|
_logger.LogInformation(exp, "[{SessionId}] Raised exception", SessionIdHex);
|
|
|
|
|
|
- if (_isDisconnecting)
|
|
|
+ if (_isDisconnecting && exp is SshConnectionException or ObjectDisposedException)
|
|
|
{
|
|
|
- // 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
|
|
|
- if (exp is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
+ // Such an exception raised while isDisconnecting is expected and can be ignored.
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
// "save" exception and set exception wait handle to ensure any waits are interrupted
|
|
|
@@ -2129,10 +1986,10 @@ namespace Renci.SshNet
|
|
|
|
|
|
ErrorOccured?.Invoke(this, new ExceptionEventArgs(exp));
|
|
|
|
|
|
- if (connectionException != null)
|
|
|
+ if (exp is SshConnectionException connectionException)
|
|
|
{
|
|
|
_logger.LogInformation(exp, "[{SessionId}] Disconnecting after exception", SessionIdHex);
|
|
|
- Disconnect(connectionException.DisconnectReason, exp.ToString());
|
|
|
+ Disconnect(connectionException.DisconnectReason, exp.Message);
|
|
|
}
|
|
|
}
|
|
|
|