You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2443 lines
101 KiB
C#

9 months ago
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using Renci.SshNet.Compression;
using Renci.SshNet.Messages;
using Renci.SshNet.Messages.Authentication;
using Renci.SshNet.Messages.Connection;
using Renci.SshNet.Messages.Transport;
using Renci.SshNet.Security;
using System.Globalization;
using System.Linq;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Security.Cryptography;
namespace Renci.SshNet
{
/// <summary>
/// Provides functionality to connect and interact with SSH server.
/// </summary>
public class Session : ISession
{
private const byte Null = 0x00;
private const byte CarriageReturn = 0x0d;
private const byte LineFeed = 0x0a;
/// <summary>
/// Specifies an infinite waiting period.
/// </summary>
/// <remarks>
/// The value of this field is <c>-1</c> millisecond.
/// </remarks>
internal static readonly TimeSpan InfiniteTimeSpan = new TimeSpan(0, 0, 0, 0, -1);
/// <summary>
/// Specifies an infinite waiting period.
/// </summary>
/// <remarks>
/// The value of this field is <c>-1</c>.
/// </remarks>
internal static readonly int Infinite = -1;
/// <summary>
/// Specifies maximum packet size defined by the protocol.
/// </summary>
private const int MaximumSshPacketSize = LocalChannelDataPacketSize + 3000;
/// <summary>
/// Holds the initial local window size for the channels.
/// </summary>
/// <value>
/// 2 MB.
/// </value>
private const int InitialLocalWindowSize = LocalChannelDataPacketSize * 32;
/// <summary>
/// Holds the maximum size of channel data packets that we receive.
/// </summary>
/// <value>
/// 64 KB.
/// </value>
private const int LocalChannelDataPacketSize = 1024*64;
#if FEATURE_REGEX_COMPILE
private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$", RegexOptions.Compiled);
#else
private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$");
#endif
/// <summary>
/// Controls how many authentication attempts can take place at the same time.
/// </summary>
/// <remarks>
/// Some server may restrict number to prevent authentication attacks
/// </remarks>
private static readonly SemaphoreLight AuthenticationConnection = new SemaphoreLight(3);
/// <summary>
/// Holds metada about session messages
/// </summary>
private SshMessageFactory _sshMessageFactory;
/// <summary>
/// Holds a <see cref="WaitHandle"/> that is signaled when the message listener loop has completed.
/// </summary>
private EventWaitHandle _messageListenerCompleted;
/// <summary>
/// Specifies outbound packet number
/// </summary>
private volatile uint _outboundPacketSequence;
/// <summary>
/// Specifies incoming packet number
/// </summary>
private uint _inboundPacketSequence;
/// <summary>
/// WaitHandle to signal that last service request was accepted
/// </summary>
private EventWaitHandle _serviceAccepted = new AutoResetEvent(false);
/// <summary>
/// WaitHandle to signal that exception was thrown by another thread.
/// </summary>
private EventWaitHandle _exceptionWaitHandle = new ManualResetEvent(false);
/// <summary>
/// WaitHandle to signal that key exchange was completed.
/// </summary>
private EventWaitHandle _keyExchangeCompletedWaitHandle = new ManualResetEvent(false);
/// <summary>
/// WaitHandle to signal that key exchange is in progress.
/// </summary>
private bool _keyExchangeInProgress;
/// <summary>
/// Exception that need to be thrown by waiting thread
/// </summary>
private Exception _exception;
/// <summary>
/// Specifies whether connection is authenticated
/// </summary>
private bool _isAuthenticated;
/// <summary>
/// Specifies whether user issued Disconnect command or not
/// </summary>
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;
/// <summary>
/// Holds the factory to use for creating new services.
/// </summary>
private readonly IServiceFactory _serviceFactory;
/// <summary>
/// Holds connection socket.
/// </summary>
private Socket _socket;
#if FEATURE_SOCKET_POLL
/// <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 object _socketReadLock = new object();
#endif // FEATURE_SOCKET_POLL
/// <summary>
/// Holds an object that is used to ensure only a single thread can write to
/// <see cref="_socket"/> at any given time.
/// </summary>
/// <remarks>
/// This is also used to ensure that <see cref="_outboundPacketSequence"/> is
/// incremented atomatically.
/// </remarks>
private readonly object _socketWriteLock = new object();
/// <summary>
/// Holds an object that is used to ensure only a single thread can dispose
/// <see cref="_socket"/> at any given time.
/// </summary>
/// <remarks>
/// 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 object _socketDisposeLock = new object();
/// <summary>
/// Gets the session semaphore that controls session channels.
/// </summary>
/// <value>
/// The session semaphore.
/// </value>
public SemaphoreLight SessionSemaphore
{
get
{
if (_sessionSemaphore == null)
{
lock (this)
{
if (_sessionSemaphore == null)
{
_sessionSemaphore = new SemaphoreLight(ConnectionInfo.MaxSessions);
}
}
}
return _sessionSemaphore;
}
}
private bool _isDisconnectMessageSent;
private uint _nextChannelNumber;
/// <summary>
/// Gets the next channel number.
/// </summary>
/// <value>
/// The next channel number.
/// </value>
private uint NextChannelNumber
{
get
{
uint result;
lock (this)
{
result = _nextChannelNumber++;
}
return result;
}
}
/// <summary>
/// Gets a value indicating whether the session is connected.
/// </summary>
/// <value>
/// <c>true</c> if the session is connected; otherwise, <c>false</c>.
/// </value>
/// <remarks>
/// This methods returns <c>true</c> in all but the following cases:
/// <list type="bullet">
/// <item>
/// <description>The <see cref="Session"/> is disposed.</description>
/// </item>
/// <item>
/// <description>The SSH_MSG_DISCONNECT message - which is used to disconnect from the server - has been sent.</description>
/// </item>
/// <item>
/// <description>The client 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 (_disposed || _isDisconnectMessageSent || !_isAuthenticated)
return false;
if (_messageListenerCompleted == null || _messageListenerCompleted.WaitOne(0))
return false;
return IsSocketConnected();
}
}
/// <summary>
/// Gets the session id.
/// </summary>
/// <value>
/// The session id, or <c>null</c> if the client has not been authenticated.
/// </value>
public byte[] SessionId { get; private set; }
private Message _clientInitMessage;
/// <summary>
/// Gets the client init message.
/// </summary>
/// <value>The client init message.</value>
public Message ClientInitMessage
{
get
{
if (_clientInitMessage == null)
{
_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;
}
}
/// <summary>
/// Gets or sets the server version string.
/// </summary>
/// <value>The server version.</value>
public string ServerVersion { get; private set; }
/// <summary>
/// Gets or sets the client version string.
/// </summary>
/// <value>The client version.</value>
public string ClientVersion { get; private set; }
/// <summary>
/// Gets or sets the connection info.
/// </summary>
/// <value>The connection info.</value>
public ConnectionInfo ConnectionInfo { get; private set; }
/// <summary>
/// Occurs when an error occurred.
/// </summary>
public event EventHandler<ExceptionEventArgs> ErrorOccured;
/// <summary>
/// Occurs when session has been disconnected from the server.
/// </summary>
public event EventHandler<EventArgs> Disconnected;
/// <summary>
/// Occurs when host key received.
/// </summary>
public event EventHandler<HostKeyEventArgs> HostKeyReceived;
/// <summary>
/// Occurs when <see cref="BannerMessage"/> message is received from the server.
/// </summary>
public event EventHandler<MessageEventArgs<BannerMessage>> UserAuthenticationBannerReceived;
/// <summary>
/// Occurs when <see cref="InformationRequestMessage"/> message is received from the server.
/// </summary>
internal event EventHandler<MessageEventArgs<InformationRequestMessage>> UserAuthenticationInformationRequestReceived;
/// <summary>
/// Occurs when <see cref="PasswordChangeRequiredMessage"/> message is received from the server.
/// </summary>
internal event EventHandler<MessageEventArgs<PasswordChangeRequiredMessage>> UserAuthenticationPasswordChangeRequiredReceived;
/// <summary>
/// Occurs when <see cref="PublicKeyMessage"/> message is received from the server.
/// </summary>
internal event EventHandler<MessageEventArgs<PublicKeyMessage>> UserAuthenticationPublicKeyReceived;
/// <summary>
/// Occurs when <see cref="KeyExchangeDhGroupExchangeGroup"/> message is received from the server.
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeDhGroupExchangeGroup>> KeyExchangeDhGroupExchangeGroupReceived;
/// <summary>
/// Occurs when <see cref="KeyExchangeDhGroupExchangeReply"/> message is received from the server.
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeDhGroupExchangeReply>> KeyExchangeDhGroupExchangeReplyReceived;
#region Message events
/// <summary>
/// Occurs when <see cref="DisconnectMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<DisconnectMessage>> DisconnectReceived;
/// <summary>
/// Occurs when <see cref="IgnoreMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<IgnoreMessage>> IgnoreReceived;
/// <summary>
/// Occurs when <see cref="UnimplementedMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<UnimplementedMessage>> UnimplementedReceived;
/// <summary>
/// Occurs when <see cref="DebugMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<DebugMessage>> DebugReceived;
/// <summary>
/// Occurs when <see cref="ServiceRequestMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<ServiceRequestMessage>> ServiceRequestReceived;
/// <summary>
/// Occurs when <see cref="ServiceAcceptMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<ServiceAcceptMessage>> ServiceAcceptReceived;
/// <summary>
/// Occurs when <see cref="KeyExchangeInitMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeInitMessage>> KeyExchangeInitReceived;
/// <summary>
/// Occurs when a <see cref="KeyExchangeDhReplyMessage"/> message is received from the SSH server.
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeDhReplyMessage>> KeyExchangeDhReplyMessageReceived;
/// <summary>
/// Occurs when <see cref="NewKeysMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<NewKeysMessage>> NewKeysReceived;
/// <summary>
/// Occurs when <see cref="RequestMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<RequestMessage>> UserAuthenticationRequestReceived;
/// <summary>
/// Occurs when <see cref="FailureMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<FailureMessage>> UserAuthenticationFailureReceived;
/// <summary>
/// Occurs when <see cref="SuccessMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<SuccessMessage>> UserAuthenticationSuccessReceived;
/// <summary>
/// Occurs when <see cref="GlobalRequestMessage"/> message received
/// </summary>
internal event EventHandler<MessageEventArgs<GlobalRequestMessage>> GlobalRequestReceived;
/// <summary>
/// Occurs when <see cref="RequestSuccessMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<RequestSuccessMessage>> RequestSuccessReceived;
/// <summary>
/// Occurs when <see cref="RequestFailureMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<RequestFailureMessage>> RequestFailureReceived;
/// <summary>
/// Occurs when <see cref="ChannelOpenMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelOpenMessage>> ChannelOpenReceived;
/// <summary>
/// Occurs when <see cref="ChannelOpenConfirmationMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelOpenConfirmationMessage>> ChannelOpenConfirmationReceived;
/// <summary>
/// Occurs when <see cref="ChannelOpenFailureMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelOpenFailureMessage>> ChannelOpenFailureReceived;
/// <summary>
/// Occurs when <see cref="ChannelWindowAdjustMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelWindowAdjustMessage>> ChannelWindowAdjustReceived;
/// <summary>
/// Occurs when <see cref="ChannelDataMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelDataMessage>> ChannelDataReceived;
/// <summary>
/// Occurs when <see cref="ChannelExtendedDataMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelExtendedDataMessage>> ChannelExtendedDataReceived;
/// <summary>
/// Occurs when <see cref="ChannelEofMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelEofMessage>> ChannelEofReceived;
/// <summary>
/// Occurs when <see cref="ChannelCloseMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelCloseMessage>> ChannelCloseReceived;
/// <summary>
/// Occurs when <see cref="ChannelRequestMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelRequestMessage>> ChannelRequestReceived;
/// <summary>
/// Occurs when <see cref="ChannelSuccessMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelSuccessMessage>> ChannelSuccessReceived;
/// <summary>
/// Occurs when <see cref="ChannelFailureMessage"/> message received
/// </summary>
public event EventHandler<MessageEventArgs<ChannelFailureMessage>> ChannelFailureReceived;
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="Session"/> class.
/// </summary>
/// <param name="connectionInfo">The connection info.</param>
/// <param name="serviceFactory">The factory to use for creating new services.</param>
/// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <c>null</c>.</exception>
internal Session(ConnectionInfo connectionInfo, IServiceFactory serviceFactory)
{
if (connectionInfo == null)
throw new ArgumentNullException("connectionInfo");
if (serviceFactory == null)
throw new ArgumentNullException("serviceFactory");
ClientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1";
ConnectionInfo = connectionInfo;
_serviceFactory = serviceFactory;
_messageListenerCompleted = new ManualResetEvent(true);
}
/// <summary>
/// Connects to the server.
/// </summary>
/// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
/// <exception cref="SshConnectionException">SSH session could not be established.</exception>
/// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
/// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
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();
switch (ConnectionInfo.ProxyType)
{
case ProxyTypes.None:
SocketConnect(ConnectionInfo.Host, ConnectionInfo.Port);
break;
case ProxyTypes.Socks4:
SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
ConnectSocks4();
break;
case ProxyTypes.Socks5:
SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
ConnectSocks5();
break;
case ProxyTypes.Http:
SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
ConnectHttp();
break;
}
Match versionMatch;
// Get server version from the server,
// ignore text lines which are sent before if any
while (true)
{
var serverVersion = SocketReadLine(ConnectionInfo.Timeout);
if (serverVersion == null)
throw new SshConnectionException("Server response does not contain SSH protocol identification.", DisconnectReason.ProtocolError);
versionMatch = ServerVersionRe.Match(serverVersion);
if (versionMatch.Success)
{
ServerVersion = serverVersion;
break;
}
}
// Set connection versions
ConnectionInfo.ServerVersion = ServerVersion;
ConnectionInfo.ClientVersion = ClientVersion;
// Get server SSH version
var version = versionMatch.Result("${protoversion}");
var softwareName = versionMatch.Result("${softwareversion}");
DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", version, softwareName));
if (!(version.Equals("2.0") || version.Equals("1.99")))
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", version), DisconnectReason.ProtocolVersionNotSupported);
}
SocketAbstraction.Send(_socket, Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\x0D\x0A", ClientVersion)));
// 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 establishing encryption algorithm
RegisterMessage("SSH_MSG_USERAUTH_BANNER");
// mark the message listener threads as started
_messageListenerCompleted.Reset();
// Start incoming request listener
ThreadAbstraction.ExecuteThread(MessageListener);
// Wait for key exchange to be completed
WaitOnHandle(_keyExchangeCompletedWaitHandle);
// If sessionId is not set then its not connected
if (SessionId == 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();
}
}
/// <summary>
/// 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()
{
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();
}
/// <summary>
/// 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>
void ISession.WaitOnHandle(WaitHandle waitHandle)
{
WaitOnHandle(waitHandle, ConnectionInfo.Timeout);
}
/// <summary>
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the specified timeout.
/// </summary>
/// <param name="waitHandle">The wait handle.</param>
/// <param name="timeout">The time to wait for any of the handles to become signaled.</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>
void ISession.WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout)
{
WaitOnHandle(waitHandle, timeout);
}
/// <summary>
/// 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)
{
WaitOnHandle(waitHandle, ConnectionInfo.Timeout);
}
/// <summary>
/// Waits for the specified handle or the exception handle for the receive thread
/// to signal within the specified timeout.
/// </summary>
/// <param name="waitHandle">The wait handle.</param>
/// <param name="timeout">The time to wait for any of the handles to become signaled.</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>
internal void WaitOnHandle(WaitHandle waitHandle, TimeSpan timeout)
{
if (waitHandle == null)
throw new ArgumentNullException("waitHandle");
var waitHandles = new[]
{
_exceptionWaitHandle,
_messageListenerCompleted,
waitHandle
};
switch (WaitHandle.WaitAny(waitHandles, timeout))
{
case 0:
throw _exception;
case 1:
throw new SshConnectionException("Client not connected.");
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;
}
}
/// <summary>
/// Sends a message to the server.
/// </summary>
/// <param name="message">The message to send.</param>
/// <exception cref="SshConnectionException">The client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">The operation timed out.</exception>
/// <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())
throw new SshConnectionException("Client not connected.");
if (_keyExchangeInProgress && !(message is 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 == 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 == 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++;
}
}
/// <summary>
/// Sends an SSH packet to the server.
/// </summary>
/// <param name="packet">A byte array containing the packet to send.</param>
/// <param name="offset">The offset of the packet.</param>
/// <param name="length">The length of the packet.</param>
/// <exception cref="SshConnectionException">Client is not connected to the server.</exception>
/// <remarks>
/// <para>
/// The send is performed in a dispose lock to avoid <see cref="NullReferenceException"/>
/// and/or <see cref="ObjectDisposedException"/> when sending the packet.
/// </para>
/// <para>
/// This method is only to be used when the connection is established, as the locking
/// overhead is not required while establising the connection.
/// </para>
/// </remarks>
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);
}
}
/// <summary>
/// Sends a message to the server.
/// </summary>
/// <param name="message">The message to send.</param>
/// <returns>
/// <c>true</c> if the message was sent to the server; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
/// <remarks>
/// This methods returns <c>false</c> when the attempt to send the message results in a
/// <see cref="SocketException"/> or a <see cref="SshException"/>.
/// </remarks>
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;
}
}
/// <summary>
/// Receives the message from the server.
/// </summary>
/// <returns>
/// The incoming SSH message, or <c>null</c> if the connection with the SSH server was closed.
/// </returns>
/// <remarks>
/// We need no locking here since all messages are read by a single thread.
/// </remarks>
private Message ReceiveMessage()
{
// 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 == null ? (byte) 8 : Math.Max((byte) 8, _serverCipher.MinimumSize);
var serverMacLength = _serverMac != null ? _serverMac.HashSize/8 : 0;
byte[] data;
uint packetLength;
#if FEATURE_SOCKET_POLL
// 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)
{
#endif // FEATURE_SOCKET_POLL
// Read first block - which starts with the packet length
var firstBlock = new byte[blockSize];
if (TrySocketRead(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(data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0)
{
return null;
}
}
#if FEATURE_SOCKET_POLL
}
#endif // FEATURE_SOCKET_POLL
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 extrate 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;
}
#region Handle received message events
/// <summary>
/// Called when <see cref="DisconnectMessage"/> received.
/// </summary>
/// <param name="message"><see cref="DisconnectMessage"/> message.</param>
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();
var disconnectReceived = DisconnectReceived;
if (disconnectReceived != null)
disconnectReceived(this, new MessageEventArgs<DisconnectMessage>(message));
var disconnected = Disconnected;
if (disconnected != null)
disconnected(this, new EventArgs());
// disconnect socket, and dispose it
SocketDisconnectAndDispose();
}
/// <summary>
/// Called when <see cref="IgnoreMessage"/> received.
/// </summary>
/// <param name="message"><see cref="IgnoreMessage"/> message.</param>
internal void OnIgnoreReceived(IgnoreMessage message)
{
var handlers = IgnoreReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<IgnoreMessage>(message));
}
/// <summary>
/// Called when <see cref="UnimplementedMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="UnimplementedMessage"/> message.</param>
internal void OnUnimplementedReceived(UnimplementedMessage message)
{
var handlers = UnimplementedReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<UnimplementedMessage>(message));
}
/// <summary>
/// Called when <see cref="DebugMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="DebugMessage"/> message.</param>
internal void OnDebugReceived(DebugMessage message)
{
var handlers = DebugReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<DebugMessage>(message));
}
/// <summary>
/// Called when <see cref="ServiceRequestMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ServiceRequestMessage"/> message.</param>
internal void OnServiceRequestReceived(ServiceRequestMessage message)
{
var handlers = ServiceRequestReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ServiceRequestMessage>(message));
}
/// <summary>
/// Called when <see cref="ServiceAcceptMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ServiceAcceptMessage"/> message.</param>
internal void OnServiceAcceptReceived(ServiceAcceptMessage message)
{
var handlers = ServiceAcceptReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ServiceAcceptMessage>(message));
_serviceAccepted.Set();
}
internal void OnKeyExchangeDhGroupExchangeGroupReceived(KeyExchangeDhGroupExchangeGroup message)
{
var handlers = KeyExchangeDhGroupExchangeGroupReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<KeyExchangeDhGroupExchangeGroup>(message));
}
internal void OnKeyExchangeDhGroupExchangeReplyReceived(KeyExchangeDhGroupExchangeReply message)
{
var handlers = KeyExchangeDhGroupExchangeReplyReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<KeyExchangeDhGroupExchangeReply>(message));
}
/// <summary>
/// Called when <see cref="KeyExchangeInitMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="KeyExchangeInitMessage"/> message.</param>
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);
var keyExchangeInitReceived = KeyExchangeInitReceived;
if (keyExchangeInitReceived != null)
keyExchangeInitReceived(this, new MessageEventArgs<KeyExchangeInitMessage>(message));
}
internal void OnKeyExchangeDhReplyMessageReceived(KeyExchangeDhReplyMessage message)
{
var handlers = KeyExchangeDhReplyMessageReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<KeyExchangeDhReplyMessage>(message));
}
/// <summary>
/// Called when <see cref="NewKeysMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="NewKeysMessage"/> message.</param>
internal void OnNewKeysReceived(NewKeysMessage message)
{
// Update sessionId
if (SessionId == null)
{
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();
var newKeysReceived = NewKeysReceived;
if (newKeysReceived != null)
newKeysReceived(this, new MessageEventArgs<NewKeysMessage>(message));
// Signal that key exchange completed
_keyExchangeCompletedWaitHandle.Set();
_keyExchangeInProgress = false;
}
/// <summary>
/// Called when client is disconnecting from the server.
/// </summary>
void ISession.OnDisconnecting()
{
_isDisconnecting = true;
}
/// <summary>
/// Called when <see cref="RequestMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="RequestMessage"/> message.</param>
internal void OnUserAuthenticationRequestReceived(RequestMessage message)
{
var handlers = UserAuthenticationRequestReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<RequestMessage>(message));
}
/// <summary>
/// Called when <see cref="FailureMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="FailureMessage"/> message.</param>
internal void OnUserAuthenticationFailureReceived(FailureMessage message)
{
var handlers = UserAuthenticationFailureReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<FailureMessage>(message));
}
/// <summary>
/// Called when <see cref="SuccessMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="SuccessMessage"/> message.</param>
internal void OnUserAuthenticationSuccessReceived(SuccessMessage message)
{
var handlers = UserAuthenticationSuccessReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<SuccessMessage>(message));
}
/// <summary>
/// Called when <see cref="BannerMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="BannerMessage"/> message.</param>
internal void OnUserAuthenticationBannerReceived(BannerMessage message)
{
var handlers = UserAuthenticationBannerReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<BannerMessage>(message));
}
/// <summary>
/// Called when <see cref="InformationRequestMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="InformationRequestMessage"/> message.</param>
internal void OnUserAuthenticationInformationRequestReceived(InformationRequestMessage message)
{
var handlers = UserAuthenticationInformationRequestReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<InformationRequestMessage>(message));
}
internal void OnUserAuthenticationPasswordChangeRequiredReceived(PasswordChangeRequiredMessage message)
{
var handlers = UserAuthenticationPasswordChangeRequiredReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<PasswordChangeRequiredMessage>(message));
}
internal void OnUserAuthenticationPublicKeyReceived(PublicKeyMessage message)
{
var handlers = UserAuthenticationPublicKeyReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<PublicKeyMessage>(message));
}
/// <summary>
/// Called when <see cref="GlobalRequestMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="GlobalRequestMessage"/> message.</param>
internal void OnGlobalRequestReceived(GlobalRequestMessage message)
{
var handlers = GlobalRequestReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<GlobalRequestMessage>(message));
}
/// <summary>
/// Called when <see cref="RequestSuccessMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="RequestSuccessMessage"/> message.</param>
internal void OnRequestSuccessReceived(RequestSuccessMessage message)
{
var handlers = RequestSuccessReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<RequestSuccessMessage>(message));
}
/// <summary>
/// Called when <see cref="RequestFailureMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="RequestFailureMessage"/> message.</param>
internal void OnRequestFailureReceived(RequestFailureMessage message)
{
var handlers = RequestFailureReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<RequestFailureMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelOpenMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelOpenMessage"/> message.</param>
internal void OnChannelOpenReceived(ChannelOpenMessage message)
{
var handlers = ChannelOpenReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelOpenMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelOpenConfirmationMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelOpenConfirmationMessage"/> message.</param>
internal void OnChannelOpenConfirmationReceived(ChannelOpenConfirmationMessage message)
{
var handlers = ChannelOpenConfirmationReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelOpenConfirmationMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelOpenFailureMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelOpenFailureMessage"/> message.</param>
internal void OnChannelOpenFailureReceived(ChannelOpenFailureMessage message)
{
var handlers = ChannelOpenFailureReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelOpenFailureMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelWindowAdjustMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelWindowAdjustMessage"/> message.</param>
internal void OnChannelWindowAdjustReceived(ChannelWindowAdjustMessage message)
{
var handlers = ChannelWindowAdjustReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelWindowAdjustMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelDataMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelDataMessage"/> message.</param>
internal void OnChannelDataReceived(ChannelDataMessage message)
{
var handlers = ChannelDataReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelDataMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelExtendedDataMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelExtendedDataMessage"/> message.</param>
internal void OnChannelExtendedDataReceived(ChannelExtendedDataMessage message)
{
var handlers = ChannelExtendedDataReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelExtendedDataMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelCloseMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelCloseMessage"/> message.</param>
internal void OnChannelEofReceived(ChannelEofMessage message)
{
var handlers = ChannelEofReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelEofMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelCloseMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelCloseMessage"/> message.</param>
internal void OnChannelCloseReceived(ChannelCloseMessage message)
{
var handlers = ChannelCloseReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelCloseMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelRequestMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelRequestMessage"/> message.</param>
internal void OnChannelRequestReceived(ChannelRequestMessage message)
{
var handlers = ChannelRequestReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelRequestMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelSuccessMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelSuccessMessage"/> message.</param>
internal void OnChannelSuccessReceived(ChannelSuccessMessage message)
{
var handlers = ChannelSuccessReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelSuccessMessage>(message));
}
/// <summary>
/// Called when <see cref="ChannelFailureMessage"/> message received.
/// </summary>
/// <param name="message"><see cref="ChannelFailureMessage"/> message.</param>
internal void OnChannelFailureReceived(ChannelFailureMessage message)
{
var handlers = ChannelFailureReceived;
if (handlers != null)
handlers(this, new MessageEventArgs<ChannelFailureMessage>(message));
}
#endregion
private void KeyExchange_HostKeyReceived(object sender, HostKeyEventArgs e)
{
var handlers = HostKeyReceived;
if (handlers != null)
handlers(this, e);
}
#region Message loading functions
/// <summary>
/// Registers SSH message with the session.
/// </summary>
/// <param name="messageName">The name of the message to register with the session.</param>
public void RegisterMessage(string messageName)
{
_sshMessageFactory.EnableAndActivateMessage(messageName);
}
/// <summary>
/// Unregister SSH message from the session.
/// </summary>
/// <param name="messageName">The name of the message to unregister with the session.</param>
public void UnRegisterMessage(string messageName)
{
_sshMessageFactory.DisableAndDeactivateMessage(messageName);
}
/// <summary>
/// Loads a message from a given buffer.
/// </summary>
/// <param name="data">An array of bytes from which to construct the message.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="data"/> at which to begin reading.</param>
/// <param name="count">The number of bytes to load.</param>
/// <returns>
/// A message constructed from <paramref name="data"/>.
/// </returns>
/// <exception cref="SshException">The type of the message is not supported.</exception>
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 == null)
return null;
return ToHex(bytes, 0);
}
#endregion
/// <summary>
/// Establishes a socket connection to the specified host and port.
/// </summary>
/// <param name="host">The host name of the server to connect to.</param>
/// <param name="port">The port to connect to.</param>
/// <exception cref="SshOperationTimeoutException">The connection failed to establish within the configured <see cref="Renci.SshNet.ConnectionInfo.Timeout"/>.</exception>
/// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
private void SocketConnect(string host, int port)
{
var ipAddress = DnsAbstraction.GetHostAddresses(host)[0];
var ep = new IPEndPoint(ipAddress, port);
DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}:{1}'.", host, port));
_socket = SocketAbstraction.Connect(ep, ConnectionInfo.Timeout);
const int socketBufferSize = 2 * MaximumSshPacketSize;
_socket.SendBufferSize = socketBufferSize;
_socket.ReceiveBufferSize = socketBufferSize;
}
/// <summary>
/// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
/// </summary>
/// <param name="buffer">An array of type <see cref="byte"/> that is the storage location for the received data.</param>
/// <param name="offset">The position in <paramref name="buffer"/> parameter to store the received data.</param>
/// <param name="length">The number of bytes to read.</param>
/// <returns>
/// The number of bytes read.
/// </returns>
/// <exception cref="SshConnectionException">The socket is closed.</exception>
/// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
/// <exception cref="SocketException">The read failed.</exception>
private int SocketRead(byte[] buffer, int offset, int length)
{
var bytesRead = SocketAbstraction.Read(_socket, buffer, offset, length, InfiniteTimeSpan);
if (bytesRead == 0)
{
// when we're in the disconnecting state (either triggered by client or server), then the
// SshConnectionException will interrupt the message listener loop (if not already interrupted)
// and the exception itself will be ignored (in RaiseError)
throw new SshConnectionException("An established connection was aborted by the server.",
DisconnectReason.ConnectionLost);
}
return bytesRead;
}
#if FEATURE_SOCKET_POLL
/// <summary>
/// Gets a value indicating whether the socket is connected.
/// </summary>
/// <returns>
/// <c>true</c> if the socket is connected; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// <para>
/// As a first check we verify whether <see cref="Socket.Connected"/> is
/// <c>true</c>. 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><c>true</c> if data is available for reading;</description>
/// </item>
/// <item>
/// <description><c>true</c> if the connection has been closed, reset, or terminated; otherwise, returns <c>false</c>.</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// <c>Conclusion:</c> when the return value is <c>true</c> - 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>
/// </remarks>
#else
/// <summary>
/// Gets a value indicating whether the socket is connected.
/// </summary>
/// <returns>
/// <c>true</c> if the socket is connected; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// We verify whether <see cref="Socket.Connected"/> is <c>true</c>. However, this only returns the state
/// of the socket as of the last I/O operation.
/// </remarks>
#endif
private bool IsSocketConnected()
{
lock (_socketDisposeLock)
{
#if FEATURE_SOCKET_POLL
if (!_socket.IsConnected())
{
return false;
}
lock (_socketReadLock)
{
var connectionClosedOrDataAvailable = _socket.Poll(0, SelectMode.SelectRead);
return !(connectionClosedOrDataAvailable && _socket.Available == 0);
}
#else
return _socket.IsConnected();
#endif // FEATURE_SOCKET_POLL
}
}
/// <summary>
/// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
/// </summary>
/// <param name="buffer">An array of type <see cref="byte"/> that is the storage location for the received data.</param>
/// <param name="offset">The position in <paramref name="buffer"/> parameter to store the received data.</param>
/// <param name="length">The number of bytes to read.</param>
/// <returns>
/// The number of bytes read.
/// </returns>
/// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
/// <exception cref="SocketException">The read failed.</exception>
private int TrySocketRead(byte[] buffer, int offset, int length)
{
return SocketAbstraction.Read(_socket, buffer, offset, length, InfiniteTimeSpan);
}
/// <summary>
/// Performs a blocking read on the socket until a line is read.
/// </summary>
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param>
/// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
/// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
/// <returns>
/// The line read from the socket, or <c>null</c> when the remote server has shutdown and all data has been received.
/// </returns>
private string SocketReadLine(TimeSpan timeout)
{
var encoding = SshData.Ascii;
var buffer = new List<byte>();
var data = new byte[1];
// read data one byte at a time to find end of line and leave any unhandled information in the buffer
// to be processed by subsequent invocations
do
{
var bytesRead = SocketAbstraction.Read(_socket, data, 0, data.Length, timeout);
if (bytesRead == 0)
// the remote server shut down the socket
break;
buffer.Add(data[0]);
}
while (!(buffer.Count > 0 && (buffer[buffer.Count - 1] == LineFeed || buffer[buffer.Count - 1] == Null)));
if (buffer.Count == 0)
return null;
if (buffer.Count == 1 && buffer[buffer.Count - 1] == 0x00)
// return an empty version string if the buffer consists of only a 0x00 character
return string.Empty;
if (buffer.Count > 1 && buffer[buffer.Count - 2] == CarriageReturn)
// strip trailing CRLF
return encoding.GetString(buffer.ToArray(), 0, buffer.Count - 2);
if (buffer.Count > 1 && buffer[buffer.Count - 1] == LineFeed)
// strip trailing LF
return encoding.GetString(buffer.ToArray(), 0, buffer.Count - 1);
return encoding.GetString(buffer.ToArray(), 0, buffer.Count);
}
/// <summary>
/// Shuts down and disposes the socket.
/// </summary>
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;
}
}
}
}
/// <summary>
/// Listens for incoming message from the server and handles them. This method run as a task on separate thread.
/// </summary>
private void MessageListener()
{
#if FEATURE_SOCKET_SELECT
var readSockets = new List<Socket> { _socket };
#endif // FEATURE_SOCKET_SELECT
try
{
// remain in message loop until socket is shut down or until we're disconnecting
while (_socket.IsConnected())
{
#if FEATURE_SOCKET_SELECT
// if the socket is already disposed when Select is invoked, then a SocketException
// stating "An operation was attempted on something that is not a socket" is thrown;
// we attempt to avoid this exception by having an IsConnected() that can break the
// message loop
//
// note that there's no guarantee that the socket will not be disposed between the
// IsConnected() check and the Select invocation; we can't take a "dispose" lock
// that includes the Select invocation as we want Dispose() to be able to interrupt
// the Select
// perform a blocking select to determine whether there's is data available to be
// read; we do not use a blocking read to allow us to use Socket.Poll to determine
// if the connection is still available (in IsSocketConnected)
Socket.Select(readSockets, null, null, -1);
// the Select invocation will be interrupted in one of the following conditions:
// * data is available to be read
// => the socket will not be removed from "readSockets"
// * the socket connection is closed during the Select invocation
// => the socket will be removed from "readSockets"
// * the socket is disposed during the Select invocation
// => the socket will not be removed from "readSocket"
//
// since we handle the second and third condition the same way and Socket.Connected
// allows us to check for both conditions, we use that instead of both checking for
// the removal from "readSockets" and the Connection check
if (!_socket.IsConnected())
{
// connection with SSH server was closed or socket was disposed;
// break out of the message loop
break;
}
#elif FEATURE_SOCKET_POLL
// when Socket.Select(IList,IList,IList,Int32) is not available or is buggy, we use
// Socket.Poll(Int, SelectMode) to block until either data is available or the socket
// is closed
_socket.Poll(-1, SelectMode.SelectRead);
if (!_socket.IsConnected())
{
// connection with SSH server was closed or socket was disposed;
// break out of the message loop
break;
}
#endif // FEATURE_SOCKET_SELECT
var message = ReceiveMessage();
if (message == null)
{
// connection with SSH server was closed;
// 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();
}
}
private byte SocketReadByte()
{
var buffer = new byte[1];
SocketRead(buffer, 0, 1);
return buffer[0];
}
private void SocketWriteByte(byte data)
{
SocketAbstraction.Send(_socket, new[] {data});
}
private void ConnectSocks4()
{
// Send socks version number
SocketWriteByte(0x04);
// Send command code
SocketWriteByte(0x01);
// Send port
SocketWriteByte((byte)(ConnectionInfo.Port / 0xFF));
SocketWriteByte((byte)(ConnectionInfo.Port % 0xFF));
// Send IP
var ipAddress = DnsAbstraction.GetHostAddresses(ConnectionInfo.Host)[0];
SocketAbstraction.Send(_socket, ipAddress.GetAddressBytes());
// Send username
var username = SshData.Ascii.GetBytes(ConnectionInfo.ProxyUsername);
SocketAbstraction.Send(_socket, username);
SocketWriteByte(0x00);
// Read 0
if (SocketReadByte() != 0)
{
throw new ProxyException("SOCKS4: Null is expected.");
}
// Read response code
var code = SocketReadByte();
switch (code)
{
case 0x5a:
break;
case 0x5b:
throw new ProxyException("SOCKS4: Connection rejected.");
case 0x5c:
throw new ProxyException("SOCKS4: Client is not running identd or not reachable from the server.");
case 0x5d:
throw new ProxyException("SOCKS4: Client's identd could not confirm the user ID string in the request.");
default:
throw new ProxyException("SOCKS4: Not valid response.");
}
var dummyBuffer = new byte[6]; // field 3 (2 bytes) and field 4 (4) should be ignored
SocketRead(dummyBuffer, 0, 6);
}
private void ConnectSocks5()
{
// Send socks version number
SocketWriteByte(0x05);
// Send number of supported authentication methods
SocketWriteByte(0x02);
// Send supported authentication methods
SocketWriteByte(0x00); // No authentication
SocketWriteByte(0x02); // Username/Password
var socksVersion = SocketReadByte();
if (socksVersion != 0x05)
throw new ProxyException(string.Format("SOCKS Version '{0}' is not supported.", socksVersion));
var authenticationMethod = SocketReadByte();
switch (authenticationMethod)
{
case 0x00:
break;
case 0x02:
// Send version
SocketWriteByte(0x01);
var username = SshData.Ascii.GetBytes(ConnectionInfo.ProxyUsername);
if (username.Length > byte.MaxValue)
throw new ProxyException("Proxy username is too long.");
// Send username length
SocketWriteByte((byte)username.Length);
// Send username
SocketAbstraction.Send(_socket, username);
var password = SshData.Ascii.GetBytes(ConnectionInfo.ProxyPassword);
if (password.Length > byte.MaxValue)
throw new ProxyException("Proxy password is too long.");
// Send username length
SocketWriteByte((byte)password.Length);
// Send username
SocketAbstraction.Send(_socket, password);
var serverVersion = SocketReadByte();
if (serverVersion != 1)
throw new ProxyException("SOCKS5: Server authentication version is not valid.");
var statusCode = SocketReadByte();
if (statusCode != 0)
throw new ProxyException("SOCKS5: Username/Password authentication failed.");
break;
case 0xFF:
throw new ProxyException("SOCKS5: No acceptable authentication methods were offered.");
}
// Send socks version number
SocketWriteByte(0x05);
// Send command code
SocketWriteByte(0x01); // establish a TCP/IP stream connection
// Send reserved, must be 0x00
SocketWriteByte(0x00);
var ip = DnsAbstraction.GetHostAddresses(ConnectionInfo.Host)[0];
// Send address type and address
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
SocketWriteByte(0x01);
var address = ip.GetAddressBytes();
SocketAbstraction.Send(_socket, address);
}
else if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
SocketWriteByte(0x04);
var address = ip.GetAddressBytes();
SocketAbstraction.Send(_socket, address);
}
else
{
throw new ProxyException(string.Format("SOCKS5: IP address '{0}' is not supported.", ip));
}
// Send port
SocketWriteByte((byte)(ConnectionInfo.Port / 0xFF));
SocketWriteByte((byte)(ConnectionInfo.Port % 0xFF));
// Read Server SOCKS5 version
if (SocketReadByte() != 5)
{
throw new ProxyException("SOCKS5: Version 5 is expected.");
}
// Read response code
var status = SocketReadByte();
switch (status)
{
case 0x00:
break;
case 0x01:
throw new ProxyException("SOCKS5: General failure.");
case 0x02:
throw new ProxyException("SOCKS5: Connection not allowed by ruleset.");
case 0x03:
throw new ProxyException("SOCKS5: Network unreachable.");
case 0x04:
throw new ProxyException("SOCKS5: Host unreachable.");
case 0x05:
throw new ProxyException("SOCKS5: Connection refused by destination host.");
case 0x06:
throw new ProxyException("SOCKS5: TTL expired.");
case 0x07:
throw new ProxyException("SOCKS5: Command not supported or protocol error.");
case 0x08:
throw new ProxyException("SOCKS5: Address type not supported.");
default:
throw new ProxyException("SOCKS5: Not valid response.");
}
// Read 0
if (SocketReadByte() != 0)
{
throw new ProxyException("SOCKS5: 0 byte is expected.");
}
var addressType = SocketReadByte();
var responseIp = new byte[16];
switch (addressType)
{
case 0x01:
SocketRead(responseIp, 0, 4);
break;
case 0x04:
SocketRead(responseIp, 0, 16);
break;
default:
throw new ProxyException(string.Format("Address type '{0}' is not supported.", addressType));
}
var port = new byte[2];
// Read 2 bytes to be ignored
SocketRead(port, 0, 2);
}
private void ConnectHttp()
{
var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");
SocketAbstraction.Send(_socket, SshData.Ascii.GetBytes(string.Format("CONNECT {0}:{1} HTTP/1.0\r\n", ConnectionInfo.Host, ConnectionInfo.Port)));
// Sent proxy authorization is specified
if (!string.IsNullOrEmpty(ConnectionInfo.ProxyUsername))
{
var authorization = string.Format("Proxy-Authorization: Basic {0}\r\n",
Convert.ToBase64String(SshData.Ascii.GetBytes(string.Format("{0}:{1}", ConnectionInfo.ProxyUsername, ConnectionInfo.ProxyPassword)))
);
SocketAbstraction.Send(_socket, SshData.Ascii.GetBytes(authorization));
}
SocketAbstraction.Send(_socket, SshData.Ascii.GetBytes("\r\n"));
HttpStatusCode? statusCode = null;
var contentLength = 0;
while (true)
{
var response = SocketReadLine(ConnectionInfo.Timeout);
if (response == null)
// server shut down socket
break;
if (statusCode == null)
{
var statusMatch = httpResponseRe.Match(response);
if (statusMatch.Success)
{
var httpStatusCode = statusMatch.Result("${statusCode}");
statusCode = (HttpStatusCode) int.Parse(httpStatusCode);
if (statusCode != HttpStatusCode.OK)
{
var reasonPhrase = statusMatch.Result("${reasonPhrase}");
throw new ProxyException(string.Format("HTTP: Status code {0}, \"{1}\"", httpStatusCode,
reasonPhrase));
}
}
continue;
}
// continue on parsing message headers coming from the server
var headerMatch = httpHeaderRe.Match(response);
if (headerMatch.Success)
{
var fieldName = headerMatch.Result("${fieldName}");
if (fieldName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
{
contentLength = int.Parse(headerMatch.Result("${fieldValue}"));
}
continue;
}
// check if we've reached the CRLF which separates request line and headers from the message body
if (response.Length == 0)
{
// read response body if specified
if (contentLength > 0)
{
var contentBody = new byte[contentLength];
SocketRead(contentBody, 0, contentLength);
}
break;
}
}
if (statusCode == null)
throw new ProxyException("HTTP response does not contain status line.");
}
/// <summary>
/// Raises the <see cref="ErrorOccured"/> event.
/// </summary>
/// <param name="exp">The <see cref="Exception"/>.</param>
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
var socketException = exp as SocketException;
if (socketException != null && socketException.SocketErrorCode == SocketError.TimedOut)
return;
}
// "save" exception and set exception wait handle to ensure any waits are interrupted
_exception = exp;
_exceptionWaitHandle.Set();
var errorOccured = ErrorOccured;
if (errorOccured != null)
errorOccured(this, new ExceptionEventArgs(exp));
if (connectionException != null)
{
DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting after exception: {1}", ToHex(SessionId), exp));
Disconnect(connectionException.DisconnectReason, exp.ToString());
}
}
/// <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();
if (_messageListenerCompleted != null)
_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);
}
#region IDisposable implementation
private bool _disposed;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <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 resources.</param>
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;
}
}
/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="Session"/> is reclaimed by garbage collection.
/// </summary>
~Session()
{
Dispose(false);
}
#endregion IDisposable implementation
#region ISession implementation
/// <summary>
/// Gets or sets the connection info.
/// </summary>
/// <value>The connection info.</value>
IConnectionInfo ISession.ConnectionInfo
{
get { return ConnectionInfo; }
}
WaitHandle ISession.MessageListenerCompleted
{
get { return _messageListenerCompleted; }
}
/// <summary>
/// Create a new SSH session channel.
/// </summary>
/// <returns>
/// A new SSH session channel.
/// </returns>
IChannelSession ISession.CreateChannelSession()
{
return new ChannelSession(this, NextChannelNumber, InitialLocalWindowSize, LocalChannelDataPacketSize);
}
/// <summary>
/// Create a new channel for a locally forwarded TCP/IP port.
/// </summary>
/// <returns>
/// A new channel for a locally forwarded TCP/IP port.
/// </returns>
IChannelDirectTcpip ISession.CreateChannelDirectTcpip()
{
return new ChannelDirectTcpip(this, NextChannelNumber, InitialLocalWindowSize, LocalChannelDataPacketSize);
}
/// <summary>
/// Creates a "forwarded-tcpip" SSH channel.
/// </summary>
/// <returns>
/// A new "forwarded-tcpip" SSH channel.
/// </returns>
IChannelForwardedTcpip ISession.CreateChannelForwardedTcpip(uint remoteChannelNumber, uint remoteWindowSize,
uint remoteChannelDataPacketSize)
{
return new ChannelForwardedTcpip(this,
NextChannelNumber,
InitialLocalWindowSize,
LocalChannelDataPacketSize,
remoteChannelNumber,
remoteWindowSize,
remoteChannelDataPacketSize);
}
/// <summary>
/// Sends a message to the server.
/// </summary>
/// <param name="message">The message to send.</param>
/// <exception cref="SshConnectionException">The client is not connected.</exception>
/// <exception cref="SshOperationTimeoutException">The operation timed out.</exception>
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
void ISession.SendMessage(Message message)
{
SendMessage(message);
}
/// <summary>
/// Sends a message to the server.
/// </summary>
/// <param name="message">The message to send.</param>
/// <returns>
/// <c>true</c> if the message was sent to the server; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
/// <remarks>
/// This methods returns <c>false</c> when the attempt to send the message results in a
/// <see cref="SocketException"/> or a <see cref="SshException"/>.
/// </remarks>
bool ISession.TrySendMessage(Message message)
{
return TrySendMessage(message);
}
#endregion ISession implementation
}
}