From de06f1235009f4c7d108b8edf8ad8f96af5303e1 Mon Sep 17 00:00:00 2001 From: drieseng Date: Tue, 29 Dec 2020 18:56:26 +0100 Subject: [PATCH] Allow for easier troubleshooting of protocol version exchange. Fixes #751. --- .../Classes/Common/PacketDumpTest.cs | 155 ++++++++++++++++++ ...ectionClosedByServer_NoDataSentByServer.cs | 117 +++++++++++++ ...ndWithIdentificationStringBeforeTimeout.cs | 125 ++++++++++++++ ...est_ServerResponseContainsNullCharacter.cs | 128 +++++++++++++++ ...entificationOnlyContainsProtocolVersion.cs | 125 ++++++++++++++ ...changeTest_ServerResponseValid_Comments.cs | 119 ++++++++++++++ ...angeTest_ServerResponseValid_NoComments.cs | 119 ++++++++++++++ .../Connection/SshIdentificationTest.cs | 139 ++++++++++++++++ src/Renci.SshNet/Common/PacketDump.cs | 102 ++++++++++++ .../Connection/IProtocolVersionExchange.cs | 22 +++ .../Connection/ProtocolVersionExchange.cs | 131 +++++++++++++++ .../Connection/SshIdentification.cs | 92 +++++++++++ src/Renci.SshNet/IServiceFactory.cs | 9 + src/Renci.SshNet/ServiceFactory.cs | 12 ++ src/Renci.SshNet/Session.cs | 88 +--------- 15 files changed, 1402 insertions(+), 81 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/Common/PacketDumpTest.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ConnectionClosedByServer_NoDataSentByServer.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerDoesNotRespondWithIdentificationStringBeforeTimeout.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseContainsNullCharacter.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseInvalid_SshIdentificationOnlyContainsProtocolVersion.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs create mode 100644 src/Renci.SshNet.Tests/Classes/Connection/SshIdentificationTest.cs create mode 100644 src/Renci.SshNet/Common/PacketDump.cs create mode 100644 src/Renci.SshNet/Connection/IProtocolVersionExchange.cs create mode 100644 src/Renci.SshNet/Connection/ProtocolVersionExchange.cs create mode 100644 src/Renci.SshNet/Connection/SshIdentification.cs diff --git a/src/Renci.SshNet.Tests/Classes/Common/PacketDumpTest.cs b/src/Renci.SshNet.Tests/Classes/Common/PacketDumpTest.cs new file mode 100644 index 00000000..00401915 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Common/PacketDumpTest.cs @@ -0,0 +1,155 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using System; + +namespace Renci.SshNet.Tests.Classes.Common +{ + [TestClass] + public class PacketDumpTest + { + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsNull() + { + const byte[] data = null; + + try + { + PacketDump.Create(data, 0); + Assert.Fail(); + } + catch (ArgumentNullException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("data", ex.ParamName); + } + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_IndentLevelLessThanZero() + { + var data = new byte[0]; + + try + { + PacketDump.Create(data, -1); + Assert.Fail(); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual(string.Format("Cannot be less than zero.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message); + Assert.AreEqual("indentLevel", ex.ParamName); + } + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsEmpty() + { + var data = new byte[0]; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(string.Empty, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsMultipleOfLineWidth_IndentLevelTwo() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53, 0x53, 0x48, + 0x2e, 0x4e, 0x45, 0x54, 0x20, 0x32, 0x30, 0x32, 0x30, 0x20, 0xf6, 0x7a, 0x32, 0x7f, 0x1f, 0x7e + }; + var expected = " 00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 53 48 ...e b.D...6.SSH" + + Environment.NewLine + + " 00000010 2E 4E 45 54 20 32 30 32 30 20 F6 7A 32 7F 1F 7E .NET 2020 .z2..~"; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsMultipleOfLineWidth_IndentLevelZero() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53, 0x53, 0x48, + 0x2e, 0x4e, 0x45, 0x54, 0x20, 0x32, 0x30, 0x32, 0x30, 0x20, 0xf6, 0x7a, 0x32, 0x7f, 0x1f, 0x7e + }; + var expected = "00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 53 48 ...e b.D...6.SSH" + + Environment.NewLine + + "00000010 2E 4E 45 54 20 32 30 32 30 20 F6 7A 32 7F 1F 7E .NET 2020 .z2..~"; + + var actual = PacketDump.Create(data, 0); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsLineWith() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53, 0x53, 0x48 + }; + var expected = " 00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 53 48 ...e b.D...6.SSH"; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsLessThanLineWith() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53 + }; + var expected = " 00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 ...e b.D...6.S"; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsGreaterThanLineWidthButLessThanMultipleOfLineWidth() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53, 0x53, 0x48, + 0x2e, 0x4e, 0x45, 0x54 + }; + var expected = " 00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 53 48 ...e b.D...6.SSH" + + Environment.NewLine + + " 00000010 2E 4E 45 54 .NET"; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void Create_ByteArrayAndIndentLevel_DataIsGreaterThanMultipleOfLineWidth() + { + var data = new byte[] + { + 0x07, 0x00, 0x1f, 0x65, 0x20, 0x62, 0x09, 0x44, 0x7f, 0x0d, 0x0a, 0x36, 0x80, 0x53, 0x53, 0x48, + 0x2e, 0x4e, 0x45, 0x54, 0x20, 0x32, 0x30, 0x32, 0x30, 0x20, 0xf6, 0x7a, 0x32, 0x7f, 0x1f, 0x7e, + 0x78, 0x54, 0x00, 0x52 + }; + var expected = " 00000000 07 00 1F 65 20 62 09 44 7F 0D 0A 36 80 53 53 48 ...e b.D...6.SSH" + + Environment.NewLine + + " 00000010 2E 4E 45 54 20 32 30 32 30 20 F6 7A 32 7F 1F 7E .NET 2020 .z2..~" + + Environment.NewLine + + " 00000020 78 54 00 52 xT.R"; + + var actual = PacketDump.Create(data, 2); + + Assert.AreEqual(expected, actual); + + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ConnectionClosedByServer_NoDataSentByServer.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ConnectionClosedByServer_NoDataSentByServer.cs new file mode 100644 index 00000000..2e9eed4c --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ConnectionClosedByServer_NoDataSentByServer.cs @@ -0,0 +1,117 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ConnectionClosedByServer_NoDataSentByServer + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private bool _clientDisconnected; + private Socket _client; + private SshConnectionException _actualException; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "\uD55C"; + _timeout = TimeSpan.FromSeconds(5); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + socket.Shutdown(SocketShutdown.Both); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + try + { + _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void StartShouldHaveThrownSshConnectionException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Server response does not contain SSH protocol identification. Connection to remote server was closed before any data was received.", _actualException.Message); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + Assert.AreEqual(5, _dataReceivedByServer.Count); + + Assert.AreEqual(0xed, _dataReceivedByServer[0]); + Assert.AreEqual(0x95, _dataReceivedByServer[1]); + Assert.AreEqual(0x9c, _dataReceivedByServer[2]); + Assert.AreEqual(0x0d, _dataReceivedByServer[3]); + Assert.AreEqual(0x0a, _dataReceivedByServer[4]); + } + + [TestMethod] + public void ConnectionIsClosedByServer() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + + var bytesReceived = _client.Receive(new byte[1]); + Assert.AreEqual(0, bytesReceived); + + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerDoesNotRespondWithIdentificationStringBeforeTimeout.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerDoesNotRespondWithIdentificationStringBeforeTimeout.cs new file mode 100644 index 00000000..34353ed3 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerDoesNotRespondWithIdentificationStringBeforeTimeout.cs @@ -0,0 +1,125 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ServerDoesNotRespondWithIdentificationStringBeforeTimeout + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private byte[] _serverIdentification; + private bool _clientDisconnected; + private Socket _client; + private SshIdentification _actual; + private SshOperationTimeoutException _actualException; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1"; + _timeout = TimeSpan.FromMilliseconds(200); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + _serverIdentification = Encoding.UTF8.GetBytes("SSH-Zero-OurSSHAppliance\r\n!"); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + socket.Send(Encoding.UTF8.GetBytes("Welcome!\r\n")); + Thread.Sleep(_timeout.Add(TimeSpan.FromMilliseconds(50))); + socket.Shutdown(SocketShutdown.Both); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + try + { + _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + Assert.Fail(); + } + catch (SshOperationTimeoutException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void StartShouldHaveThrownSshOperationTimeoutException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual(string.Format("Socket read operation has timed out after {0} milliseconds.", _timeout.TotalMilliseconds), _actualException.Message); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + var expected = Encoding.UTF8.GetBytes(_clientVersion); + + Assert.AreEqual(expected.Length + 2, _dataReceivedByServer.Count); + + Assert.IsTrue(expected.SequenceEqual(_dataReceivedByServer.Take(expected.Length))); + Assert.AreEqual(Session.CarriageReturn, _dataReceivedByServer[_dataReceivedByServer.Count - 2]); + Assert.AreEqual(Session.LineFeed, _dataReceivedByServer[_dataReceivedByServer.Count - 1]); + } + + [TestMethod] + public void ConnectionIsClosedByServer() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + + var bytesReceived = _client.Receive(new byte[1]); + Assert.AreEqual(0, bytesReceived); + + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseContainsNullCharacter.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseContainsNullCharacter.cs new file mode 100644 index 00000000..b3694c60 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseContainsNullCharacter.cs @@ -0,0 +1,128 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ServerResponseContainsNullCharacter + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private byte[] _serverIdentification; + private bool _clientDisconnected; + private Socket _client; + private SshConnectionException _actualException; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1"; + _timeout = TimeSpan.FromSeconds(5); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + _serverIdentification = Encoding.UTF8.GetBytes("\uD55C!\0\uD55CSSH -2.0-Renci.SshNet.SshClient.0.0.1"); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.Connected += socket => socket.Send(_serverIdentification); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + try + { + _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void StartShouldHaveThrownSshConnectionException() + { + var expectedMessage = "The identification string contains a null character at position 0x00000005:" + Environment.NewLine + + " 00000000 ED 95 9C 21 00 ...!."; + + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual(expectedMessage, _actualException.Message); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + var expected = Encoding.UTF8.GetBytes(_clientVersion); + + Assert.AreEqual(expected.Length + 2, _dataReceivedByServer.Count); + + Assert.IsTrue(expected.SequenceEqual(_dataReceivedByServer.Take(expected.Length))); + Assert.AreEqual(Session.CarriageReturn, _dataReceivedByServer[_dataReceivedByServer.Count - 2]); + Assert.AreEqual(Session.LineFeed, _dataReceivedByServer[_dataReceivedByServer.Count - 1]); + } + + [TestMethod] + public void ClientRemainsConnected() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + + [TestMethod] + public void ClientHasNotReadPastNullCharacter() + { + var buffer = new byte[1]; + + var bytesReceived = _client.Receive(buffer); + Assert.AreEqual(1, bytesReceived); + Assert.AreEqual(0xed, buffer[0]); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseInvalid_SshIdentificationOnlyContainsProtocolVersion.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseInvalid_SshIdentificationOnlyContainsProtocolVersion.cs new file mode 100644 index 00000000..fe3eb0a7 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseInvalid_SshIdentificationOnlyContainsProtocolVersion.cs @@ -0,0 +1,125 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ServerResponseInvalid_SshIdentificationOnlyContainsProtocolVersion + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private byte[] _serverIdentification; + private bool _clientDisconnected; + private Socket _client; + private SshConnectionException _actualException; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1"; + _timeout = TimeSpan.FromSeconds(5); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + _serverIdentification = Encoding.UTF8.GetBytes("SSH-2.0\r\n"); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + socket.Send(_serverIdentification); + socket.Shutdown(SocketShutdown.Both); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + try + { + _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void StartShouldHaveThrownSshConnectionException() + { + var expectedMessage = "Server response does not contain SSH protocol identification:" + Environment.NewLine + + " 00000000 53 53 48 2D 32 2E 30 0D 0A SSH-2.0.."; + + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual(expectedMessage, _actualException.Message); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + var expected = Encoding.UTF8.GetBytes(_clientVersion); + + Assert.AreEqual(expected.Length + 2, _dataReceivedByServer.Count); + + Assert.IsTrue(expected.SequenceEqual(_dataReceivedByServer.Take(expected.Length))); + Assert.AreEqual(Session.CarriageReturn, _dataReceivedByServer[_dataReceivedByServer.Count - 2]); + Assert.AreEqual(Session.LineFeed, _dataReceivedByServer[_dataReceivedByServer.Count - 1]); + } + + [TestMethod] + public void ConnectionIsClosedByServer() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + + var bytesReceived = _client.Receive(new byte[1]); + Assert.AreEqual(0, bytesReceived); + + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs new file mode 100644 index 00000000..a71a1edf --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs @@ -0,0 +1,119 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ServerResponseValid_Comments + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private byte[] _serverIdentification; + private bool _clientDisconnected; + private Socket _client; + private SshIdentification _actual; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1"; + _timeout = TimeSpan.FromSeconds(5); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + _serverIdentification = Encoding.UTF8.GetBytes("Welcome stranger!\r\n\r\nSSH-ABC2.0-OurSSHAppliance-1.4.7 Use at own risk.\uD55C\r\n!"); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + socket.Send(_serverIdentification); + socket.Shutdown(SocketShutdown.Both); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + _actual = _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + } + + [TestMethod] + public void StartShouldReturnIdentificationOfServer() + { + Assert.IsNotNull(_actual); + Assert.AreEqual("ABC2.0", _actual.ProtocolVersion); + Assert.AreEqual("OurSSHAppliance-1.4.7", _actual.SoftwareVersion); + Assert.AreEqual("Use at own risk.\uD55C", _actual.Comments); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + var expected = Encoding.UTF8.GetBytes(_clientVersion); + + Assert.AreEqual(expected.Length + 2, _dataReceivedByServer.Count); + + Assert.IsTrue(expected.SequenceEqual(_dataReceivedByServer.Take(expected.Length))); + Assert.AreEqual(Session.CarriageReturn, _dataReceivedByServer[_dataReceivedByServer.Count - 2]); + Assert.AreEqual(Session.LineFeed, _dataReceivedByServer[_dataReceivedByServer.Count - 1]); + } + + [TestMethod] + public void ClientRemainsConnected() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + + [TestMethod] + public void ClientDidNotReadPastIdentification() + { + var buffer = new byte[1]; + + var bytesReceived = _client.Receive(buffer); + Assert.AreEqual(1, bytesReceived); + Assert.AreEqual(0x21, buffer[0]); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs new file mode 100644 index 00000000..4b2b0e83 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs @@ -0,0 +1,119 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Common; +using Renci.SshNet.Connection; +using Renci.SshNet.Tests.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ProtocolVersionExchangeTest_ServerResponseValid_NoComments + { + private AsyncSocketListener _server; + private ProtocolVersionExchange _protocolVersionExchange; + private string _clientVersion; + private TimeSpan _timeout; + private IPEndPoint _serverEndPoint; + private List _dataReceivedByServer; + private byte[] _serverIdentification; + private bool _clientDisconnected; + private Socket _client; + private SshIdentification _actual; + + [TestInitialize] + public void Setup() + { + Arrange(); + Act(); + } + + [TestCleanup] + public void Cleanup() + { + if (_server != null) + { + _server.Dispose(); + _server = null; + } + + if (_client != null) + { + _client.Shutdown(SocketShutdown.Both); + _client.Close(); + _client = null; + } + } + + protected void Arrange() + { + _clientVersion = "SSH-2.0-Renci.SshNet.SshClient.0.0.1"; + _timeout = TimeSpan.FromSeconds(5); + _serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8122); + _dataReceivedByServer = new List(); + _serverIdentification = Encoding.UTF8.GetBytes("Welcome stranger!\r\n\r\nSSH-Zero-OurSSHAppliance\r\n!"); + + _server = new AsyncSocketListener(_serverEndPoint); + _server.Start(); + _server.BytesReceived += (bytes, socket) => + { + _dataReceivedByServer.AddRange(bytes); + socket.Send(_serverIdentification); + socket.Shutdown(SocketShutdown.Both); + }; + _server.Disconnected += (socket) => _clientDisconnected = true; + + _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _client.Connect(_serverEndPoint); + + _protocolVersionExchange = new ProtocolVersionExchange(); + } + + protected void Act() + { + _actual = _protocolVersionExchange.Start(_clientVersion, _client, _timeout); + } + + [TestMethod] + public void StartShouldReturnIdentificationOfServer() + { + Assert.IsNotNull(_actual); + Assert.AreEqual("Zero", _actual.ProtocolVersion); + Assert.AreEqual("OurSSHAppliance", _actual.SoftwareVersion); + Assert.IsNull(_actual.Comments); + } + + [TestMethod] + public void ClientIdentificationWasSentToServer() + { + var expected = Encoding.UTF8.GetBytes(_clientVersion); + + Assert.AreEqual(expected.Length + 2, _dataReceivedByServer.Count); + + Assert.IsTrue(expected.SequenceEqual(_dataReceivedByServer.Take(expected.Length))); + Assert.AreEqual(Session.CarriageReturn, _dataReceivedByServer[_dataReceivedByServer.Count - 2]); + Assert.AreEqual(Session.LineFeed, _dataReceivedByServer[_dataReceivedByServer.Count - 1]); + } + + [TestMethod] + public void ClientRemainsConnected() + { + Assert.IsTrue(_client.Connected); + Assert.IsFalse(_clientDisconnected); + } + + [TestMethod] + public void ClientDidNotReadPastIdentification() + { + var buffer = new byte[1]; + + var bytesReceived = _client.Receive(buffer); + Assert.AreEqual(1, bytesReceived); + Assert.AreEqual(0x21, buffer[0]); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/SshIdentificationTest.cs b/src/Renci.SshNet.Tests/Classes/Connection/SshIdentificationTest.cs new file mode 100644 index 00000000..2a7137ae --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/Connection/SshIdentificationTest.cs @@ -0,0 +1,139 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Renci.SshNet.Connection; +using System; + +namespace Renci.SshNet.Tests.Classes.Connection +{ + [TestClass] + public class SshIdentificationTest + { + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersion() + { + const string protocolVersion = "1.5"; + const string softwareVersion = "SSH.NET_2020.0.0"; + + var sshIdentification = new SshIdentification(protocolVersion, softwareVersion); + Assert.AreSame(protocolVersion, sshIdentification.ProtocolVersion); + Assert.AreSame(softwareVersion, sshIdentification.SoftwareVersion); + Assert.IsNull(sshIdentification.Comments); + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersion_ProtocolVersionIsNull() + { + const string protocolVersion = null; + const string softwareVersion = "SSH.NET_2020.0.0"; + + try + { + new SshIdentification(protocolVersion, softwareVersion); + Assert.Fail(); + } + catch (ArgumentNullException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("protocolVersion", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersion_SoftwareVersionIsNull() + { + const string protocolVersion = "2.0"; + const string softwareVersion = null; + + try + { + new SshIdentification(protocolVersion, softwareVersion); + Assert.Fail(); + } + catch (ArgumentNullException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("softwareVersion", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersionAndComments() + { + const string protocolVersion = "1.5"; + const string softwareVersion = "SSH.NET_2020.0.0"; + const string comments = "Beware, dangerous!"; + + var sshIdentification = new SshIdentification(protocolVersion, softwareVersion, comments); + Assert.AreSame(protocolVersion, sshIdentification.ProtocolVersion); + Assert.AreSame(softwareVersion, sshIdentification.SoftwareVersion); + Assert.AreSame(comments, sshIdentification.Comments); + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersionAndComments_CommentsIsNull() + { + const string protocolVersion = "1.5"; + const string softwareVersion = "SSH.NET_2020.0.0"; + const string comments = null; + + var sshIdentification = new SshIdentification(protocolVersion, softwareVersion, comments); + Assert.AreSame(protocolVersion, sshIdentification.ProtocolVersion); + Assert.AreSame(softwareVersion, sshIdentification.SoftwareVersion); + Assert.IsNull(comments, sshIdentification.Comments); + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersionAndComments_ProtocolVersionIsNull() + { + const string protocolVersion = null; + const string softwareVersion = "SSH.NET_2020.0.0"; + const string comments = "Beware!"; + + try + { + new SshIdentification(protocolVersion, softwareVersion, comments); + Assert.Fail(); + } + catch (ArgumentNullException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("protocolVersion", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_ProtocolVersionAndSoftwareVersionAndComments_SoftwareVersionIsNull() + { + const string protocolVersion = "2.0"; + const string softwareVersion = null; + const string comments = "Beware!"; + + try + { + new SshIdentification(protocolVersion, softwareVersion, comments); + Assert.Fail(); + } + catch (ArgumentNullException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("softwareVersion", ex.ParamName); + } + } + + [TestMethod] + public void ToString_Comments() + { + var sshIdentification = new SshIdentification("2.0", "SSH.NET", "Beware, dangerous"); + Assert.AreEqual("SSH-2.0-SSH.NET Beware, dangerous", sshIdentification.ToString()); + } + + [TestMethod] + public void ToString_CommentsIsNull() + { + var sshIdentification = new SshIdentification("2.0", "SSH.NET_2020.0.0"); + Assert.AreEqual("SSH-2.0-SSH.NET_2020.0.0", sshIdentification.ToString()); + + sshIdentification = new SshIdentification("2.0", "SSH.NET_2020.0.0", null); + Assert.AreEqual("SSH-2.0-SSH.NET_2020.0.0", sshIdentification.ToString()); + } + } +} diff --git a/src/Renci.SshNet/Common/PacketDump.cs b/src/Renci.SshNet/Common/PacketDump.cs new file mode 100644 index 00000000..2dd6a7e2 --- /dev/null +++ b/src/Renci.SshNet/Common/PacketDump.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Renci.SshNet.Common +{ + internal class PacketDump + { + public static string Create(List data, int indentLevel) + { + return Create(data.ToArray(), indentLevel); + } + + public static string Create(byte[] data, int indentLevel) + { + if (data == null) + throw new ArgumentNullException("data"); + if (indentLevel < 0) + throw new ArgumentOutOfRangeException("indentLevel", "Cannot be less than zero."); + + const int lineWidth = 16; + + var result = new StringBuilder(); + var line = new byte[lineWidth]; + var indentChars = new string(' ', indentLevel); + + for (var pos = 0; pos < data.Length; ) + { + var linePos = 0; + + if (result.Length > 0) + { + result.Append(Environment.NewLine); + } + + result.Append(indentChars); + result.Append(pos.ToString("X8")); + result.Append(" "); + + while (true) + { + line[linePos++] = data[pos++]; + + if (linePos == lineWidth || pos == data.Length) + { + break; + } + } + + result.Append(AsHex(line, linePos)); + result.Append(" "); + result.Append(AsAscii(line, linePos)); + } + return result.ToString(); + } + + private static string AsHex(byte[] data, int length) + { + var hex = new StringBuilder(); + + for (var i = 0; i < length; i++) + { + if (i > 0) + { + hex.Append(' '); + } + + hex.Append(data[i].ToString("X2", CultureInfo.InvariantCulture)); + } + + if (length < data.Length) + { + hex.Append(new string(' ', (data.Length - length) * 3)); + } + + return hex.ToString(); + } + + private static string AsAscii(byte[] data, int length) + { + var ascii = new StringBuilder(); + const char dot = '.'; + + for (var i = 0; i < length; i++) + { + var b = data[i]; + + if (b < 32 || b >= 127) + { + ascii.Append(dot); + } + else + { + ascii.Append(Encoding.ASCII.GetString(data, i, 1)); + } + } + + return ascii.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs b/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs new file mode 100644 index 00000000..77bcfbd3 --- /dev/null +++ b/src/Renci.SshNet/Connection/IProtocolVersionExchange.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Sockets; + +namespace Renci.SshNet.Connection +{ + /// + /// Handles the SSH protocol version exchange. + /// + internal interface IProtocolVersionExchange + { + /// + /// Performs the SSH protocol version exchange. + /// + /// The identification string of the SSH client. + /// A connected to the server. + /// The maximum time to wait for the server to respond. + /// + /// The SSH identification of the server. + /// + SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout); + } +} diff --git a/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs new file mode 100644 index 00000000..6b43bb59 --- /dev/null +++ b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs @@ -0,0 +1,131 @@ +using Renci.SshNet.Abstractions; +using Renci.SshNet.Common; +using Renci.SshNet.Messages.Transport; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; + +namespace Renci.SshNet.Connection +{ + /// + /// Handles the SSH protocol version exchange. + /// + internal class ProtocolVersionExchange : IProtocolVersionExchange + { + private const byte Null = 0x00; + +#if FEATURE_REGEX_COMPILE + private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+?)([ ](?.+))?$", RegexOptions.Compiled); +#else + private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+?)([ ](?.+))?$"); +#endif + + /// + /// Performs the SSH protocol version exchange. + /// + /// The identification string of the SSH client. + /// A connected to the server. + /// The maximum time to wait for the server to respond. + /// + /// The SSH identification of the server. + /// + public SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout) + { + // Immediately send the identification string since the spec states both sides MUST send an identification string + // when the connection has been established + SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A")); + + var bytesReceived = new List(); + + // Get server version from the server, + // ignore text lines which are sent before if any + while (true) + { + var line = SocketReadLine(socket, timeout, bytesReceived); + if (line == null) + { + if (bytesReceived.Count == 0) + { + throw new SshConnectionException("Server response does not contain SSH protocol identification. Connection to remote server was closed before any data was received.", DisconnectReason.ConnectionLost); + } + + throw new SshConnectionException(string.Format("Server response does not contain SSH protocol identification:{0}{1}", Environment.NewLine, PacketDump.Create(bytesReceived, 2)), + DisconnectReason.ProtocolError); + } + + var identificationMatch = ServerVersionRe.Match(line); + if (identificationMatch.Success) + { + return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"), + GetGroupValue(identificationMatch, "softwareversion"), + GetGroupValue(identificationMatch, "comments")); + } + } + } + + private static string GetGroupValue(Match match, string groupName) + { + var commentsGroup = match.Groups[groupName]; + if (commentsGroup.Success) + { + return commentsGroup.Value; + } + + return null; + } + + /// + /// Performs a blocking read on the socket until a line is read. + /// + /// The to read from. + /// A that represents the time to wait until a line is read. + /// A to which read bytes will be added. + /// The read has timed-out. + /// An error occurred when trying to access the socket. + /// + /// The line read from the socket, or null when the remote server has shutdown and all data has been received. + /// + private static string SocketReadLine(Socket socket, TimeSpan timeout, List buffer) + { + var data = new byte[1]; + + var startPosition = buffer.Count; + + // 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. + while (true) + { + var bytesRead = SocketAbstraction.Read(socket, data, 0, data.Length, timeout); + if (bytesRead == 0) + { + // The remote server shut down the socket. + break; + } + + var byteRead = data[0]; + buffer.Add(byteRead); + + // The null character MUST NOT be sent + if (byteRead == Null) + { + throw new SshConnectionException(string.Format(CultureInfo.InvariantCulture, + "The identification string contains a null character at position 0x{0:X8}:{1}{2}", + buffer.Count, + Environment.NewLine, + PacketDump.Create(buffer.ToArray(), 2))); + } + + if (byteRead == Session.LineFeed && buffer.Count > startPosition + 1 && buffer[buffer.Count - 2] == Session.CarriageReturn) + { + // Return current line without CRLF + return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 2)); + } + } + + return null; + } + } +} diff --git a/src/Renci.SshNet/Connection/SshIdentification.cs b/src/Renci.SshNet/Connection/SshIdentification.cs new file mode 100644 index 00000000..90c00fe9 --- /dev/null +++ b/src/Renci.SshNet/Connection/SshIdentification.cs @@ -0,0 +1,92 @@ +using System; + +namespace Renci.SshNet.Connection +{ + /// + /// Represents an SSH identification. + /// + internal class SshIdentification + { + /// + /// Initializes a new instance with the specified protocol version + /// and software version. + /// + /// The SSH protocol version. + /// The software version of the implementation + /// is . + /// is . + public SshIdentification(string protocolVersion, string softwareVersion) + : this(protocolVersion, softwareVersion, null) + { + } + + /// + /// Initializes a new instance with the specified protocol version, + /// software version and comments. + /// + /// The SSH protocol version. + /// The software version of the implementation + /// The comments. + /// is . + /// is . + public SshIdentification(string protocolVersion, string softwareVersion, string comments) + { + if (protocolVersion == null) + throw new ArgumentNullException("protocolVersion"); + if (softwareVersion == null) + throw new ArgumentNullException("softwareVersion"); + + ProtocolVersion = protocolVersion; + SoftwareVersion = softwareVersion; + Comments = comments; + } + + /// + /// Gets or sets the software version of the implementation. + /// + /// + /// The software version of the implementation. + /// + /// + /// This is primarily used to trigger compatibility extensions and to indicate + /// the capabilities of an implementation. + /// + public string SoftwareVersion { get; private set; } + + /// + /// Gets or sets the SSH protocol version. + /// + /// + /// The SSH protocol version. + /// + public string ProtocolVersion { get; private set; } + + /// + /// Gets or sets the comments. + /// + /// + /// The comments, or if there are no comments. + /// + /// + /// should contain additional information that might be useful + /// in solving user problems. + /// + public string Comments { get; private set; } + + /// + /// Returns the SSH identification string. + /// + /// + /// The SSH identification string. + /// + public override string ToString() + { + var identificationString = "SSH-" + ProtocolVersion + "-" + SoftwareVersion; + if (Comments != null) + { + identificationString += " " + Comments; + } + return identificationString; + } + } +} diff --git a/src/Renci.SshNet/IServiceFactory.cs b/src/Renci.SshNet/IServiceFactory.cs index a244dda5..5b501961 100644 --- a/src/Renci.SshNet/IServiceFactory.cs +++ b/src/Renci.SshNet/IServiceFactory.cs @@ -120,5 +120,14 @@ namespace Renci.SshNet /// is . /// The value of is not supported. IConnector CreateConnector(IConnectionInfo connectionInfo); + + /// + /// Create an that deals with the SSH protocol + /// version exchange. + /// + /// + /// An . + /// + IProtocolVersionExchange CreateProtocolVersionExchange(); } } diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index e708f6a7..72112f32 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -220,5 +220,17 @@ namespace Renci.SshNet throw new NotSupportedException(string.Format("ProxyTypes '{0}' is not supported.", connectionInfo.ProxyType)); } } + + /// + /// Create an that deals with the SSH protocol + /// version exchange. + /// + /// + /// An . + /// + public IProtocolVersionExchange CreateProtocolVersionExchange() + { + return new ProtocolVersionExchange(); + } } } diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 1f657e1e..bc3c0769 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -2,7 +2,6 @@ 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; @@ -16,7 +15,6 @@ using System.Globalization; using System.Linq; using Renci.SshNet.Abstractions; using Renci.SshNet.Security.Cryptography; -using System.Collections.Generic; namespace Renci.SshNet { @@ -25,7 +23,6 @@ namespace Renci.SshNet /// public class Session : ISession { - private const byte Null = 0x00; internal const byte CarriageReturn = 0x0d; internal const byte LineFeed = 0x0a; @@ -81,12 +78,6 @@ namespace Renci.SshNet /// private const int LocalChannelDataPacketSize = 1024*64; -#if FEATURE_REGEX_COMPILE - private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+)( SP.+)?$", RegexOptions.Compiled); -#else - private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+)( SP.+)?$"); -#endif - /// /// Controls how many authentication attempts can take place at the same time. /// @@ -589,41 +580,19 @@ namespace Renci.SshNet _socket = _serviceFactory.CreateConnector(ConnectionInfo) .Connect(ConnectionInfo); - // Immediately send the identification string since the spec states both sides MUST send an identification string - // when the connection has been established - SocketAbstraction.Send(_socket, Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\x0D\x0A", ClientVersion))); - - Match versionMatch; - - // Get server version from the server, - // ignore text lines which are sent before if any - while (true) - { - var serverVersion = SocketReadLine(_socket, 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; - } - } + var serverIdentification = _serviceFactory.CreateProtocolVersionExchange() + .Start(ClientVersion, _socket, ConnectionInfo.Timeout); // Set connection versions - ConnectionInfo.ServerVersion = ServerVersion; + ConnectionInfo.ServerVersion = serverIdentification.ToString(); ConnectionInfo.ClientVersion = ClientVersion; - // Get server SSH version - var version = versionMatch.Result("${protoversion}"); + DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", serverIdentification.ProtocolVersion, serverIdentification.SoftwareVersion)); - 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"))) + if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99"))) { - throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", version), DisconnectReason.ProtocolVersionNotSupported); + throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", serverIdentification.ProtocolVersion), + DisconnectReason.ProtocolVersionNotSupported); } // Register Transport response messages @@ -1771,49 +1740,6 @@ namespace Renci.SshNet return SocketAbstraction.Read(socket, buffer, offset, length, InfiniteTimeSpan); } - /// - /// Performs a blocking read on the socket until a line is read. - /// - /// The to read from. - /// A that represents the time to wait until a line is read. - /// The read has timed-out. - /// An error occurred when trying to access the socket. - /// - /// The line read from the socket, or null when the remote server has shutdown and all data has been received. - /// - private static string SocketReadLine(Socket socket, TimeSpan timeout) - { - var encoding = SshData.Ascii; - var buffer = new List(); - 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); - } - /// /// Shuts down and disposes the socket. ///