diff --git a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs index 1f536ff27..55be94a43 100644 --- a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs @@ -98,6 +98,18 @@ public void Bind(IPEndPoint remoteEndpoint, IForwardedPort forwardedPort) SocketAbstraction.ReadContinuous(_socket, buffer, 0, buffer.Length, SendData); } + /// + /// Called when channel has no more data to receive. + /// + protected override void OnEof() + { + base.OnEof(); + + // the server will not send any more data; shut down the send direction of the socket so the + // connected host receives end-of-stream, while it can still send data back over the channel + ShutdownSocket(SocketShutdown.Send); + } + protected override void OnErrorOccurred(Exception exp) { base.OnErrorOccurred(exp); diff --git a/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs b/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs new file mode 100644 index 000000000..bc81fcfea --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_ChannelEofReceived.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Renci.SshNet.Channels; +using Renci.SshNet.Messages; +using Renci.SshNet.Messages.Connection; + +namespace Renci.SshNet.Tests.Classes.Channels +{ + [TestClass] + public class ChannelForwardedTcpipTest_ChannelEofReceived + { + private Mock _sessionMock; + private Mock _forwardedPortMock; + private Mock _connectionInfoMock; + private ChannelForwardedTcpip _channel; + private uint _localChannelNumber; + private uint _localWindowSize; + private uint _localPacketSize; + private uint _remoteWindowSize; + private uint _remotePacketSize; + private uint _remoteChannelNumber; + private TimeSpan _channelCloseTimeout; + private TcpListener _remoteListener; + private Socket _remoteSocket; + private Exception _channelException; + private Thread _channelThread; + private List _dataSentToServer; + private ManualResetEvent _dataSentToServerWaitHandle; + private int _bytesReceivedAfterEof; + private byte[] _dataToSendAfterEof; + + [TestInitialize] + public void Initialize() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void CleanUp() + { + _remoteSocket?.Dispose(); + _remoteSocket = null; + + _channelThread?.Join(TimeSpan.FromSeconds(5)); + _channelThread = null; + + _channel?.Dispose(); + _channel = null; + + _remoteListener?.Stop(); + _remoteListener = null; + + _dataSentToServerWaitHandle?.Dispose(); + _dataSentToServerWaitHandle = null; + } + + private void Arrange() + { + var random = new Random(); + _localChannelNumber = (uint)random.Next(0, int.MaxValue); + _localWindowSize = (uint)random.Next(2000, 3000); + _localPacketSize = (uint)random.Next(1000, 2000); + _remoteChannelNumber = (uint)random.Next(0, int.MaxValue); + _remoteWindowSize = (uint)random.Next(1000, int.MaxValue); + _remotePacketSize = (uint)random.Next(100, 200); + _channelCloseTimeout = TimeSpan.FromSeconds(random.Next(10, 20)); + _channelException = null; + _dataSentToServer = new List(); + _dataSentToServerWaitHandle = new ManualResetEvent(false); + _dataToSendAfterEof = new byte[] { 0x0a, 0x0b, 0x0c }; + + _sessionMock = new Mock(MockBehavior.Strict); + _connectionInfoMock = new Mock(MockBehavior.Strict); + _forwardedPortMock = new Mock(MockBehavior.Strict); + + _ = _sessionMock.Setup(p => p.SessionLoggerFactory).Returns(NullLoggerFactory.Instance); + _ = _sessionMock.Setup(p => p.IsConnected).Returns(true); + _ = _sessionMock.Setup(p => p.ConnectionInfo).Returns(_connectionInfoMock.Object); + _ = _connectionInfoMock.Setup(p => p.Timeout).Returns(TimeSpan.FromSeconds(5)); + _ = _connectionInfoMock.Setup(p => p.ChannelCloseTimeout).Returns(_channelCloseTimeout); + _ = _sessionMock.Setup(p => p.SendMessage(It.IsAny())); + _ = _sessionMock.Setup(p => p.SendMessage(It.IsAny())) + .Callback(m => + { + var dataMessage = (ChannelDataMessage)m; + lock (_dataSentToServer) + { + _dataSentToServer.AddRange(dataMessage.Data.Skip(dataMessage.Offset).Take(dataMessage.Size)); + } + + _ = _dataSentToServerWaitHandle.Set(); + }); + _ = _sessionMock.Setup(p => p.TrySendMessage(It.IsAny())) + .Returns(true); + _ = _sessionMock.Setup(p => p.TryWait(It.IsAny(), _channelCloseTimeout)) + .Callback((waitHandle, channelCloseTimeout) => + { + _sessionMock.Raise( + s => s.ChannelCloseReceived += null, + new MessageEventArgs(new ChannelCloseMessage(_localChannelNumber))); + _ = waitHandle.WaitOne(); + }) + .Returns(WaitResult.Success); + + _remoteListener = new TcpListener(IPAddress.Loopback, 0); + _remoteListener.Start(); + var remoteEndpoint = (IPEndPoint)_remoteListener.LocalEndpoint; + + _channel = new ChannelForwardedTcpip(_sessionMock.Object, + _localChannelNumber, + _localWindowSize, + _localPacketSize, + _remoteChannelNumber, + _remoteWindowSize, + _remotePacketSize); + + _channelThread = new Thread(() => + { + try + { + _channel.Bind(remoteEndpoint, _forwardedPortMock.Object); + } + catch (Exception ex) + { + _channelException = ex; + } + }); + _channelThread.Start(); + + _remoteSocket = _remoteListener.AcceptSocket(); + _remoteSocket.ReceiveTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds; + } + + private void Act() + { + _sessionMock.Raise(s => s.ChannelEofReceived += null, + new MessageEventArgs(new ChannelEofMessage(_localChannelNumber))); + + var buffer = new byte[16]; + _bytesReceivedAfterEof = _remoteSocket.Receive(buffer); + + // only the send direction of the socket should be shut down, so the connected host can still send data + _ = _remoteSocket.Send(_dataToSendAfterEof); + _ = _dataSentToServerWaitHandle.WaitOne(TimeSpan.FromSeconds(5)); + + _remoteSocket.Shutdown(SocketShutdown.Send); + } + + [TestMethod] + public void ConnectedHostShouldReceiveEndOfStream() + { + Assert.AreEqual(0, _bytesReceivedAfterEof); + } + + [TestMethod] + public void DataSentByConnectedHostAfterEofShouldBeForwardedToServer() + { + lock (_dataSentToServer) + { + CollectionAssert.AreEqual(_dataToSendAfterEof, _dataSentToServer); + } + } + + [TestMethod] + public void BindShouldFinishWithoutExceptionWhenConnectedHostShutsDownSocket() + { + Assert.IsTrue(_channelThread.Join(TimeSpan.FromSeconds(5))); + Assert.IsNull(_channelException, _channelException?.ToString()); + } + } +}