< Summary

Information
Class: Renci.SshNet.Connection.ProtocolVersionExchange
Assembly: Renci.SshNet
File(s): \home\appveyor\projects\ssh-net\src\Renci.SshNet\Connection\ProtocolVersionExchange.cs
Line coverage
90%
Covered lines: 109
Uncovered lines: 11
Coverable lines: 120
Total lines: 266
Line coverage: 90.8%
Branch coverage
76%
Covered branches: 26
Total branches: 34
Branch coverage: 76.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
.cctor()100%1100%
Start(...)100%6100%
StartAsync()33.33%670%
GetGroupValue(...)100%2100%
SocketReadLine(...)100%10100%
SocketReadLineAsync()60%1077.27%
CreateConnectionLostException()100%1100%
CreateServerResponseContainsNullCharacterException(...)100%1100%
CreateServerResponseDoesNotContainIdentification(...)100%1100%

File(s)

\home\appveyor\projects\ssh-net\src\Renci.SshNet\Connection\ProtocolVersionExchange.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Net.Sockets;
 5using System.Text;
 6using System.Text.RegularExpressions;
 7using System.Threading;
 8using System.Threading.Tasks;
 9
 10using Renci.SshNet.Abstractions;
 11using Renci.SshNet.Common;
 12using Renci.SshNet.Messages.Transport;
 13
 14namespace Renci.SshNet.Connection
 15{
 16    /// <summary>
 17    /// Handles the SSH protocol version exchange.
 18    /// </summary>
 19    /// <remarks>
 20    /// https://tools.ietf.org/html/rfc4253#section-4.2.
 21    /// </remarks>
 22    internal sealed class ProtocolVersionExchange : IProtocolVersionExchange
 23    {
 24        private const byte Null = 0x00;
 25
 426        private static readonly Regex ServerVersionRe = new Regex("^SSH-(?<protoversion>[^-]+)-(?<softwareversion>.+?)([
 27
 28        /// <summary>
 29        /// Performs the SSH protocol version exchange.
 30        /// </summary>
 31        /// <param name="clientVersion">The identification string of the SSH client.</param>
 32        /// <param name="socket">A <see cref="Socket"/> connected to the server.</param>
 33        /// <param name="timeout">The maximum time to wait for the server to respond.</param>
 34        /// <returns>
 35        /// The SSH identification of the server.
 36        /// </returns>
 37        public SshIdentification Start(string clientVersion, Socket socket, TimeSpan timeout)
 127238        {
 39            // Immediately send the identification string since the spec states both sides MUST send an identification s
 40            // when the connection has been established
 127241            SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A"));
 42
 127243            var bytesReceived = new List<byte>();
 44
 45            // Get server version from the server,
 46            // ignore text lines which are sent before if any
 137447            while (true)
 137448            {
 137449                var line = SocketReadLine(socket, timeout, bytesReceived);
 135350                if (line is null)
 1851                {
 1852                    if (bytesReceived.Count == 0)
 953                    {
 954                        throw CreateConnectionLostException();
 55                    }
 56
 957                    throw CreateServerResponseDoesNotContainIdentification(bytesReceived);
 58                }
 59
 133560                var identificationMatch = ServerVersionRe.Match(line);
 133561                if (identificationMatch.Success)
 123362                {
 123363                    return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"),
 123364                                                 GetGroupValue(identificationMatch, "softwareversion"),
 123365                                                 GetGroupValue(identificationMatch, "comments"));
 66                }
 10267            }
 123368        }
 69
 70        /// <summary>
 71        /// Asynchronously performs the SSH protocol version exchange.
 72        /// </summary>
 73        /// <param name="clientVersion">The identification string of the SSH client.</param>
 74        /// <param name="socket">A <see cref="Socket"/> connected to the server.</param>
 75        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
 76        /// <returns>
 77        /// A task that represents the SSH protocol version exchange. The value of its
 78        /// <see cref="Task{Task}.Result"/> contains the SSH identification of the server.
 79        /// </returns>
 80        public async Task<SshIdentification> StartAsync(string clientVersion, Socket socket, CancellationToken cancellat
 281        {
 82            // Immediately send the identification string since the spec states both sides MUST send an identification s
 83            // when the connection has been established
 284            SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A"));
 85
 286            var bytesReceived = new List<byte>();
 87
 88            // Get server version from the server,
 89            // ignore text lines which are sent before if any
 290            while (true)
 291            {
 292                var line = await SocketReadLineAsync(socket, bytesReceived, cancellationToken).ConfigureAwait(false);
 293                if (line is null)
 094                {
 095                    if (bytesReceived.Count == 0)
 096                    {
 097                        throw CreateConnectionLostException();
 98                    }
 99
 0100                    throw CreateServerResponseDoesNotContainIdentification(bytesReceived);
 101                }
 102
 2103                var identificationMatch = ServerVersionRe.Match(line);
 2104                if (identificationMatch.Success)
 2105                {
 2106                    return new SshIdentification(GetGroupValue(identificationMatch, "protoversion"),
 2107                                                 GetGroupValue(identificationMatch, "softwareversion"),
 2108                                                 GetGroupValue(identificationMatch, "comments"));
 109                }
 0110            }
 2111        }
 112
 113        private static string GetGroupValue(Match match, string groupName)
 3705114        {
 3705115            var commentsGroup = match.Groups[groupName];
 3705116            if (commentsGroup.Success)
 2482117            {
 2482118                return commentsGroup.Value;
 119            }
 120
 1223121            return null;
 3705122        }
 123
 124        /// <summary>
 125        /// Performs a blocking read on the socket until a line is read.
 126        /// </summary>
 127        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
 128        /// <param name="timeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param
 129        /// <param name="buffer">A <see cref="List{Byte}"/> to which read bytes will be added.</param>
 130        /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
 131        /// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
 132        /// <returns>
 133        /// The line read from the socket, or <see langword="null"/> when the remote server has shutdown and all data ha
 134        /// </returns>
 135        private static string SocketReadLine(Socket socket, TimeSpan timeout, List<byte> buffer)
 1374136        {
 1374137            var data = new byte[1];
 138
 1374139            var startPosition = buffer.Count;
 140
 141            // Read data one byte at a time to find end of line and leave any unhandled information in the buffer
 142            // to be processed by subsequent invocations.
 27411143            while (true)
 27411144            {
 27411145                var bytesRead = SocketAbstraction.Read(socket, data, 0, data.Length, timeout);
 27402146                if (bytesRead == 0)
 18147                {
 148                    // The remote server shut down the socket.
 18149                    break;
 150                }
 151
 27384152                var byteRead = data[0];
 27384153                buffer.Add(byteRead);
 154
 155                // The null character MUST NOT be sent
 27384156                if (byteRead is Null)
 12157                {
 12158                    throw CreateServerResponseContainsNullCharacterException(buffer);
 159                }
 160
 27372161                if (byteRead == Session.LineFeed)
 1335162                {
 1335163                    if (buffer.Count > startPosition + 1 && buffer[buffer.Count - 2] == Session.CarriageReturn)
 1299164                    {
 165                        // Return current line without CRLF
 1299166                        return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 
 167                    }
 168
 169                    // Even though RFC4253 clearly indicates that the identification string should be terminated
 170                    // by a CR LF we also support banners and identification strings that are terminated by a LF
 171
 172                    // Return current line without LF
 36173                    return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 1));
 174                }
 26037175            }
 176
 18177            return null;
 1353178        }
 179
 180        private static async Task<string> SocketReadLineAsync(Socket socket, List<byte> buffer, CancellationToken cancel
 2181        {
 2182            var data = new byte[1];
 183
 2184            var startPosition = buffer.Count;
 185
 186            // Read data one byte at a time to find end of line and leave any unhandled information in the buffer
 187            // to be processed by subsequent invocations.
 42188            while (true)
 42189            {
 42190                var bytesRead = await SocketAbstraction.ReadAsync(socket, data, 0, data.Length, cancellationToken).Confi
 42191                if (bytesRead == 0)
 0192                {
 0193                    throw new SshConnectionException("The connection was closed by the remote host.");
 194                }
 195
 42196                var byteRead = data[0];
 42197                buffer.Add(byteRead);
 198
 199                // The null character MUST NOT be sent
 42200                if (byteRead is Null)
 0201                {
 0202                    throw CreateServerResponseContainsNullCharacterException(buffer);
 203                }
 204
 42205                if (byteRead == Session.LineFeed)
 2206                {
 2207                    if (buffer.Count > startPosition + 1 && buffer[buffer.Count - 2] == Session.CarriageReturn)
 2208                    {
 209                        // Return current line without CRLF
 2210                        return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 
 211                    }
 212
 213                    // Even though RFC4253 clearly indicates that the identification string should be terminated
 214                    // by a CR LF we also support banners and identification strings that are terminated by a LF
 215
 216                    // Return current line without LF
 0217                    return Encoding.UTF8.GetString(buffer.ToArray(), startPosition, buffer.Count - (startPosition + 1));
 218                }
 40219            }
 2220        }
 221
 222        private static SshConnectionException CreateConnectionLostException()
 9223        {
 224#pragma warning disable SA1118 // Parameter should not span multiple lines
 9225            var message = string.Format(CultureInfo.InvariantCulture,
 9226                                        "The server response does not contain an SSH identification string.{0}" +
 9227                                        "The connection to the remote server was closed before any data was received.{0}
 9228                                        "More information on the Protocol Version Exchange is available here:{0}" +
 9229                                        "https://tools.ietf.org/html/rfc4253#section-4.2",
 9230                                        Environment.NewLine);
 231#pragma warning restore SA1118 // Parameter should not span multiple lines
 232
 9233            return new SshConnectionException(message, DisconnectReason.ConnectionLost);
 9234        }
 235
 236        private static SshConnectionException CreateServerResponseContainsNullCharacterException(List<byte> buffer)
 12237        {
 238#pragma warning disable SA1118 // Parameter should not span multiple lines
 12239            var message = string.Format(CultureInfo.InvariantCulture,
 12240                                        "The server response contains a null character at position 0x{0:X8}:{1}{1}{2}{1}
 12241                                        "A server must not send a null character before the Protocol Version Exchange is
 12242                                        "More information is available here:{1}" +
 12243                                        "https://tools.ietf.org/html/rfc4253#section-4.2",
 12244                                        buffer.Count,
 12245                                        Environment.NewLine,
 12246                                        PacketDump.Create(buffer.ToArray(), 2));
 247#pragma warning restore SA1118 // Parameter should not span multiple lines
 248
 12249            throw new SshConnectionException(message);
 250        }
 251
 252        private static SshConnectionException CreateServerResponseDoesNotContainIdentification(List<byte> bytesReceived)
 9253        {
 254#pragma warning disable SA1118 // Parameter should not span multiple lines
 9255            var message = string.Format(CultureInfo.InvariantCulture,
 9256                                        "The server response does not contain an SSH identification string:{0}{0}{1}{0}{
 9257                                        "More information on the Protocol Version Exchange is available here:{0}" +
 9258                                        "https://tools.ietf.org/html/rfc4253#section-4.2",
 9259                                        Environment.NewLine,
 9260                                        PacketDump.Create(bytesReceived, 2));
 261#pragma warning restore SA1118 // Parameter should not span multiple lines
 262
 9263            throw new SshConnectionException(message, DisconnectReason.ProtocolError);
 264        }
 265    }
 266}