Allow for easier troubleshooting of protocol version exchange.

Fixes #751.
This commit is contained in:
drieseng
2020-12-29 18:56:26 +01:00
parent eefcd8fbc5
commit de06f12350
15 changed files with 1402 additions and 81 deletions
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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]);
}
}
}
@@ -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);
}
}
}
@@ -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]);
}
}
}
@@ -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]);
}
}
}
@@ -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
View File
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}");
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);
}
/// <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>