Bladeren bron

Allow for easier troubleshooting of protocol version exchange.
Fixes #751.

drieseng 4 jaren geleden
bovenliggende
commit
de06f12350

+ 155 - 0
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);
+
+        }
+    }
+}

+ 117 - 0
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<byte> _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<byte>();
+
+            _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);
+        }
+    }
+}

+ 125 - 0
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<byte> _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<byte>();
+            _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);
+        }
+    }
+}

+ 128 - 0
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<byte> _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<byte>();
+            _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]);
+        }
+    }
+}

+ 125 - 0
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<byte> _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<byte>();
+            _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);
+        }
+    }
+}

+ 119 - 0
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<byte> _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<byte>();
+            _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]);
+        }
+    }
+}

+ 119 - 0
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<byte> _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<byte>();
+            _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]);
+        }
+    }
+}

+ 139 - 0
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());
+        }
+    }
+}

+ 102 - 0
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<byte> 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();
+        }
+    }
+}

+ 22 - 0
src/Renci.SshNet/Connection/IProtocolVersionExchange.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    /// <summary>
+    /// Handles the SSH protocol version exchange.
+    /// </summary>
+    internal interface IProtocolVersionExchange
+    {
+        /// <summary>
+        /// Performs the SSH protocol version exchange.
+        /// </summary>
+        /// <param name="clientVersion">The identification string of the SSH client.</param>
+        /// <param name="socket">A <see cref="Socket"/> connected to the server.</param>
+        /// <param name="timeout">The maximum time to wait for the server to respond.</param>
+        /// <returns>
+        /// The SSH identification of the server.
+        /// </returns>
+        SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout);
+    }
+}

+ 131 - 0
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
+{
+    /// <summary>
+    /// Handles the SSH protocol version exchange.
+    /// </summary>
+    internal class ProtocolVersionExchange : IProtocolVersionExchange
+    {
+        private const byte Null = 0x00;
+
+#if FEATURE_REGEX_COMPILE
+        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+?)([ ](?<comments>.+))?$", RegexOptions.Compiled);
+#else
+        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+?)([ ](?<comments>.+))?$");
+#endif
+
+        /// <summary>
+        /// Performs the SSH protocol version exchange.
+        /// </summary>
+        /// <param name="clientVersion">The identification string of the SSH client.</param>
+        /// <param name="socket">A <see cref="Socket"/> connected to the server.</param>
+        /// <param name="timeout">The maximum time to wait for the server to respond.</param>
+        /// <returns>
+        /// The SSH identification of the server.
+        /// </returns>
+        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<byte>();
+
+            // 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;
+        }
+
+        /// <summary>
+        /// Performs a blocking read on the socket until a line is read.
+        /// </summary>
+        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
+        /// <param name="timeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param>
+        /// <param name="buffer">A <see cref="List{Byte}"/> to which read bytes will be added.</param>
+        /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
+        /// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
+        /// <returns>
+        /// The line read from the socket, or <c>null</c> when the remote server has shutdown and all data has been received.
+        /// </returns>
+        private static string SocketReadLine(Socket socket, TimeSpan timeout, List<byte> 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;
+        }
+    }
+}

+ 92 - 0
src/Renci.SshNet/Connection/SshIdentification.cs

@@ -0,0 +1,92 @@
+using System;
+
+namespace Renci.SshNet.Connection
+{
+    /// <summary>
+    /// Represents an SSH identification.
+    /// </summary>
+    internal class SshIdentification
+    {
+        /// <summary>
+        /// Initializes a new <see cref="SshIdentification"/> instance with the specified protocol version
+        /// and software version.
+        /// </summary>
+        /// <param name="protocolVersion">The SSH protocol version.</param>
+        /// <param name="softwareVersion">The software version of the implementation</param>
+        /// <exception cref="ArgumentNullException"><paramref name="protocolVersion"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="softwareVersion"/> is <see langword="null"/>.</exception>
+        public SshIdentification(string protocolVersion, string softwareVersion)
+            : this(protocolVersion, softwareVersion, null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new <see cref="SshIdentification"/> instance with the specified protocol version,
+        /// software version and comments.
+        /// </summary>
+        /// <param name="protocolVersion">The SSH protocol version.</param>
+        /// <param name="softwareVersion">The software version of the implementation</param>
+        /// <param name="comments">The comments.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="protocolVersion"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="softwareVersion"/> is <see langword="null"/>.</exception>
+        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;
+        }
+
+        /// <summary>
+        /// Gets or sets the software version of the implementation.
+        /// </summary>
+        /// <value>
+        /// The software version of the implementation.
+        /// </value>
+        /// <remarks>
+        /// This is primarily used to trigger compatibility extensions and to indicate
+        /// the capabilities of an implementation.
+        /// </remarks>
+        public string SoftwareVersion { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the SSH protocol version.
+        /// </summary>
+        /// <value>
+        /// The SSH protocol version.
+        /// </value>
+        public string ProtocolVersion { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the comments.
+        /// </summary>
+        /// <value>
+        /// The comments, or <see langword="null"/> if there are no comments.
+        /// </value>
+        /// <remarks>
+        /// <see cref="Comments"/> should contain additional information that might be useful
+        /// in solving user problems.
+        /// </remarks>
+        public string Comments { get; private set; }
+
+        /// <summary>
+        /// Returns the SSH identification string.
+        /// </summary>
+        /// <returns>
+        /// The SSH identification string.
+        /// </returns>
+        public override string ToString()
+        {
+            var identificationString = "SSH-" + ProtocolVersion + "-" + SoftwareVersion;
+            if (Comments != null)
+            {
+                identificationString += " " + Comments;
+            }
+            return identificationString;
+        }
+    }
+}

+ 9 - 0
src/Renci.SshNet/IServiceFactory.cs

@@ -120,5 +120,14 @@ namespace Renci.SshNet
         /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
         /// <exception cref="NotSupportedException">The <see cref="IConnectionInfo.ProxyType"/> value of <paramref name="connectionInfo"/> is not supported.</exception>
         IConnector CreateConnector(IConnectionInfo connectionInfo);
+
+        /// <summary>
+        /// Create an <see cref="IProtocolVersionExchange"/> that deals with the SSH protocol
+        /// version exchange.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IProtocolVersionExchange"/>.
+        /// </returns>
+        IProtocolVersionExchange CreateProtocolVersionExchange();
     }
 }

+ 12 - 0
src/Renci.SshNet/ServiceFactory.cs

@@ -220,5 +220,17 @@ namespace Renci.SshNet
                     throw new NotSupportedException(string.Format("ProxyTypes '{0}' is not supported.", connectionInfo.ProxyType));
             }
         }
+
+        /// <summary>
+        /// Create an <see cref="IProtocolVersionExchange"/> that deals with the SSH protocol
+        /// version exchange.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="IProtocolVersionExchange"/>.
+        /// </returns>
+        public IProtocolVersionExchange CreateProtocolVersionExchange()
+        {
+            return new ProtocolVersionExchange();
+        }
     }
 }

+ 7 - 81
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
     /// </summary>
     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
         /// </remarks>
         private const int LocalChannelDataPacketSize = 1024*64;
 
-#if FEATURE_REGEX_COMPILE
-        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$", RegexOptions.Compiled);
-#else
-        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+)( SP.+)?$");
-#endif
-
         /// <summary>
         /// Controls how many authentication attempts can take place at the same time.
         /// </summary>
@@ -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}");
-
-                    var softwareName = versionMatch.Result("${softwareversion}");
+                    DiagnosticAbstraction.Log(string.Format("Server version '{0}' on '{1}'.", serverIdentification.ProtocolVersion, serverIdentification.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);
         }
 
-        /// <summary>
-        /// Performs a blocking read on the socket until a line is read.
-        /// </summary>
-        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
-        /// <param name="timeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param>
-        /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
-        /// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
-        /// <returns>
-        /// The line read from the socket, or <c>null</c> when the remote server has shutdown and all data has been received.
-        /// </returns>
-        private static string SocketReadLine(Socket socket, TimeSpan timeout)
-        {
-            var encoding = SshData.Ascii;
-            var buffer = new List<byte>();
-            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);
-        }
-
         /// <summary>
         /// Shuts down and disposes the socket.
         /// </summary>