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 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. /// /// /// true when a SSH_MSG_CHANNEL_CLOSE message has been sent to the other party; /// otherwise, false. /// private bool _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. /// /// /// true when a SSH_MSG_CHANNEL_EOF message has been sent to the remote party; /// otherwise, false. /// private bool _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 is received. /// public event EventHandler DataReceived; /// /// Occurs when is received. /// public event EventHandler ExtendedDataReceived; /// /// Occurs when is received. /// public event EventHandler EndOfData; /// /// Occurs when is received. /// public event EventHandler Closed; /// /// Occurs when is received. /// public event EventHandler RequestReceived; /// /// Occurs when is received. /// public event EventHandler RequestSucceeded; /// /// Occurs when is 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; } } #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; // signal that SSH_MSG_CHANNEL_CLOSE message was received from server var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle != null) channelClosedWaitHandle.Set(); // close the channel Close(); // raise event signaling that the server has closed the channel 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 // Channel virtual methods /// /// Raises event. /// /// The exception. private 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(); lock (this) { _session.SendMessage(new ChannelEofMessage(RemoteChannelNumber)); _eofMessageSent = true; } } /// /// 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, waiting for the SSH_MSG_CHANNEL_CLOSE message to be received from the server. /// protected virtual void Close() { // synchronize sending SSH_MSG_CHANNEL_EOF and SSH_MSG_CHANNEL_CLOSE to ensure that these messages // are sent in that other; when both the client and the server attempt to close the channel at the // same time we would otherwise risk sending the SSH_MSG_CHANNEL_EOF after the SSH_MSG_CHANNEL_CLOSE // message causing the server to disconnect the session. lock (this) { // send EOF message first the following conditions are met: // * we have not sent a SSH_MSG_CHANNEL_EOF message // * remote party has not already sent a SSH_MSG_CHANNEL_EOF message // * remote party has not already sent a SSH_MSG_CHANNEL_CLOSE message // * the channel is open // * the session is connected if (!_eofMessageSent && !_closeMessageReceived && !_eofMessageReceived && IsOpen && IsConnected) { if (TrySendMessage(new ChannelEofMessage(RemoteChannelNumber))) { _eofMessageSent = true; } } // send message to close the channel on the server when it has not already been sent // and the channel is open and the session is connected if (!_closeMessageSent && IsOpen && IsConnected) { if (TrySendMessage(new ChannelCloseMessage(RemoteChannelNumber))) { _closeMessageSent = true; // 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 try { WaitOnHandle(_channelClosedWaitHandle); } catch (SshConnectionException) { // ignore connection failures as we're closing the channel anyway } } } IsOpen = false; } } 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); } } } private void OnChannelRequest(object sender, MessageEventArgs e) { if (e.Message.LocalChannelNumber == LocalChannelNumber) { try { RequestInfo requestInfo; if (_session.ConnectionInfo.ChannelRequests.TryGetValue(e.Message.RequestName, out requestInfo)) { // 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 // Channel message event handlers 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 == 0U) { // 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 static InvalidOperationException CreateRemoteChannelInfoNotAvailableException() { throw new InvalidOperationException("The channel has not been opened, or the open has not yet been confirmed."); } private static 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) { Close(); var session = _session; if (session != null) { _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; } var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle != null) { _channelClosedWaitHandle = null; channelClosedWaitHandle.Dispose(); } var channelServerWindowAdjustWaitHandle = _channelServerWindowAdjustWaitHandle; if (channelServerWindowAdjustWaitHandle != null) { _channelServerWindowAdjustWaitHandle = null; channelServerWindowAdjustWaitHandle.Dispose(); } var errorOccuredWaitHandle = _errorOccuredWaitHandle; if (errorOccuredWaitHandle != null) { _errorOccuredWaitHandle = null; errorOccuredWaitHandle.Dispose(); } _isDisposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~Channel() { Dispose(false); } #endregion // IDisposable Members } }