using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; using System; using System.Collections.Generic; using System.Globalization; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; namespace Renci.SshNet.Connection { /// /// Handles the SSH protocol version exchange. /// internal class ProtocolVersionExchange : IProtocolVersionExchange { private const byte Null = 0x00; #if FEATURE_REGEX_COMPILE private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+?)([ ](?.+))?$", RegexOptions.Compiled); #else private static readonly Regex ServerVersionRe = new Regex("^SSH-(?[^-]+)-(?.+?)([ ](?.+))?$"); #endif /// /// Performs the SSH protocol version exchange. /// /// The identification string of the SSH client. /// A connected to the server. /// The maximum time to wait for the server to respond. /// /// The SSH identification of the server. /// public SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout) { // Immediately send the identification string since the spec states both sides MUST send an identification string // when the connection has been established SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A")); var bytesReceived = new List(); // Get server version from the server, // ignore text lines which are sent before if any while (true) { var line = SocketReadLine(socket, timeout, bytesReceived); if (line == null) { if (bytesReceived.Count == 0) { throw new SshConnectionException("Server response does not contain SSH protocol identification. Connection to remote server was closed before any data was received.", DisconnectReason.ConnectionLost); } throw new SshConnectionException(string.Format("Server response does not contain SSH protocol identification:{0}{1}", Environment.NewLine, PacketDump.Create(bytesReceived, 2)), DisconnectReason.ProtocolError); } var identificationMatch = ServerVersionRe.Match(line); if (identificationMatch.Success) { return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"), GetGroupValue(identificationMatch, "softwareversion"), GetGroupValue(identificationMatch, "comments")); } } } private static string GetGroupValue(Match match, string groupName) { var commentsGroup = match.Groups[groupName]; if (commentsGroup.Success) { return commentsGroup.Value; } return null; } /// /// Performs a blocking read on the socket until a line is read. /// /// The to read from. /// A that represents the time to wait until a line is read. /// A to which read bytes will be added. /// The read has timed-out. /// An error occurred when trying to access the socket. /// /// The line read from the socket, or null when the remote server has shutdown and all data has been received. /// private static string SocketReadLine(Socket socket, TimeSpan timeout, List buffer) { var data = new byte[1]; var startPosition = buffer.Count; // Read data one byte at a time to find end of line and leave any unhandled information in the buffer // to be processed by subsequent invocations. while (true) { var bytesRead = SocketAbstraction.Read(socket, data, 0, data.Length, timeout); if (bytesRead == 0) { // The remote server shut down the socket. break; } var byteRead = data[0]; buffer.Add(byteRead); // The null character MUST NOT be sent if (byteRead == Null) { throw new SshConnectionException(string.Format(CultureInfo.InvariantCulture, "The identification string contains a null character at position 0x{0:X8}:{1}{2}", buffer.Count, Environment.NewLine, PacketDump.Create(buffer.ToArray(), 2))); } if (byteRead == Session.LineFeed && buffer.Count > startPosition + 1 && buffer[buffer.Count - 2] == Session.CarriageReturn) { // Return current line without CRLF return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 2)); } } return null; } } }