using System; using System.Globalization; using System.Net.Sockets; using System.Threading; using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages; using Renci.SshNet.Messages.Connection; namespace Renci.SshNet.Channels { /// /// Represents base class for SSH channel implementations. /// internal abstract class Channel : IChannel { private readonly object _serverWindowSizeLock = new object(); private readonly object _messagingLock = new object(); private readonly uint _initialWindowSize; private readonly ISession _session; private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false); private EventWaitHandle _channelServerWindowAdjustWaitHandle = new ManualResetEvent(initialState: false); private uint? _remoteWindowSize; private uint? _remoteChannelNumber; private uint? _remotePacketSize; private bool _isDisposed; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_CLOSE has been sent to the remote party. /// /// /// when a SSH_MSG_CHANNEL_CLOSE message has been sent to the other party; /// otherwise, . /// private bool _closeMessageSent; /// /// Holds a value indicating whether a SSH_MSG_CHANNEL_CLOSE has been received from the other /// party. /// /// /// when a SSH_MSG_CHANNEL_CLOSE message has been received from the other party; /// otherwise, . /// private bool _closeMessageReceived; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_EOF has been received from the other party. /// /// /// when a SSH_MSG_CHANNEL_EOF message has been received from the other party; /// otherwise, . /// private bool _eofMessageReceived; /// /// Holds a value indicating whether the SSH_MSG_CHANNEL_EOF has been sent to the remote party. /// /// /// when a SSH_MSG_CHANNEL_EOF message has been sent to the remote party; /// otherwise, . /// private bool _eofMessageSent; /// /// Occurs when an exception is thrown when processing channel messages. /// public event EventHandler Exception; /// /// Initializes a new instance of the class. /// /// 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 data packet that we can receive using the channel. /// /// /// The maximum size of a packet. /// /// /// /// This is the maximum size (in bytes) we support for the data (payload) of a /// SSH_MSG_CHANNEL_DATA message we receive. /// /// /// We currently do not enforce this limit. /// /// 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 or sets a value indicating whether this channel is open. /// /// /// if this channel is open; otherwise, . /// public bool IsOpen { get; protected set; } /// /// 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; /// /// Gets a value indicating whether the session is connected. /// /// /// if the session is connected; otherwise, . /// 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; } } /// /// Initializes the information on the remote channel. /// /// The remote channel number. /// The remote window size. /// The remote packet size. 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; } } /// /// 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); DataReceived?.Invoke(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); ExtendedDataReceived?.Invoke(this, new ChannelExtendedDataEventArgs(LocalChannelNumber, data, dataTypeCode)); } /// /// Called when channel has no more data to receive. /// protected virtual void OnEof() { _eofMessageReceived = true; EndOfData?.Invoke(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. // We need to signal this before invoking Close() as it may very well // be blocked waiting for this signal. var channelClosedWaitHandle = _channelClosedWaitHandle; if (channelClosedWaitHandle != null) { _ = channelClosedWaitHandle.Set(); } // close the channel Close(); } /// /// Called when channel request received. /// /// Channel request information. protected virtual void OnRequest(RequestInfo info) { RequestReceived?.Invoke(this, new ChannelRequestEventArgs(info)); } /// /// Called when channel request was successful. /// protected virtual void OnSuccess() { RequestSucceeded?.Invoke(this, new ChannelEventArgs(LocalChannelNumber)); } /// /// Called when channel request failed. /// protected virtual void OnFailure() { RequestFailed?.Invoke(this, new ChannelEventArgs(LocalChannelNumber)); } /// /// Raises event. /// /// The exception. private void RaiseExceptionEvent(Exception exception) { Exception?.Invoke(this, new ExceptionEventArgs(exception)); } /// /// Sends a message to the server. /// /// The message to send. /// /// if the message was sent to the server; otherwise, . /// /// The size of the packet exceeds the maximum size defined by the protocol. /// /// This methods returns 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 (_messagingLock) { _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 (_messagingLock) { // 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; // only wait for the channel to be closed by the server if we didn't send a // SSH_MSG_CHANNEL_CLOSE as response to a SSH_MSG_CHANNEL_CLOSE sent by the // server var closeWaitResult = _session.TryWait(_channelClosedWaitHandle, ConnectionInfo.ChannelCloseTimeout); if (closeWaitResult != WaitResult.Success) { DiagnosticAbstraction.Log(string.Format("Wait for channel close not successful: {0:G}.", closeWaitResult)); } } } if (IsOpen) { // mark sure the channel is marked closed before we raise the Closed event // this also ensures don't raise the Closed event more than once IsOpen = false; if (_closeMessageReceived) { // raise event signaling that both ends of the channel have been closed Closed?.Invoke(this, new ChannelEventArgs(LocalChannelNumber)); } } } } 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); } catch (Exception ex) { RaiseExceptionEvent(ex); } } 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 { if (_session.ConnectionInfo.ChannelRequests.TryGetValue(e.Message.RequestName, out var 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); } } } 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."); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// to release both managed and unmanaged resources; to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!_isDisposed && disposing) { Close(); var session = _session; if (session is not 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 is not null) { _channelClosedWaitHandle = null; channelClosedWaitHandle.Dispose(); } var channelServerWindowAdjustWaitHandle = _channelServerWindowAdjustWaitHandle; if (channelServerWindowAdjustWaitHandle is not null) { _channelServerWindowAdjustWaitHandle = null; channelServerWindowAdjustWaitHandle.Dispose(); } _isDisposed = true; } } /// /// Finalizes an instance of the class. /// ~Channel() { Dispose(disposing: false); } } }