using System;
using System.Globalization;
using System.Linq;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using Renci.SshNet.Compression;
using Renci.SshNet.Connection;
using Renci.SshNet.Messages;
using Renci.SshNet.Messages.Authentication;
using Renci.SshNet.Messages.Connection;
using Renci.SshNet.Messages.Transport;
using Renci.SshNet.Security;
using Renci.SshNet.Security.Cryptography;
namespace Renci.SshNet
{
///
/// Provides functionality to connect and interact with SSH server.
///
public class Session : ISession
{
internal const byte CarriageReturn = 0x0d;
internal const byte LineFeed = 0x0a;
///
/// Specifies an infinite waiting period.
///
///
/// The value of this field is -1.
///
internal const int Infinite = -1;
///
/// Specifies maximum packet size defined by the protocol.
///
///
/// 68536 (64 KB + 3000 bytes).
///
internal const int MaximumSshPacketSize = LocalChannelDataPacketSize + 3000;
///
/// Holds the initial local window size for the channels.
///
///
/// 2147483647 (2^31 - 1) bytes.
///
///
/// We currently do not define a maximum (remote) window size.
///
private const int InitialLocalWindowSize = 0x7FFFFFFF;
///
/// Holds the maximum size of channel data packets that we receive.
///
///
/// 64 KB.
///
///
///
/// This is the maximum size (in bytes) we support for the data (payload) of a
/// SSH_MSG_CHANNEL_DATA message we receive.
///
///
/// We currently do not enforce this limit.
///
///
private const int LocalChannelDataPacketSize = 1024 * 64;
///
/// Specifies an infinite waiting period.
///
///
/// The value of this field is -1 millisecond.
///
internal static readonly TimeSpan InfiniteTimeSpan = new TimeSpan(0, 0, 0, 0, -1);
///
/// Controls how many authentication attempts can take place at the same time.
///
///
/// Some server may restrict number to prevent authentication attacks.
///
private static readonly SemaphoreLight AuthenticationConnection = new SemaphoreLight(3);
///
/// Holds the factory to use for creating new services.
///
private readonly IServiceFactory _serviceFactory;
private readonly ISocketFactory _socketFactory;
///
/// Holds an object that is used to ensure only a single thread can read from
/// at any given time.
///
private readonly object _socketReadLock = new object();
///
/// Holds an object that is used to ensure only a single thread can write to
/// at any given time.
///
///
/// This is also used to ensure that is
/// incremented atomatically.
///
private readonly object _socketWriteLock = new object();
///
/// Holds an object that is used to ensure only a single thread can dispose
/// at any given time.
///
///
/// This is also used to ensure that will not be disposed
/// while performing a given operation or set of operations on .
///
private readonly object _socketDisposeLock = new object();
///
/// Holds metadata about session messages.
///
private SshMessageFactory _sshMessageFactory;
///
/// Holds a that is signaled when the message listener loop has completed.
///
private ManualResetEvent _messageListenerCompleted;
///
/// Specifies outbound packet number.
///
private volatile uint _outboundPacketSequence;
///
/// Specifies incoming packet number.
///
private uint _inboundPacketSequence;
///
/// WaitHandle to signal that last service request was accepted.
///
private EventWaitHandle _serviceAccepted = new AutoResetEvent(initialState: false);
///
/// WaitHandle to signal that exception was thrown by another thread.
///
private EventWaitHandle _exceptionWaitHandle = new ManualResetEvent(initialState: false);
///
/// WaitHandle to signal that key exchange was completed.
///
private EventWaitHandle _keyExchangeCompletedWaitHandle = new ManualResetEvent(initialState: false);
///
/// WaitHandle to signal that key exchange is in progress.
///
private bool _keyExchangeInProgress;
///
/// Exception that need to be thrown by waiting thread.
///
private Exception _exception;
///
/// Specifies whether connection is authenticated.
///
private bool _isAuthenticated;
///
/// Specifies whether user issued Disconnect command or not.
///
private bool _isDisconnecting;
private IKeyExchange _keyExchange;
private HashAlgorithm _serverMac;
private HashAlgorithm _clientMac;
private Cipher _clientCipher;
private Cipher _serverCipher;
private Compressor _serverDecompression;
private Compressor _clientCompression;
private SemaphoreLight _sessionSemaphore;
///
/// Holds connection socket.
///
private Socket _socket;
///
/// Gets the session semaphore that controls session channels.
///
///
/// The session semaphore.
///
public SemaphoreLight SessionSemaphore
{
get
{
if (_sessionSemaphore is null)
{
lock (this)
{
_sessionSemaphore ??= new SemaphoreLight(ConnectionInfo.MaxSessions);
}
}
return _sessionSemaphore;
}
}
private bool _isDisconnectMessageSent;
private uint _nextChannelNumber;
///
/// Gets the next channel number.
///
///
/// The next channel number.
///
private uint NextChannelNumber
{
get
{
uint result;
lock (this)
{
result = _nextChannelNumber++;
}
return result;
}
}
///
/// Gets a value indicating whether the session is connected.
///
///
/// true if the session is connected; otherwise, false.
///
///
/// This methods returns true in all but the following cases:
///
/// -
/// The is disposed.
///
/// -
/// The SSH_MSG_DISCONNECT message - which is used to disconnect from the server - has been sent.
///
/// -
/// The client has not been authenticated successfully.
///
/// -
/// The listener thread - which is used to receive messages from the server - has stopped.
///
/// -
/// The socket used to communicate with the server is no longer connected.
///
///
///
public bool IsConnected
{
get
{
if (_disposed || _isDisconnectMessageSent || !_isAuthenticated)
{
return false;
}
if (_messageListenerCompleted is null || _messageListenerCompleted.WaitOne(0))
{
return false;
}
return IsSocketConnected();
}
}
///
/// Gets the session id.
///
///
/// The session id, or null if the client has not been authenticated.
///
public byte[] SessionId { get; private set; }
private Message _clientInitMessage;
///
/// Gets the client init message.
///
/// The client init message.
public Message ClientInitMessage
{
get
{
_clientInitMessage ??= new KeyExchangeInitMessage
{
KeyExchangeAlgorithms = ConnectionInfo.KeyExchangeAlgorithms.Keys.ToArray(),
ServerHostKeyAlgorithms = ConnectionInfo.HostKeyAlgorithms.Keys.ToArray(),
EncryptionAlgorithmsClientToServer = ConnectionInfo.Encryptions.Keys.ToArray(),
EncryptionAlgorithmsServerToClient = ConnectionInfo.Encryptions.Keys.ToArray(),
MacAlgorithmsClientToServer = ConnectionInfo.HmacAlgorithms.Keys.ToArray(),
MacAlgorithmsServerToClient = ConnectionInfo.HmacAlgorithms.Keys.ToArray(),
CompressionAlgorithmsClientToServer = ConnectionInfo.CompressionAlgorithms.Keys.ToArray(),
CompressionAlgorithmsServerToClient = ConnectionInfo.CompressionAlgorithms.Keys.ToArray(),
LanguagesClientToServer = new[] { string.Empty },
LanguagesServerToClient = new[] { string.Empty },
FirstKexPacketFollows = false,
Reserved = 0
};
return _clientInitMessage;
}
}
///
/// Gets the server version string.
///
///
/// The server version.
///
public string ServerVersion { get; private set; }
///
/// Gets the client version string.
///
///
/// The client version.
///
public string ClientVersion { get; private set; }
///
/// Gets the connection info.
///
///
/// The connection info.
///
public ConnectionInfo ConnectionInfo { get; private set; }
///
/// Occurs when an error occurred.
///
public event EventHandler ErrorOccured;
///
/// Occurs when session has been disconnected from the server.
///
public event EventHandler Disconnected;
///
/// Occurs when host key received.
///
public event EventHandler HostKeyReceived;
///
/// Occurs when message is received from the server.
///
public event EventHandler> UserAuthenticationBannerReceived;
///
/// Occurs when message is received from the server.
///
internal event EventHandler> UserAuthenticationInformationRequestReceived;
///
/// Occurs when message is received from the server.
///
internal event EventHandler> UserAuthenticationPasswordChangeRequiredReceived;
///
/// Occurs when message is received from the server.
///
internal event EventHandler> UserAuthenticationPublicKeyReceived;
///
/// Occurs when message is received from the server.
///
internal event EventHandler> KeyExchangeDhGroupExchangeGroupReceived;
///
/// Occurs when message is received from the server.
///
internal event EventHandler> KeyExchangeDhGroupExchangeReplyReceived;
///
/// Occurs when message received
///
internal event EventHandler> DisconnectReceived;
///
/// Occurs when message received
///
internal event EventHandler> IgnoreReceived;
///
/// Occurs when message received
///
internal event EventHandler> UnimplementedReceived;
///
/// Occurs when message received
///
internal event EventHandler> DebugReceived;
///
/// Occurs when message received
///
internal event EventHandler> ServiceRequestReceived;
///
/// Occurs when message received
///
internal event EventHandler> ServiceAcceptReceived;
///
/// Occurs when message received
///
internal event EventHandler> KeyExchangeInitReceived;
///
/// Occurs when a message is received from the SSH server.
///
internal event EventHandler> KeyExchangeDhReplyMessageReceived;
///
/// Occurs when a message is received from the SSH server.
///
internal event EventHandler> KeyExchangeEcdhReplyMessageReceived;
///
/// Occurs when message received
///
internal event EventHandler> NewKeysReceived;
///
/// Occurs when message received
///
internal event EventHandler> UserAuthenticationRequestReceived;
///
/// Occurs when message received
///
internal event EventHandler> UserAuthenticationFailureReceived;
///
/// Occurs when message received
///
internal event EventHandler> UserAuthenticationSuccessReceived;
///
/// Occurs when message received
///
internal event EventHandler> GlobalRequestReceived;
///
/// Occurs when message received
///
public event EventHandler> RequestSuccessReceived;
///
/// Occurs when message received
///
public event EventHandler> RequestFailureReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelOpenReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelOpenConfirmationReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelOpenFailureReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelWindowAdjustReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelDataReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelExtendedDataReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelEofReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelCloseReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelRequestReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelSuccessReceived;
///
/// Occurs when message received
///
public event EventHandler> ChannelFailureReceived;
///
/// Initializes a new instance of the class.
///
/// The connection info.
/// The factory to use for creating new services.
/// A factory to create instances.
/// is null.
/// is null.
/// is null.
internal Session(ConnectionInfo connectionInfo, IServiceFactory serviceFactory, ISocketFactory socketFactory)
{
if (connectionInfo is null)
{
throw new ArgumentNullException(nameof(connectionInfo));
}
if (serviceFactory is null)
{
throw new ArgumentNullException(nameof(serviceFactory));
}
if (socketFactory is null)
{
throw new ArgumentNullException(nameof(socketFactory));
}
ClientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1";
ConnectionInfo = connectionInfo;
_serviceFactory = serviceFactory;
_socketFactory = socketFactory;
_messageListenerCompleted = new ManualResetEvent(initialState: true);
}
///
/// Connects to the server.
///
/// Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.
/// SSH session could not be established.
/// Authentication of SSH session failed.
/// Failed to establish proxy connection.
public void Connect()
{
if (IsConnected)
{
return;
}
try
{
AuthenticationConnection.Wait();
if (IsConnected)
{
return;
}
lock (this)
{
// If connected don't connect again
if (IsConnected)
{
return;
}
// Reset connection specific information
Reset();
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.Connect(ConnectionInfo);
var serverIdentification = _serviceFactory.CreateProtocolVersionExchange()
.Start(ClientVersion, _socket, ConnectionInfo.Timeout);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", serverIdentification.ProtocolVersion, serverIdentification.SoftwareVersion));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
}
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
{
Disconnect();
return;
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
}
}
finally
{
_ = AuthenticationConnection.Release();
}
}
///
/// Asynchronously connects to the server.
///
///
/// Please note this function is NOT thread safe.
/// The caller SHOULD limit the number of simultaneous connection attempts to a server to a single connection attempt.
/// The to observe.
/// A that represents the asynchronous connect operation.
/// Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.
/// SSH session could not be established.
/// Authentication of SSH session failed.
/// Failed to establish proxy connection.
public async Task ConnectAsync(CancellationToken cancellationToken)
{
// If connected don't connect again
if (IsConnected)
{
return;
}
// Reset connection specific information
Reset();
// Build list of available messages while connecting
_sshMessageFactory = new SshMessageFactory();
_socket = await _serviceFactory.CreateConnector(ConnectionInfo, _socketFactory)
.ConnectAsync(ConnectionInfo, cancellationToken).ConfigureAwait(false);
var serverIdentification = await _serviceFactory.CreateProtocolVersionExchange()
.StartAsync(ClientVersion, _socket, cancellationToken).ConfigureAwait(false);
// Set connection versions
ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
ConnectionInfo.ClientVersion = ClientVersion;
DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", serverIdentification.ProtocolVersion, serverIdentification.SoftwareVersion));
if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion),
DisconnectReason.ProtocolVersionNotSupported);
}
// Register Transport response messages
RegisterMessage("SSH_MSG_DISCONNECT");
RegisterMessage("SSH_MSG_IGNORE");
RegisterMessage("SSH_MSG_UNIMPLEMENTED");
RegisterMessage("SSH_MSG_DEBUG");
RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
RegisterMessage("SSH_MSG_KEXINIT");
RegisterMessage("SSH_MSG_NEWKEYS");
// Some server implementations might sent this message first, prior to establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// Mark the message listener threads as started
_ = _messageListenerCompleted.Reset();
// Start incoming request listener
// ToDo: Make message pump async, to not consume a thread for every session
ThreadAbstraction.ExecuteThreadLongRunning(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle);
// If sessionId is not set then its not connected
if (SessionId is null)
{
Disconnect();
return;
}
// Request user authorization service
SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));
// Wait for service to be accepted
WaitOnHandle(_serviceAccepted);
if (string.IsNullOrEmpty(ConnectionInfo.Username))
{
throw new SshException("Username is not specified.");
}
// Some servers send a global request immediately after successful authentication
// Avoid race condition by already enabling SSH_MSG_GLOBAL_REQUEST before authentication
RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
ConnectionInfo.Authenticate(this, _serviceFactory);
_isAuthenticated = true;
// Register Connection messages
RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
RegisterMessage("SSH_MSG_REQUEST_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
RegisterMessage("SSH_MSG_CHANNEL_DATA");
RegisterMessage("SSH_MSG_CHANNEL_EOF");
RegisterMessage("SSH_MSG_CHANNEL_CLOSE");
}
///
/// Disconnects from the server.
///
///
/// This sends a SSH_MSG_DISCONNECT message to the server, waits for the
/// server to close the socket on its end and subsequently closes the client socket.
///
public void Disconnect()
{
DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting session.", ToHex(SessionId)));
// send SSH_MSG_DISCONNECT message, clear socket read buffer and dispose it
Disconnect(DisconnectReason.ByApplication, "Connection terminated by the client.");
// at this point, we are sure that the listener thread will stop as we've
// disconnected the socket, so lets wait until the message listener thread
// has completed
if (_messageListenerCompleted != null)
{
_ = _messageListenerCompleted.WaitOne();
}
}
private void Disconnect(DisconnectReason reason, string message)
{
// transition to disconnecting state to avoid throwing exceptions while cleaning up, and to
// ensure any exceptions that are raised do not overwrite the exception that is set
_isDisconnecting = true;
// 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 loop to be interrupted as
// the server should respond by closing the socket
if (IsConnected)
{
TrySendDisconnect(reason, message);
}
// disconnect socket, and dispose it
SocketDisconnectAndDispose();
}
///
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the connection timeout.
///
/// The wait handle.
/// A received package was invalid or failed the message integrity check.
/// None of the handles are signaled in time and the session is not disconnecting.
/// A socket error was signaled while receiving messages from the server.
///
/// When neither handles are signaled in time and the session is not closing, then the
/// session is disconnected.
///
void ISession.WaitOnHandle(WaitHandle waitHandle)
{
WaitOnHandle(waitHandle, ConnectionInfo.Timeout);
}
///
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the specified timeout.
///
/// The wait handle.
/// The time to wait for any of the handles to become signaled.
/// A received package was invalid or failed the message integrity check.
/// None of the handles are signaled in time and the session is not disconnecting.
/// A socket error was signaled while receiving messages from the server.
///
/// When neither handles are signaled in time and the session is not closing, then the
/// session is disconnected.
///
void ISession.WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout)
{
WaitOnHandle(waitHandle, timeout);
}
///
/// Waits for the specified to receive a signal, using a
/// to specify the time interval.
///
/// The that should be signaled.
/// A that represents the number of milliseconds to wait, or a that represents -1 milliseconds to wait indefinitely.
///
/// A .
///
WaitResult ISession.TryWait(WaitHandle waitHandle, TimeSpan timeout)
{
return TryWait(waitHandle, timeout, out _);
}
///
/// Waits for the specified to receive a signal, using a
/// to specify the time interval.
///
/// The that should be signaled.
/// A that represents the number of milliseconds to wait, or a that represents -1 milliseconds to wait indefinitely.
/// When this method returns , contains the .
///
/// A .
///
WaitResult ISession.TryWait(WaitHandle waitHandle, TimeSpan timeout, out Exception exception)
{
return TryWait(waitHandle, timeout, out exception);
}
///
/// Waits for the specified to receive a signal, using a
/// to specify the time interval.
///
/// The that should be signaled.
/// A that represents the number of milliseconds to wait, or a that represents -1 milliseconds to wait indefinitely.
/// When this method returns , contains the .
///
/// A .
///
private WaitResult TryWait(WaitHandle waitHandle, TimeSpan timeout, out Exception exception)
{
if (waitHandle is null)
{
throw new ArgumentNullException(nameof(waitHandle));
}
var waitHandles = new[]
{
_exceptionWaitHandle,
_messageListenerCompleted,
waitHandle
};
switch (WaitHandle.WaitAny(waitHandles, timeout))
{
case 0:
if (_exception is SshConnectionException)
{
exception = null;
return WaitResult.Disconnected;
}
exception = _exception;
return WaitResult.Failed;
case 1:
exception = null;
return WaitResult.Disconnected;
case 2:
exception = null;
return WaitResult.Success;
case WaitHandle.WaitTimeout:
exception = null;
return WaitResult.TimedOut;
default:
throw new InvalidOperationException("Unexpected result.");
}
}
///
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the connection timeout.
///
/// The wait handle.
/// A received package was invalid or failed the message integrity check.
/// None of the handles are signaled in time and the session is not disconnecting.
/// A socket error was signaled while receiving messages from the server.
///
/// When neither handles are signaled in time and the session is not closing, then the
/// session is disconnected.
///
internal void WaitOnHandle(WaitHandle waitHandle)
{
WaitOnHandle(waitHandle, ConnectionInfo.Timeout);
}
///
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the specified timeout.
///
/// The wait handle.
/// The time to wait for any of the handles to become signaled.
/// A received package was invalid or failed the message integrity check.
/// None of the handles are signaled in time and the session is not disconnecting.
/// A socket error was signaled while receiving messages from the server.
internal void WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout)
{
if (waitHandle is null)
{
throw new ArgumentNullException(nameof(waitHandle));
}
var waitHandles = new[]
{
_exceptionWaitHandle,
_messageListenerCompleted,
waitHandle
};
var signaledElement = WaitHandle.WaitAny(waitHandles, timeout);
switch (signaledElement)
{
case 0:
throw _exception;
case 1:
throw new SshConnectionException("Client not connected.");
case 2:
// Specified waithandle was signaled
break;
case WaitHandle.WaitTimeout:
// 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
// report an exception to the client when we're disconnecting anyway
if (!_isDisconnecting)
{
throw new SshOperationTimeoutException("Session operation has timed out");
}
break;
default:
throw new SshException($"Unexpected element '{signaledElement.ToString(CultureInfo.InvariantCulture)}' signaled.");
}
}
///
/// Sends a message to the server.
///
/// The message to send.
/// The client is not connected.
/// The operation timed out.
/// The size of the packet exceeds the maximum size defined by the protocol.
internal void SendMessage(Message message)
{
if (!_socket.CanWrite())
{
throw new SshConnectionException("Client not connected.");
}
if (_keyExchangeInProgress && message is not IKeyExchangedAllowed)
{
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle);
}
DiagnosticAbstraction.Log(string.Format("[{0}] Sending message '{1}' to server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
var paddingMultiplier = _clientCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
var packetData = message.GetPacket(paddingMultiplier, _clientCompression);
// take a write lock to ensure the outbound packet sequence number is incremented
// atomically, and only after the packet has actually been sent
lock (_socketWriteLock)
{
byte[] hash = null;
var packetDataOffset = 4; // first four bytes are reserved for outbound packet sequence
if (_clientMac != null)
{
// write outbound packet sequence to start of packet data
Pack.UInt32ToBigEndian(_outboundPacketSequence, packetData);
// calculate packet hash
hash = _clientMac.ComputeHash(packetData);
}
// Encrypt packet data
if (_clientCipher != null)
{
packetData = _clientCipher.Encrypt(packetData, packetDataOffset, packetData.Length - packetDataOffset);
packetDataOffset = 0;
}
if (packetData.Length > MaximumSshPacketSize)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Packet is too big. Maximum packet size is {0} bytes.", MaximumSshPacketSize));
}
var packetLength = packetData.Length - packetDataOffset;
if (hash is null)
{
SendPacket(packetData, packetDataOffset, packetLength);
}
else
{
var data = new byte[packetLength + hash.Length];
Buffer.BlockCopy(packetData, packetDataOffset, data, 0, packetLength);
Buffer.BlockCopy(hash, 0, data, packetLength, hash.Length);
SendPacket(data, 0, data.Length);
}
// increment the packet sequence number only after we're sure the packet has
// been sent; even though it's only used for the MAC, it needs to be incremented
// for each package sent.
//
// the server will use it to verify the data integrity, and as such the order in
// which messages are sent must follow the outbound packet sequence number
_outboundPacketSequence++;
}
}
///
/// Sends an SSH packet to the server.
///
/// A byte array containing the packet to send.
/// The offset of the packet.
/// The length of the packet.
/// Client is not connected to the server.
///
///
/// The send is performed in a dispose lock to avoid
/// and/or when sending the packet.
///
///
/// This method is only to be used when the connection is established, as the locking
/// overhead is not required while establising the connection.
///
///
private void SendPacket(byte[] packet, int offset, int length)
{
lock (_socketDisposeLock)
{
if (!_socket.IsConnected())
{
throw new SshConnectionException("Client not connected.");
}
SocketAbstraction.Send(_socket, packet, offset, length);
}
}
///
/// Sends a message to the server.
///
/// The message to send.
///
/// true if the message was sent to the server; otherwise, false.
///
/// The size of the packet exceeds the maximum size defined by the protocol.
///
/// This methods returns false when the attempt to send the message results in a
/// or a .
///
private bool TrySendMessage(Message message)
{
try
{
SendMessage(message);
return true;
}
catch (SshException ex)
{
DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex));
return false;
}
catch (SocketException ex)
{
DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex));
return false;
}
}
///
/// Receives the message from the server.
///
///
/// The incoming SSH message, or null if the connection with the SSH server was closed.
///
///
/// We need no locking here since all messages are read by a single thread.
///
private Message ReceiveMessage(Socket socket)
{
// the length of the packet sequence field in bytes
const int inboundPacketSequenceLength = 4;
// The length of the "packet length" field in bytes
const int packetLengthFieldLength = 4;
// The length of the "padding length" field in bytes
const int paddingLengthFieldLength = 1;
// Determine the size of the first block, which is 8 or cipher block size (whichever is larger) bytes
var blockSize = _serverCipher is null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
var serverMacLength = _serverMac != null ? _serverMac.HashSize/8 : 0;
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)
{
// connection with SSH server was closed
return null;
}
if (_serverCipher != null)
{
firstBlock = _serverCipher.Decrypt(firstBlock);
}
packetLength = Pack.BigEndianToUInt32(firstBlock);
// Test packet minimum and maximum boundaries
if (packetLength < Math.Max((byte) 16, 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];
Pack.UInt32ToBigEndian(_inboundPacketSequence, data);
Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, firstBlock.Length);
if (bytesToRead > 0)
{
if (TrySocketRead(socket, data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0)
{
return null;
}
}
}
if (_serverCipher != null)
{
var numberOfBytesToDecrypt = data.Length - (blockSize + inboundPacketSequenceLength + serverMacLength);
if (numberOfBytesToDecrypt > 0)
{
var decryptedData = _serverCipher.Decrypt(data, blockSize + inboundPacketSequenceLength, numberOfBytesToDecrypt);
Buffer.BlockCopy(decryptedData, 0, data, blockSize + inboundPacketSequenceLength, decryptedData.Length);
}
}
var paddingLength = data[inboundPacketSequenceLength + packetLengthFieldLength];
var messagePayloadLength = (int) packetLength - paddingLength - paddingLengthFieldLength;
var messagePayloadOffset = inboundPacketSequenceLength + packetLengthFieldLength + paddingLengthFieldLength;
// validate message against MAC
if (_serverMac != null)
{
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
var serverHash = data.Take(data.Length - serverMacLength, serverMacLength);
// TODO add IsEqualTo overload that takes left+right index and number of bytes to compare;
// TODO that way we can eliminate the extra allocation of the Take above
if (!serverHash.IsEqualTo(clientHash))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}
}
if (_serverDecompression != null)
{
data = _serverDecompression.Decompress(data, messagePayloadOffset, messagePayloadLength);
// data now only contains the decompressed payload, and as such the offset is reset to zero
messagePayloadOffset = 0;
// the length of the payload is now the complete decompressed content
messagePayloadLength = data.Length;
}
_inboundPacketSequence++;
return LoadMessage(data, messagePayloadOffset, messagePayloadLength);
}
private void TrySendDisconnect(DisconnectReason reasonCode, string message)
{
var disconnectMessage = new DisconnectMessage(reasonCode, message);
// send the disconnect message, but ignore the outcome
_ = TrySendMessage(disconnectMessage);
// mark disconnect message sent regardless of whether the send sctually succeeded
_isDisconnectMessageSent = true;
}
///
/// Called when received.
///
/// message.
internal void OnDisconnectReceived(DisconnectMessage message)
{
DiagnosticAbstraction.Log(string.Format("[{0}] Disconnect received: {1} {2}.", ToHex(SessionId), message.ReasonCode, message.Description));
// transition to disconnecting state to avoid throwing exceptions while cleaning up, and to
// ensure any exceptions that are raised do not overwrite the SshConnectionException that we
// set below
_isDisconnecting = true;
_exception = new SshConnectionException(string.Format(CultureInfo.InvariantCulture, "The connection was closed by the server: {0} ({1}).", message.Description, message.ReasonCode), message.ReasonCode);
_ = _exceptionWaitHandle.Set();
DisconnectReceived?.Invoke(this, new MessageEventArgs(message));
Disconnected?.Invoke(this, EventArgs.Empty);
// disconnect socket, and dispose it
SocketDisconnectAndDispose();
}
///
/// Called when received.
///
/// message.
internal void OnIgnoreReceived(IgnoreMessage message)
{
IgnoreReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnUnimplementedReceived(UnimplementedMessage message)
{
UnimplementedReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnDebugReceived(DebugMessage message)
{
DebugReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnServiceRequestReceived(ServiceRequestMessage message)
{
ServiceRequestReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnServiceAcceptReceived(ServiceAcceptMessage message)
{
ServiceAcceptReceived?.Invoke(this, new MessageEventArgs(message));
_ = _serviceAccepted.Set();
}
internal void OnKeyExchangeDhGroupExchangeGroupReceived(KeyExchangeDhGroupExchangeGroup message)
{
KeyExchangeDhGroupExchangeGroupReceived?.Invoke(this, new MessageEventArgs(message));
}
internal void OnKeyExchangeDhGroupExchangeReplyReceived(KeyExchangeDhGroupExchangeReply message)
{
KeyExchangeDhGroupExchangeReplyReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnKeyExchangeInitReceived(KeyExchangeInitMessage message)
{
_keyExchangeInProgress = true;
_ = _keyExchangeCompletedWaitHandle.Reset();
// Disable messages that are not key exchange related
_sshMessageFactory.DisableNonKeyExchangeMessages();
_keyExchange = _serviceFactory.CreateKeyExchange(ConnectionInfo.KeyExchangeAlgorithms,
message.KeyExchangeAlgorithms);
ConnectionInfo.CurrentKeyExchangeAlgorithm = _keyExchange.Name;
_keyExchange.HostKeyReceived += KeyExchange_HostKeyReceived;
// Start the algorithm implementation
_keyExchange.Start(this, message);
KeyExchangeInitReceived?.Invoke(this, new MessageEventArgs(message));
}
internal void OnKeyExchangeDhReplyMessageReceived(KeyExchangeDhReplyMessage message)
{
KeyExchangeDhReplyMessageReceived?.Invoke(this, new MessageEventArgs(message));
}
internal void OnKeyExchangeEcdhReplyMessageReceived(KeyExchangeEcdhReplyMessage message)
{
KeyExchangeEcdhReplyMessageReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnNewKeysReceived(NewKeysMessage message)
{
// Update sessionId
SessionId ??= _keyExchange.ExchangeHash;
// Dispose of old ciphers and hash algorithms
if (_serverMac != null)
{
_serverMac.Dispose();
_serverMac = null;
}
if (_clientMac != null)
{
_clientMac.Dispose();
_clientMac = null;
}
// Update negotiated algorithms
_serverCipher = _keyExchange.CreateServerCipher();
_clientCipher = _keyExchange.CreateClientCipher();
_serverMac = _keyExchange.CreateServerHash();
_clientMac = _keyExchange.CreateClientHash();
_clientCompression = _keyExchange.CreateCompressor();
_serverDecompression = _keyExchange.CreateDecompressor();
// Dispose of old KeyExchange object as it is no longer needed.
if (_keyExchange != null)
{
_keyExchange.HostKeyReceived -= KeyExchange_HostKeyReceived;
_keyExchange.Dispose();
_keyExchange = null;
}
// Enable activated messages that are not key exchange related
_sshMessageFactory.EnableActivatedMessages();
NewKeysReceived?.Invoke(this, new MessageEventArgs(message));
// Signal that key exchange completed
_ = _keyExchangeCompletedWaitHandle.Set();
_keyExchangeInProgress = false;
}
///
/// Called when client is disconnecting from the server.
///
void ISession.OnDisconnecting()
{
_isDisconnecting = true;
}
///
/// Called when message received.
///
/// message.
internal void OnUserAuthenticationRequestReceived(RequestMessage message)
{
UserAuthenticationRequestReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnUserAuthenticationFailureReceived(FailureMessage message)
{
UserAuthenticationFailureReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnUserAuthenticationSuccessReceived(SuccessMessage message)
{
UserAuthenticationSuccessReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnUserAuthenticationBannerReceived(BannerMessage message)
{
UserAuthenticationBannerReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnUserAuthenticationInformationRequestReceived(InformationRequestMessage message)
{
UserAuthenticationInformationRequestReceived?.Invoke(this, new MessageEventArgs(message));
}
internal void OnUserAuthenticationPasswordChangeRequiredReceived(PasswordChangeRequiredMessage message)
{
UserAuthenticationPasswordChangeRequiredReceived?.Invoke(this, new MessageEventArgs(message));
}
internal void OnUserAuthenticationPublicKeyReceived(PublicKeyMessage message)
{
UserAuthenticationPublicKeyReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnGlobalRequestReceived(GlobalRequestMessage message)
{
GlobalRequestReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnRequestSuccessReceived(RequestSuccessMessage message)
{
RequestSuccessReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnRequestFailureReceived(RequestFailureMessage message)
{
RequestFailureReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelOpenReceived(ChannelOpenMessage message)
{
ChannelOpenReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelOpenConfirmationReceived(ChannelOpenConfirmationMessage message)
{
ChannelOpenConfirmationReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelOpenFailureReceived(ChannelOpenFailureMessage message)
{
ChannelOpenFailureReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelWindowAdjustReceived(ChannelWindowAdjustMessage message)
{
ChannelWindowAdjustReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelDataReceived(ChannelDataMessage message)
{
ChannelDataReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelExtendedDataReceived(ChannelExtendedDataMessage message)
{
ChannelExtendedDataReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelEofReceived(ChannelEofMessage message)
{
ChannelEofReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelCloseReceived(ChannelCloseMessage message)
{
ChannelCloseReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelRequestReceived(ChannelRequestMessage message)
{
ChannelRequestReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelSuccessReceived(ChannelSuccessMessage message)
{
ChannelSuccessReceived?.Invoke(this, new MessageEventArgs(message));
}
///
/// Called when message received.
///
/// message.
internal void OnChannelFailureReceived(ChannelFailureMessage message)
{
ChannelFailureReceived?.Invoke(this, new MessageEventArgs(message));
}
private void KeyExchange_HostKeyReceived(object sender, HostKeyEventArgs e)
{
HostKeyReceived?.Invoke(this, e);
}
///
/// Registers SSH message with the session.
///
/// The name of the message to register with the session.
public void RegisterMessage(string messageName)
{
_sshMessageFactory.EnableAndActivateMessage(messageName);
}
///
/// Unregister SSH message from the session.
///
/// The name of the message to unregister with the session.
public void UnRegisterMessage(string messageName)
{
_sshMessageFactory.DisableAndDeactivateMessage(messageName);
}
///
/// Loads a message from a given buffer.
///
/// An array of bytes from which to construct the message.
/// The zero-based byte offset in at which to begin reading.
/// The number of bytes to load.
///
/// A message constructed from .
///
/// The type of the message is not supported.
private Message LoadMessage(byte[] data, int offset, int count)
{
var messageType = data[offset];
var message = _sshMessageFactory.Create(messageType);
message.Load(data, offset + 1, count - 1);
DiagnosticAbstraction.Log(string.Format("[{0}] Received message '{1}' from server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
return message;
}
private static string ToHex(byte[] bytes, int offset)
{
var byteCount = bytes.Length - offset;
var builder = new StringBuilder(bytes.Length * 2);
for (var i = offset; i < byteCount; i++)
{
var b = bytes[i];
_ = builder.Append(b.ToString("X2"));
}
return builder.ToString();
}
internal static string ToHex(byte[] bytes)
{
if (bytes is null)
{
return null;
}
return ToHex(bytes, 0);
}
///
/// Gets a value indicating whether the socket is connected.
///
///
/// true if the socket is connected; otherwise, false.
///
///
///
/// As a first check we verify whether is
/// true. However, this only returns the state of the socket as of
/// the last I/O operation.
///
///
/// Therefore we use the combination of with mode
/// and to verify if the socket is still connected.
///
///
/// The MSDN doc mention the following on the return value of
/// with mode :
///
/// -
/// true if data is available for reading;
///
/// -
/// true if the connection has been closed, reset, or terminated; otherwise, returns false.
///
///
///
///
/// Conclusion: when the return value is true - but no data is available for reading - then
/// the socket is no longer connected.
///
///
/// When a is used from multiple threads, there's a race condition
/// between the invocation of and the moment
/// when the value of is obtained. To workaround this issue
/// we synchronize reads from the .
///
///
private bool IsSocketConnected()
{
lock (_socketDisposeLock)
{
if (!_socket.IsConnected())
{
return false;
}
lock (_socketReadLock)
{
var connectionClosedOrDataAvailable = _socket.Poll(0, SelectMode.SelectRead);
return !(connectionClosedOrDataAvailable && _socket.Available == 0);
}
}
}
///
/// Performs a blocking read on the socket until bytes are received.
///
/// The to read from.
/// An array of type that is the storage location for the received data.
/// The position in parameter to store the received data.
/// The number of bytes to read.
///
/// The number of bytes read.
///
/// The read has timed-out.
/// The read failed.
private static int TrySocketRead(Socket socket, byte[] buffer, int offset, int length)
{
return SocketAbstraction.Read(socket, buffer, offset, length, InfiniteTimeSpan);
}
///
/// Shuts down and disposes the socket.
///
private void SocketDisconnectAndDispose()
{
if (_socket != null)
{
lock (_socketDisposeLock)
{
if (_socket != null)
{
if (_socket.Connected)
{
try
{
DiagnosticAbstraction.Log(string.Format("[{0}] Shutting down socket.", ToHex(SessionId)));
// 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)
{
// TODO: log as warning
DiagnosticAbstraction.Log("Failure shutting down socket: " + ex);
}
}
DiagnosticAbstraction.Log(string.Format("[{0}] Disposing socket.", ToHex(SessionId)));
_socket.Dispose();
DiagnosticAbstraction.Log(string.Format("[{0}] Disposed socket.", ToHex(SessionId)));
_socket = null;
}
}
}
}
///
/// Listens for incoming message from the server and handles them. This method run as a task on separate thread.
///
private void MessageListener()
{
try
{
// remain in message loop until socket is shut down or until we're disconnecting
while (true)
{
var socket = _socket;
if (socket is null || !socket.Connected)
{
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)
{
// Connection with SSH server was closed, so break out of the message loop
break;
}
// process message
message.Process(this);
}
// connection with SSH server was closed or socket was disposed
RaiseError(CreateConnectionAbortedByServerException());
}
catch (SocketException ex)
{
RaiseError(new SshConnectionException(ex.Message, DisconnectReason.ConnectionLost, ex));
}
catch (Exception exp)
{
RaiseError(exp);
}
finally
{
// signal that the message listener thread has stopped
_ = _messageListenerCompleted.Set();
}
}
///
/// Raises the event.
///
/// The .
private void RaiseError(Exception exp)
{
var connectionException = exp as SshConnectionException;
DiagnosticAbstraction.Log(string.Format("[{0}] Raised exception: {1}", ToHex(SessionId), exp));
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
if (exp is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut)
{
return;
}
}
// "save" exception and set exception wait handle to ensure any waits are interrupted
_exception = exp;
_ = _exceptionWaitHandle.Set();
ErrorOccured?.Invoke(this, new ExceptionEventArgs(exp));
if (connectionException != null)
{
DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting after exception: {1}", ToHex(SessionId), exp));
Disconnect(connectionException.DisconnectReason, exp.ToString());
}
}
///
/// Resets connection-specific information to ensure state of a previous connection
/// does not affect new connections.
///
private void Reset()
{
_ = _exceptionWaitHandle?.Reset();
_ = _keyExchangeCompletedWaitHandle?.Reset();
_ = _messageListenerCompleted?.Set();
SessionId = null;
_isDisconnectMessageSent = false;
_isDisconnecting = false;
_isAuthenticated = false;
_exception = null;
_keyExchangeInProgress = false;
}
private static SshConnectionException CreateConnectionAbortedByServerException()
{
return new SshConnectionException("An established connection was aborted by the server.",
DisconnectReason.ConnectionLost);
}
private bool _disposed;
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
DiagnosticAbstraction.Log(string.Format("[{0}] Disposing session.", ToHex(SessionId)));
Disconnect();
var serviceAccepted = _serviceAccepted;
if (serviceAccepted != null)
{
serviceAccepted.Dispose();
_serviceAccepted = null;
}
var exceptionWaitHandle = _exceptionWaitHandle;
if (exceptionWaitHandle != null)
{
exceptionWaitHandle.Dispose();
_exceptionWaitHandle = null;
}
var keyExchangeCompletedWaitHandle = _keyExchangeCompletedWaitHandle;
if (keyExchangeCompletedWaitHandle != null)
{
keyExchangeCompletedWaitHandle.Dispose();
_keyExchangeCompletedWaitHandle = null;
}
var serverMac = _serverMac;
if (serverMac != null)
{
serverMac.Dispose();
_serverMac = null;
}
var clientMac = _clientMac;
if (clientMac != null)
{
clientMac.Dispose();
_clientMac = null;
}
var keyExchange = _keyExchange;
if (keyExchange != null)
{
keyExchange.HostKeyReceived -= KeyExchange_HostKeyReceived;
keyExchange.Dispose();
_keyExchange = null;
}
var messageListenerCompleted = _messageListenerCompleted;
if (messageListenerCompleted != null)
{
messageListenerCompleted.Dispose();
_messageListenerCompleted = null;
}
_disposed = true;
}
}
///
/// Finalizes an instance of the class.
///
~Session()
{
Dispose(disposing: false);
}
///
/// Gets the connection info.
///
/// The connection info.
IConnectionInfo ISession.ConnectionInfo
{
get { return ConnectionInfo; }
}
WaitHandle ISession.MessageListenerCompleted
{
get { return _messageListenerCompleted; }
}
///
/// Create a new SSH session channel.
///
///
/// A new SSH session channel.
///
IChannelSession ISession.CreateChannelSession()
{
return new ChannelSession(this, NextChannelNumber, InitialLocalWindowSize, LocalChannelDataPacketSize);
}
///
/// Create a new channel for a locally forwarded TCP/IP port.
///
///
/// A new channel for a locally forwarded TCP/IP port.
///
IChannelDirectTcpip ISession.CreateChannelDirectTcpip()
{
return new ChannelDirectTcpip(this, NextChannelNumber, InitialLocalWindowSize, LocalChannelDataPacketSize);
}
///
/// Creates a "forwarded-tcpip" SSH channel.
///
///
/// A new "forwarded-tcpip" SSH channel.
///
IChannelForwardedTcpip ISession.CreateChannelForwardedTcpip(uint remoteChannelNumber,
uint remoteWindowSize,
uint remoteChannelDataPacketSize)
{
return new ChannelForwardedTcpip(this,
NextChannelNumber,
InitialLocalWindowSize,
LocalChannelDataPacketSize,
remoteChannelNumber,
remoteWindowSize,
remoteChannelDataPacketSize);
}
///
/// Sends a message to the server.
///
/// The message to send.
/// The client is not connected.
/// The operation timed out.
/// The size of the packet exceeds the maximum size defined by the protocol.
void ISession.SendMessage(Message message)
{
SendMessage(message);
}
///
/// Sends a message to the server.
///
/// The message to send.
///
/// true if the message was sent to the server; otherwise, false.
///
/// The size of the packet exceeds the maximum size defined by the protocol.
///
/// This methods returns false when the attempt to send the message results in a
/// or a .
///
bool ISession.TrySendMessage(Message message)
{
return TrySendMessage(message);
}
}
///
/// Represents the result of a wait operations.
///
internal enum WaitResult
{
///
/// The was signaled within the specified interval.
///
Success = 1,
///
/// The was not signaled within the specified interval.
///
TimedOut = 2,
///
/// The session is in a disconnected state.
///
Disconnected = 3,
///
/// The session is in a failed state.
///
Failed = 4
}
}