using System; using System.Net.Sockets; using System.Threading; using Renci.SshNet.Common; using Renci.SshNet.Messages; using Renci.SshNet.Messages.Connection; using System.Globalization; namespace Renci.SshNet.Channels { /// /// Represents base class for SSH channel implementations. /// internal abstract class Channel : IChannel { private const int Initial = 0; private const int Considered = 1; private const int Sent = 2; private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(false); private EventWaitHandle _channelServerWindowAdjustWaitHandle = new ManualResetEvent(false); private EventWaitHandle _errorOccuredWaitHandle = new ManualResetEvent(false); private readonly object _serverWindowSizeLock = new object(); private readonly uint _initialWindowSize; private uint? _remoteWindowSize; private uint? _remoteChannelNumber; private uint? _remotePacketSize; private ISession _session; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_CLOSE has been sent to the remote party. /// /// /// 0 when the SSH_MSG_CHANNEL_CLOSE message has not been sent or considered /// 1 when sending a SSH_MSG_CHANNEL_CLOSE message to the remote party is under consideration /// 2 when this message has been sent to the remote party /// private int _closeMessageSent; /// /// Holds a value indicating whether a SSH_MSG_CHANNEL_CLOSE has been received from the other /// party. /// /// /// true when a SSH_MSG_CHANNEL_CLOSE message has been received from the other party; /// otherwise, false. /// private bool _closeMessageReceived; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_EOF has been received from the other party. /// /// /// true when a SSH_MSG_CHANNEL_EOF message has been received from the other party; /// otherwise, false. /// private bool _eofMessageReceived; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_EOF has been sent to the remote party. /// /// /// 0 when the SSH_MSG_CHANNEL_EOF message has not been sent or considered /// 1 when sending a SSH_MSG_CHANNEL_EOF message to the remote party is under consideration /// 2 when this message has been sent to the remote party /// private int _eofMessageSent; /// /// Occurs when an exception is thrown when processing channel messages. /// public event EventHandler Exception; /// /// Initializes a new instance. /// /// The session. /// The local channel number. /// Size of the window. /// Size of the packet. protected Channel(ISession session, uint localChannelNumber, uint localWindowSize, uint localPacketSize) { _session = session; _initialWindowSize = localWindowSize; LocalChannelNumber = localChannelNumber; LocalPacketSize = localPacketSize; LocalWindowSize = localWindowSize; _session.ChannelWindowAdjustReceived += OnChannelWindowAdjust; _session.ChannelDataReceived += OnChannelData; _session.ChannelExtendedDataReceived += OnChannelExtendedData; _session.ChannelEofReceived += OnChannelEof; _session.ChannelCloseReceived += OnChannelClose; _session.ChannelRequestReceived += OnChannelRequest; _session.ChannelSuccessReceived += OnChannelSuccess; _session.ChannelFailureReceived += OnChannelFailure; _session.ErrorOccured += Session_ErrorOccured; _session.Disconnected += Session_Disconnected; } /// /// Gets the session. /// /// /// Thhe session. /// protected ISession Session { get { return _session; } } /// /// Gets the type of the channel. /// /// /// The type of the channel. /// public abstract ChannelTypes ChannelType { get; } /// /// Gets the local channel number. /// /// /// The local channel number. /// public uint LocalChannelNumber { get; private set; } /// /// Gets the maximum size of a packet. /// /// /// The maximum size of a packet. /// public uint LocalPacketSize { get; private set; } /// /// Gets the size of the local window. /// /// /// The size of the local window. /// public uint LocalWindowSize { get; private set; } /// /// Gets the remote channel number. /// /// /// The remote channel number. /// public uint RemoteChannelNumber { get { if (!_remoteChannelNumber.HasValue) throw CreateRemoteChannelInfoNotAvailableException(); return _remoteChannelNumber.Value; } private set { _remoteChannelNumber = value; } } /// /// Gets the maximum size of a data packet that we can send using the channel. /// /// /// The maximum size of data that can be sent using a /// on the current channel. /// /// The channel has not been opened, or the open has not yet been confirmed. public uint RemotePacketSize { get { if (!_remotePacketSize.HasValue) throw CreateRemoteChannelInfoNotAvailableException(); return _remotePacketSize.Value; } private set { _remotePacketSize = value; } } /// /// Gets the window size of the remote server. /// /// /// The size of the server window. /// public uint RemoteWindowSize { get { if (!_remoteWindowSize.HasValue) throw CreateRemoteChannelInfoNotAvailableException(); return _remoteWindowSize.Value; } private set { _remoteWindowSize = value; } } /// /// Gets a value indicating whether this channel is open. /// /// /// true if this channel is open; otherwise, false. /// public bool IsOpen { get; protected set; } #region Message events /// /// Occurs when message received /// public event EventHandler DataReceived; /// /// Occurs when message received /// public event EventHandler ExtendedDataReceived; /// /// Occurs when message received /// public event EventHandler EndOfData; /// /// Occurs when message received /// public event EventHandler Closed; /// /// Occurs when message received /// public event EventHandler RequestReceived; /// /// Occurs when message received /// public event EventHandler RequestSucceeded; /// /// Occurs when message received /// public event EventHandler RequestFailed; #endregion /// /// Gets a value indicating whether the session is connected. /// /// /// true if the session is connected; otherwise, false. /// protected bool IsConnected { get { return _session.IsConnected; } } /// /// Gets the connection info. /// /// The connection info. protected IConnectionInfo ConnectionInfo { get { return _session.ConnectionInfo; } } /// /// Gets the session semaphore to control number of session channels /// /// The session semaphore. protected SemaphoreLight SessionSemaphore { get { return _session.SessionSemaphore; } } protected void InitializeRemoteInfo(uint remoteChannelNumber, uint remoteWindowSize, uint remotePacketSize) { RemoteChannelNumber = remoteChannelNumber; RemoteWindowSize = remoteWindowSize; RemotePacketSize = remotePacketSize; } /// /// Sends a SSH_MSG_CHANNEL_DATA message with the specified payload. /// /// The payload to send. public void SendData(byte[] data) { SendData(data, 0, data.Length); } /// /// Sends a SSH_MSG_CHANNEL_DATA message with the specified payload. /// /// An array of containing the payload to send. /// The zero-based offset in at which to begin taking data from. /// The number of bytes of to send. /// /// /// When the size of the data to send exceeds the maximum packet size or the remote window /// size does not allow the full data to be sent, then this method will send the data in /// multiple chunks and will wait for the remote window size to be adjusted when it's zero. /// /// /// This is done to support SSH servers will a small window size that do not agressively /// increase their window size. We need to take into account that there may be SSH servers /// that only increase their window size when it has reached zero. /// /// public void SendData(byte[] data, int offset, int size) { // send channel messages only while channel is open if (!IsOpen) return; var totalBytesToSend = size; while (totalBytesToSend > 0) { var sizeOfCurrentMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend); var channelDataMessage = new ChannelDataMessage( RemoteChannelNumber, data, offset, sizeOfCurrentMessage); _session.SendMessage(channelDataMessage); totalBytesToSend -= sizeOfCurrentMessage; offset += sizeOfCurrentMessage; } } /// /// Closes the channel. /// public void Close() { #if DEBUG_GERT Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | Channel.Close"); #endif // DEBUG_GERT Close(true); } #region Channel virtual methods /// /// Called when channel window need to be adjust. /// /// The bytes to add. protected virtual void OnWindowAdjust(uint bytesToAdd) { lock (_serverWindowSizeLock) { RemoteWindowSize += bytesToAdd; } _channelServerWindowAdjustWaitHandle.Set(); } /// /// Called when channel data is received. /// /// The data. protected virtual void OnData(byte[] data) { AdjustDataWindow(data); var dataReceived = DataReceived; if (dataReceived != null) dataReceived(this, new ChannelDataEventArgs(LocalChannelNumber, data)); } /// /// Called when channel extended data is received. /// /// The data. /// The data type code. protected virtual void OnExtendedData(byte[] data, uint dataTypeCode) { AdjustDataWindow(data); var extendedDataReceived = ExtendedDataReceived; if (extendedDataReceived != null) extendedDataReceived(this, new ChannelExtendedDataEventArgs(LocalChannelNumber, data, dataTypeCode)); } /// /// Called when channel has no more data to receive. /// protected virtual void OnEof() { _eofMessageReceived = true; var endOfData = EndOfData; if (endOfData != null) endOfData(this, new ChannelEventArgs(LocalChannelNumber)); } /// /// Called when channel is closed by the server. /// protected virtual void OnClose() { _closeMessageReceived = true; #if DEBUG_GERT Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | Channel.OnClose()"); #endif // DEBUG_GERT // close the channel Close(false); var closed = Closed; if (closed != null) closed(this, new ChannelEventArgs(LocalChannelNumber)); } /// /// Called when channel request received. /// /// Channel request information. protected virtual void OnRequest(RequestInfo info) { var requestReceived = RequestReceived; if (requestReceived != null) requestReceived(this, new ChannelRequestEventArgs(info)); } /// /// Called when channel request was successful /// protected virtual void OnSuccess() { var requestSuccessed = RequestSucceeded; if (requestSuccessed != null) requestSuccessed(this, new ChannelEventArgs(LocalChannelNumber)); } /// /// Called when channel request failed. /// protected virtual void OnFailure() { var requestFailed = RequestFailed; if (requestFailed != null) requestFailed(this, new ChannelEventArgs(LocalChannelNumber)); } #endregion /// /// Raises event. /// /// The exception. protected void RaiseExceptionEvent(Exception exception) { var handlers = Exception; if (handlers != null) { handlers(this, new ExceptionEventArgs(exception)); } } /// /// 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) { return _session.TrySendMessage(message); } /// /// Sends SSH message to the server. /// /// The message. protected void SendMessage(Message message) { // send channel messages only while channel is open if (!IsOpen) return; _session.SendMessage(message); } /// /// Sends a SSH_MSG_CHANNEL_EOF message to the remote server. /// /// The channel is closed. public void SendEof() { if (!IsOpen) throw CreateChannelClosedException(); _session.SendMessage(new ChannelEofMessage(RemoteChannelNumber)); _eofMessageSent = Sent; } /// /// Waits for the handle to be signaled or for an error to occurs. /// /// The wait handle. protected void WaitOnHandle(WaitHandle waitHandle) { _session.WaitOnHandle(waitHandle); } /// /// Closes the channel, optionally waiting for the SSH_MSG_CHANNEL_CLOSE message to /// be received from the server. /// /// true to wait for the SSH_MSG_CHANNEL_CLOSE message to be received from the server; otherwise, false. protected virtual void Close(bool wait) { // send EOF message first when channel need to be closed, and the remote party has not already sent // a SSH_MSG_CHANNEL_EOF or SSH_MSG_CHANNEL_CLOSE message // // note that we might have had a race condition here when the remote party sends a SSH_MSG_CHANNEL_CLOSE // immediately after it has sent a SSH_MSG_CHANNEL_EOF message // // in that case, we would risk sending a SSH_MSG_CHANNEL_EOF message after the remote party has // closed its end of the channel // // as a solution for this issue we only send a SSH_MSG_CHANNEL_EOF message if we haven't received a // SSH_MSG_CHANNEL_EOF or SSH_MSG_CHANNEL_CLOSE message from the remote party if (Interlocked.CompareExchange(ref _eofMessageSent, Considered, Initial) == Initial) { if (!_closeMessageReceived && !_eofMessageReceived && IsOpen && IsConnected) { if (TrySendMessage(new ChannelEofMessage(RemoteChannelNumber))) _eofMessageSent = Sent; } } // send message to close the channel on the server if (Interlocked.CompareExchange(ref _closeMessageSent, Considered, Initial) == Initial) { // ignore sending close message when client is not connected or the channel is closed if (IsOpen && IsConnected) { if (TrySendMessage(new ChannelCloseMessage(RemoteChannelNumber))) _closeMessageSent = Sent; } } // mark the channel closed IsOpen = false; // wait for channel to be closed if we actually sent a close message (either to initiate closing // the channel, or as response to a SSH_MSG_CHANNEL_CLOSE message sent by the server if (wait && _closeMessageSent == Sent) { try { WaitOnHandle(_channelClosedWaitHandle); } catch (SshConnectionException) { // ignore connection failures as we're closing the channel anyway } } // reset indicators in case we want to reopen the channel; these are safe to reset // since the channel is marked closed by now _eofMessageSent = Initial; _eofMessageReceived = false; _closeMessageReceived = false; _closeMessageSent = Initial; } protected virtual void OnDisconnected() { } protected virtual void OnErrorOccured(Exception exp) { } private void Session_Disconnected(object sender, EventArgs e) { IsOpen = false; try { OnDisconnected(); } catch (Exception ex) { OnChannelException(ex); } } /// /// Called when an occurs while processing a channel message. /// /// The . /// /// This method will in turn invoke , and /// raise the event. /// protected void OnChannelException(Exception ex) { OnErrorOccured(ex); RaiseExceptionEvent(ex); } private void Session_ErrorOccured(object sender, ExceptionEventArgs e) { try { OnErrorOccured(e.Exception); var errorOccuredWaitHandle = _errorOccuredWaitHandle; if (errorOccuredWaitHandle != null) errorOccuredWaitHandle.Set(); } catch (Exception ex) { RaiseExceptionEvent(ex); } } #region Channel message event handlers private void OnChannelWindowAdjust(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnWindowAdjust(e.Message.BytesToAdd); } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelData(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnData(e.Message.Data); } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelExtendedData(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnExtendedData(e.Message.Data, e.Message.DataTypeCode); } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelEof(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnEof(); } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelClose(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnClose(); } catch (Exception ex) { OnChannelException(ex); } var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle != null) channelClosedWaitHandle.Set(); } } private void OnChannelRequest(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { if (_session.ConnectionInfo.ChannelRequests.ContainsKey(e.Message.RequestName)) { // Get request specific class var requestInfo = _session.ConnectionInfo.ChannelRequests[e.Message.RequestName]; // Load request specific data requestInfo.Load(e.Message.RequestData); // Raise request specific event OnRequest(requestInfo); } else { // TODO: we should also send a SSH_MSG_CHANNEL_FAILURE message throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Request '{0}' is not supported.", e.Message.RequestName)); } } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelSuccess(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnSuccess(); } catch (Exception ex) { OnChannelException(ex); } } } private void OnChannelFailure(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { OnFailure(); } catch (Exception ex) { OnChannelException(ex); } } } #endregion private void AdjustDataWindow(byte[] messageData) { LocalWindowSize -= (uint)messageData.Length; // Adjust window if window size is too low if (LocalWindowSize < LocalPacketSize) { SendMessage(new ChannelWindowAdjustMessage(RemoteChannelNumber, _initialWindowSize - LocalWindowSize)); LocalWindowSize = _initialWindowSize; } } /// /// Determines the length of data that currently can be sent in a single message. /// /// The length of the message that must be sent. /// /// The actual data length that currently can be sent. /// private int GetDataLengthThatCanBeSentInMessage(int messageLength) { do { lock (_serverWindowSizeLock) { var serverWindowSize = RemoteWindowSize; if (serverWindowSize == 0) { // allow us to be signal when remote window size is adjusted _channelServerWindowAdjustWaitHandle.Reset(); } else { var bytesThatCanBeSent = Math.Min(Math.Min(RemotePacketSize, (uint) messageLength), serverWindowSize); RemoteWindowSize -= bytesThatCanBeSent; return (int) bytesThatCanBeSent; } } // wait for remote window size to change WaitOnHandle(_channelServerWindowAdjustWaitHandle); } while (true); } private InvalidOperationException CreateRemoteChannelInfoNotAvailableException() { throw new InvalidOperationException("The channel has not been opened, or the open has not yet been confirmed."); } private InvalidOperationException CreateChannelClosedException() { throw new InvalidOperationException("The channel is closed."); } #region IDisposable Members private bool _isDisposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(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 (_isDisposed) return; if (disposing) { #if DEBUG_GERT Console.WriteLine("ID: " + Thread.CurrentThread.ManagedThreadId + " | Channel.Dipose(bool)"); #endif // DEBUG_GERT Close(false); var session = _session; if (session != null) { session.ChannelWindowAdjustReceived -= OnChannelWindowAdjust; session.ChannelDataReceived -= OnChannelData; session.ChannelExtendedDataReceived -= OnChannelExtendedData; session.ChannelEofReceived -= OnChannelEof; session.ChannelCloseReceived -= OnChannelClose; session.ChannelRequestReceived -= OnChannelRequest; session.ChannelSuccessReceived -= OnChannelSuccess; session.ChannelFailureReceived -= OnChannelFailure; session.ErrorOccured -= Session_ErrorOccured; session.Disconnected -= Session_Disconnected; _session = null; } var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle != null) { channelClosedWaitHandle.Dispose(); _channelClosedWaitHandle = null; } var channelServerWindowAdjustWaitHandle = _channelServerWindowAdjustWaitHandle; if (channelServerWindowAdjustWaitHandle != null) { channelServerWindowAdjustWaitHandle.Dispose(); _channelServerWindowAdjustWaitHandle = null; } var errorOccuredWaitHandle = _errorOccuredWaitHandle; if (errorOccuredWaitHandle != null) { errorOccuredWaitHandle.Dispose(); _errorOccuredWaitHandle = null; } _isDisposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~Channel() { Dispose(false); } #endregion } }