Jelajahi Sumber

Refactor how connection is established to server.

drieseng 4 tahun lalu
induk
melakukan
dc9c637759

+ 71 - 0
src/Renci.SshNet/Connection/ConnectorBase.cs

@@ -0,0 +1,71 @@
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using Renci.SshNet.Messages.Transport;
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    internal abstract class ConnectorBase : IConnector
+    {
+        public abstract Socket Connect(IConnectionInfo connectionInfo);
+
+        /// <summary>
+        /// Establishes a socket connection to the specified host and port.
+        /// </summary>
+        /// <param name="host">The host name of the server to connect to.</param>
+        /// <param name="port">The port to connect to.</param>
+        /// <param name="timeout">The maximum time to wait for the connection to be established.</param>
+        /// <exception cref="SshOperationTimeoutException">The connection failed to establish within the configured <see cref="ConnectionInfo.Timeout"/>.</exception>
+        /// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
+        protected Socket SocketConnect(string host, int port, TimeSpan timeout)
+        {
+            var ipAddress = DnsAbstraction.GetHostAddresses(host)[0];
+            var ep = new IPEndPoint(ipAddress, port);
+
+            DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}:{1}'.", host, port));
+
+            var socket = SocketAbstraction.Connect(ep, timeout);
+
+            const int socketBufferSize = 2 * Session.MaximumSshPacketSize;
+            socket.SendBufferSize = socketBufferSize;
+            socket.ReceiveBufferSize = socketBufferSize;
+            return socket;
+        }
+
+        protected static byte SocketReadByte(Socket socket)
+        {
+            var buffer = new byte[1];
+            SocketRead(socket, buffer, 0, 1);
+            return buffer[0];
+        }
+
+        /// <summary>
+        /// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
+        /// </summary>
+        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
+        /// <param name="buffer">An array of type <see cref="byte"/> that is the storage location for the received data.</param>
+        /// <param name="offset">The position in <paramref name="buffer"/> parameter to store the received data.</param>
+        /// <param name="length">The number of bytes to read.</param>
+        /// <returns>
+        /// The number of bytes read.
+        /// </returns>
+        /// <exception cref="SshConnectionException">The socket is closed.</exception>
+        /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
+        /// <exception cref="SocketException">The read failed.</exception>
+        protected static int SocketRead(Socket socket, byte[] buffer, int offset, int length)
+        {
+            var bytesRead = SocketAbstraction.Read(socket, buffer, offset, length, Session.InfiniteTimeSpan);
+            if (bytesRead == 0)
+            {
+                // when we're in the disconnecting state (either triggered by client or server), then the
+                // SshConnectionException will interrupt the message listener loop (if not already interrupted)
+                // and the exception itself will be ignored (in RaiseError)
+                throw new SshConnectionException("An established connection was aborted by the server.",
+                                                 DisconnectReason.ConnectionLost);
+            }
+            return bytesRead;
+        }
+    }
+}

+ 12 - 0
src/Renci.SshNet/Connection/DirectConnector.cs

@@ -0,0 +1,12 @@
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    internal class DirectConnector : ConnectorBase
+    {
+        public override Socket Connect(IConnectionInfo connectionInfo)
+        {
+            return SocketConnect(connectionInfo.Host, connectionInfo.Port, connectionInfo.Timeout);
+        }
+    }
+}

+ 140 - 0
src/Renci.SshNet/Connection/HttpConnector.cs

@@ -0,0 +1,140 @@
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+
+namespace Renci.SshNet.Connection
+{
+    internal class HttpConnector : ConnectorBase
+    {
+        public override Socket Connect(IConnectionInfo connectionInfo)
+        {
+            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
+
+            var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
+            var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");
+
+            SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format("CONNECT {0}:{1} HTTP/1.0\r\n", connectionInfo.Host, connectionInfo.Port)));
+
+            //  Sent proxy authorization is specified
+            if (!string.IsNullOrEmpty(connectionInfo.ProxyUsername))
+            {
+                var authorization = string.Format("Proxy-Authorization: Basic {0}\r\n",
+                                                  Convert.ToBase64String(SshData.Ascii.GetBytes(string.Format("{0}:{1}", connectionInfo.ProxyUsername, connectionInfo.ProxyPassword))));
+                SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(authorization));
+            }
+
+            SocketAbstraction.Send(socket, SshData.Ascii.GetBytes("\r\n"));
+
+            HttpStatusCode? statusCode = null;
+            var contentLength = 0;
+
+            while (true)
+            {
+                var response = SocketReadLine(socket, connectionInfo.Timeout);
+                if (response == null)
+                {
+                    // server shut down socket
+                    break;
+                }
+
+                if (statusCode == null)
+                {
+                    var statusMatch = httpResponseRe.Match(response);
+                    if (statusMatch.Success)
+                    {
+                        var httpStatusCode = statusMatch.Result("${statusCode}");
+                        statusCode = (HttpStatusCode)int.Parse(httpStatusCode);
+                        if (statusCode != HttpStatusCode.OK)
+                        {
+                            var reasonPhrase = statusMatch.Result("${reasonPhrase}");
+                            throw new ProxyException(string.Format("HTTP: Status code {0}, \"{1}\"", httpStatusCode,
+                                reasonPhrase));
+                        }
+                    }
+
+                    continue;
+                }
+
+                // continue on parsing message headers coming from the server
+                var headerMatch = httpHeaderRe.Match(response);
+                if (headerMatch.Success)
+                {
+                    var fieldName = headerMatch.Result("${fieldName}");
+                    if (fieldName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
+                    {
+                        contentLength = int.Parse(headerMatch.Result("${fieldValue}"));
+                    }
+                    continue;
+                }
+
+                // check if we've reached the CRLF which separates request line and headers from the message body
+                if (response.Length == 0)
+                {
+                    //  read response body if specified
+                    if (contentLength > 0)
+                    {
+                        var contentBody = new byte[contentLength];
+                        SocketRead(socket, contentBody, 0, contentLength);
+                    }
+                    break;
+                }
+            }
+
+            if (statusCode == null)
+            {
+                throw new ProxyException("HTTP response does not contain status line.");
+            }
+
+            return socket;
+        }
+
+        /// <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;
+
+                var b = data[0];
+
+                if (b == Session.LineFeed && buffer.Count > 1 && buffer[buffer.Count - 1] == Session.CarriageReturn)
+                {
+                    // Return line without CR
+                    return encoding.GetString(buffer.ToArray(), 0, buffer.Count - 1);
+                }
+
+                buffer.Add(b);
+            }
+            while (true);
+
+            if (buffer.Count == 0)
+            {
+                return null;
+            }
+
+            return encoding.GetString(buffer.ToArray(), 0, buffer.Count);
+        }
+    }
+}

+ 9 - 0
src/Renci.SshNet/Connection/IConnector.cs

@@ -0,0 +1,9 @@
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    internal interface IConnector
+    {
+        Socket Connect(IConnectionInfo connectionInfo);
+    }
+}

+ 103 - 0
src/Renci.SshNet/Connection/Socks4Connector.cs

@@ -0,0 +1,103 @@
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using System;
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    internal class Socks4Connector : ConnectorBase
+    {
+        public override Socket Connect(IConnectionInfo connectionInfo)
+        {
+            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
+
+            var connectionRequest = CreateSocks4ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port, connectionInfo.ProxyUsername);
+            SocketAbstraction.Send(socket, connectionRequest);
+
+            //  Read null byte
+            if (SocketReadByte(socket) != 0)
+            {
+                throw new ProxyException("SOCKS4: Null is expected.");
+            }
+
+            //  Read response code
+            var code = SocketReadByte(socket);
+
+            switch (code)
+            {
+                case 0x5a:
+                    break;
+                case 0x5b:
+                    throw new ProxyException("SOCKS4: Connection rejected.");
+                case 0x5c:
+                    throw new ProxyException("SOCKS4: Client is not running identd or not reachable from the server.");
+                case 0x5d:
+                    throw new ProxyException("SOCKS4: Client's identd could not confirm the user ID string in the request.");
+                default:
+                    throw new ProxyException("SOCKS4: Not valid response.");
+            }
+
+            var dummyBuffer = new byte[6]; // field 3 (2 bytes) and field 4 (4) should be ignored
+            SocketRead(socket, dummyBuffer, 0, 6);
+
+            return socket;
+        }
+
+        private static byte[] CreateSocks4ConnectionRequest(string hostname, ushort port, string username)
+        {
+            var addressBytes = GetSocks4DestinationAddress(hostname);
+
+            var connectionRequest = new byte
+                [
+                    // SOCKS version number
+                    1 +
+                    // Command code
+                    1 +
+                    // Port number
+                    2 +
+                    // IP address
+                    addressBytes.Length +
+                    // Username
+                    username.Length +
+                    // Null terminator
+                    1
+                ];
+
+            var index = 0;
+
+            // SOCKS version number
+            connectionRequest[index++] = 0x04;
+
+            // Command code
+            connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection
+
+            // Port number
+            Pack.UInt16ToBigEndian(port, connectionRequest, index);
+            index += 2;
+
+            // Address
+            Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length);
+            index += addressBytes.Length;
+
+            connectionRequest[index] = 0x00;
+
+            return connectionRequest;
+        }
+
+        private static byte[] GetSocks4DestinationAddress(string hostname)
+        {
+            var addresses = DnsAbstraction.GetHostAddresses(hostname);
+
+            for (var i = 0; i < addresses.Length; i++)
+            {
+                var address = addresses[i];
+                if (address.AddressFamily == AddressFamily.InterNetwork)
+                {
+                    return address.GetAddressBytes();
+                }
+            }
+
+            throw new ProxyException(string.Format("SOCKS4 only supports IPv4. No such address found for '{0}'.", hostname));
+        }
+    }
+}

+ 231 - 0
src/Renci.SshNet/Connection/Socks5Connector.cs

@@ -0,0 +1,231 @@
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using System;
+using System.Net.Sockets;
+
+namespace Renci.SshNet.Connection
+{
+    internal class Socks5Connector : ConnectorBase
+    {
+        public override Socket Connect(IConnectionInfo connectionInfo)
+        {
+            var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
+
+            var greeting = new byte[]
+                {
+                    // SOCKS version number
+                    0x05,
+                    // Number of supported authentication methods
+                    0x02,
+                    // No authentication
+                    0x00,
+                    // Username/Password authentication
+                    0x02
+                };
+            SocketAbstraction.Send(socket, greeting);
+
+            var socksVersion = SocketReadByte(socket);
+            if (socksVersion != 0x05)
+                throw new ProxyException(string.Format("SOCKS Version '{0}' is not supported.", socksVersion));
+
+            var authenticationMethod = SocketReadByte(socket);
+            switch (authenticationMethod)
+            {
+                case 0x00:
+                    break;
+                case 0x02:
+                    // Create username/password authentication request
+                    var authenticationRequest = CreateSocks5UserNameAndPasswordAuthenticationRequest(connectionInfo.ProxyUsername, connectionInfo.ProxyPassword);
+                    // Send authentication request
+                    SocketAbstraction.Send(socket, authenticationRequest);
+                    // Read authentication result
+                    var authenticationResult = SocketAbstraction.Read(socket, 2, connectionInfo.Timeout);
+
+                    if (authenticationResult[0] != 0x01)
+                        throw new ProxyException("SOCKS5: Server authentication version is not valid.");
+                    if (authenticationResult[1] != 0x00)
+                        throw new ProxyException("SOCKS5: Username/Password authentication failed.");
+                    break;
+                case 0xFF:
+                    throw new ProxyException("SOCKS5: No acceptable authentication methods were offered.");
+            }
+
+            var connectionRequest = CreateSocks5ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port);
+            SocketAbstraction.Send(socket, connectionRequest);
+
+            //  Read Server SOCKS5 version
+            if (SocketReadByte(socket) != 5)
+            {
+                throw new ProxyException("SOCKS5: Version 5 is expected.");
+            }
+
+            //  Read response code
+            var status = SocketReadByte(socket);
+
+            switch (status)
+            {
+                case 0x00:
+                    break;
+                case 0x01:
+                    throw new ProxyException("SOCKS5: General failure.");
+                case 0x02:
+                    throw new ProxyException("SOCKS5: Connection not allowed by ruleset.");
+                case 0x03:
+                    throw new ProxyException("SOCKS5: Network unreachable.");
+                case 0x04:
+                    throw new ProxyException("SOCKS5: Host unreachable.");
+                case 0x05:
+                    throw new ProxyException("SOCKS5: Connection refused by destination host.");
+                case 0x06:
+                    throw new ProxyException("SOCKS5: TTL expired.");
+                case 0x07:
+                    throw new ProxyException("SOCKS5: Command not supported or protocol error.");
+                case 0x08:
+                    throw new ProxyException("SOCKS5: Address type not supported.");
+                default:
+                    throw new ProxyException("SOCKS5: Not valid response.");
+            }
+
+            //  Read reserved byte
+            if (SocketReadByte(socket) != 0)
+            {
+                throw new ProxyException("SOCKS5: 0 byte is expected.");
+            }
+
+            var addressType = SocketReadByte(socket);
+            switch (addressType)
+            {
+                case 0x01:
+                    var ipv4 = new byte[4];
+                    SocketRead(socket, ipv4, 0, 4);
+                    break;
+                case 0x04:
+                    var ipv6 = new byte[16];
+                    SocketRead(socket, ipv6, 0, 16);
+                    break;
+                default:
+                    throw new ProxyException(string.Format("Address type '{0}' is not supported.", addressType));
+            }
+
+            var port = new byte[2];
+
+            //  Read 2 bytes to be ignored
+            SocketRead(socket, port, 0, 2);
+
+            return socket;
+        }
+
+        /// <summary>
+        /// https://tools.ietf.org/html/rfc1929
+        /// </summary>
+        private static byte[] CreateSocks5UserNameAndPasswordAuthenticationRequest(string username, string password)
+        {
+            if (username.Length > byte.MaxValue)
+                throw new ProxyException("Proxy username is too long.");
+            if (password.Length > byte.MaxValue)
+                throw new ProxyException("Proxy password is too long.");
+
+            var authenticationRequest = new byte
+                [
+                    // Version of the negotiation
+                    1 +
+                    // Length of the username
+                    1 +
+                    // Username
+                    username.Length +
+                    // Length of the password
+                    1 +
+                    // Password
+                    password.Length
+                ];
+
+            var index = 0;
+
+            // Version of the negiotiation
+            authenticationRequest[index++] = 0x01;
+
+            // Length of the username
+            authenticationRequest[index++] = (byte)username.Length;
+
+            // Username
+            SshData.Ascii.GetBytes(username, 0, username.Length, authenticationRequest, index);
+            index += username.Length;
+
+            // Length of the password
+            authenticationRequest[index++] = (byte)password.Length;
+
+            // Password
+            SshData.Ascii.GetBytes(password, 0, password.Length, authenticationRequest, index);
+
+            return authenticationRequest;
+        }
+
+        private static byte[] CreateSocks5ConnectionRequest(string hostname, ushort port)
+        {
+            byte addressType;
+            var addressBytes = GetSocks5DestinationAddress(hostname, out addressType);
+
+            var connectionRequest = new byte
+                [
+                    // SOCKS version number
+                    1 +
+                    // Command code
+                    1 +
+                    // Reserved
+                    1 +
+                    // Address type
+                    1 +
+                    // Address
+                    addressBytes.Length +
+                    // Port number
+                    2
+                ];
+
+            var index = 0;
+
+            // SOCKS version number
+            connectionRequest[index++] = 0x05;
+
+            // Command code
+            connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection
+
+            // Reserved
+            connectionRequest[index++] = 0x00;
+
+            // Address type
+            connectionRequest[index++] = addressType;
+
+            // Address
+            Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length);
+            index += addressBytes.Length;
+
+            // Port number
+            Pack.UInt16ToBigEndian(port, connectionRequest, index);
+
+            return connectionRequest;
+        }
+
+        private static byte[] GetSocks5DestinationAddress(string hostname, out byte addressType)
+        {
+            var ip = DnsAbstraction.GetHostAddresses(hostname)[0];
+
+            byte[] address;
+
+            switch (ip.AddressFamily)
+            {
+                case AddressFamily.InterNetwork:
+                    addressType = 0x01; // IPv4
+                    address = ip.GetAddressBytes();
+                    break;
+                case AddressFamily.InterNetworkV6:
+                    addressType = 0x04; // IPv6
+                    address = ip.GetAddressBytes();
+                    break;
+                default:
+                    throw new ProxyException(string.Format("SOCKS5: IP address '{0}' is not supported.", ip));
+            }
+
+            return address;
+        }
+    }
+}

+ 3 - 0
src/Renci.SshNet/ConnectionInfo.cs

@@ -88,6 +88,9 @@ namespace Renci.SshNet
         /// <summary>
         /// Gets connection host.
         /// </summary>
+        /// <value>
+        /// The connection host.
+        /// </value>
         public string Host { get; private set; }
 
         /// <summary>

+ 44 - 0
src/Renci.SshNet/IConnectionInfo.cs

@@ -68,6 +68,50 @@ namespace Renci.SshNet
         /// </value>
         Encoding Encoding { get; }
 
+        /// <summary>
+        /// Gets connection host.
+        /// </summary>
+        /// <value>
+        /// The connection host.
+        /// </value>
+        string Host { get; }
+
+        /// <summary>
+        /// Gets connection port.
+        /// </summary>
+        /// <value>
+        /// The connection port. The default value is 22.
+        /// </value>
+        int Port { get; }
+
+        /// <summary>
+        /// Gets proxy type.
+        /// </summary>
+        /// <value>
+        /// The type of the proxy.
+        /// </value>
+        ProxyTypes ProxyType { get; }
+
+        /// <summary>
+        /// Gets proxy connection host.
+        /// </summary>
+        string ProxyHost { get; }
+
+        /// <summary>
+        /// Gets proxy connection port.
+        /// </summary>
+        int ProxyPort { get; }
+
+        /// <summary>
+        /// Gets proxy connection username.
+        /// </summary>
+        string ProxyUsername { get; }
+
+        /// <summary>
+        /// Gets proxy connection password.
+        /// </summary>
+        string ProxyPassword { get; }
+
         /// <summary>
         /// Gets the number of retry attempts when session channel creation failed.
         /// </summary>

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

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 using Renci.SshNet.Common;
+using Renci.SshNet.Connection;
 using Renci.SshNet.Security;
 using Renci.SshNet.Sftp;
 
@@ -107,5 +108,17 @@ namespace Renci.SshNet
         /// with a shell.
         /// </returns>
         IRemotePathTransformation CreateRemotePathDoubleQuoteTransformation();
+
+        /// <summary>
+        /// Creates an <see cref="IConnector"/> that can be used to establish a connection
+        /// to the server identified by the specified <paramref name="connectionInfo"/>.
+        /// </summary>
+        /// <param name="connectionInfo">A <see cref="IConnectionInfo"/> detailing the server to establish a connection to.</param>
+        /// <returns>
+        /// An <see cref="IConnector"/>.
+        /// </returns>
+        /// <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);
     }
 }

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

@@ -7,6 +7,7 @@ using Renci.SshNet.Messages.Transport;
 using Renci.SshNet.Security;
 using Renci.SshNet.Sftp;
 using Renci.SshNet.Abstractions;
+using Renci.SshNet.Connection;
 
 namespace Renci.SshNet
 {
@@ -188,5 +189,36 @@ namespace Renci.SshNet
         {
             return RemotePathTransformation.DoubleQuote;
         }
+
+        /// <summary>
+        /// Creates an <see cref="IConnector"/> that can be used to establish a connection
+        /// to the server identified by the specified <paramref name="connectionInfo"/>.
+        /// </summary>
+        /// <param name="connectionInfo">A <see cref="IConnectionInfo"/> detailing the server to establish a connection to.</param>
+        /// <returns>
+        /// An <see cref="IConnector"/> that can be used to establish a connection to the
+        /// server identified by the specified <paramref name="connectionInfo"/>.
+        /// </returns>
+        /// <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>
+        public IConnector CreateConnector(IConnectionInfo connectionInfo)
+        {
+            if (connectionInfo == null)
+                throw new ArgumentNullException("connectionInfo");
+
+            switch (connectionInfo.ProxyType)
+            {
+                case ProxyTypes.None:
+                    return new DirectConnector();
+                case ProxyTypes.Socks4:
+                    return new Socks4Connector();
+                case ProxyTypes.Socks5:
+                    return new Socks5Connector();
+                case ProxyTypes.Http:
+                    return new HttpConnector();
+                default:
+                    throw new NotSupportedException(string.Format("ProxyTypes '{0}' is not supported.", connectionInfo.ProxyType));
+            }
+        }
     }
 }

+ 13 - 461
src/Renci.SshNet/Session.cs

@@ -1,6 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Net;
 using System.Net.Sockets;
 using System.Security.Cryptography;
 using System.Text;
@@ -18,6 +16,7 @@ using System.Globalization;
 using System.Linq;
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Security.Cryptography;
+using System.Collections.Generic;
 
 namespace Renci.SshNet
 {
@@ -27,7 +26,7 @@ namespace Renci.SshNet
     public class Session : ISession
     {
         private const byte Null = 0x00;
-        private const byte CarriageReturn = 0x0d;
+        internal const byte CarriageReturn = 0x0d;
         internal const byte LineFeed = 0x0a;
 
         /// <summary>
@@ -52,7 +51,7 @@ namespace Renci.SshNet
         /// <value>
         /// 68536 (64 KB + 3000 bytes).
         /// </value>
-        private const int MaximumSshPacketSize = LocalChannelDataPacketSize + 3000;
+        internal const int MaximumSshPacketSize = LocalChannelDataPacketSize + 3000;
 
         /// <summary>
         /// Holds the initial local window size for the channels.
@@ -587,24 +586,8 @@ namespace Renci.SshNet
                     // Build list of available messages while connecting
                     _sshMessageFactory = new SshMessageFactory();
 
-                    switch (ConnectionInfo.ProxyType)
-                    {
-                        case ProxyTypes.None:
-                            SocketConnect(ConnectionInfo.Host, ConnectionInfo.Port);
-                            break;
-                        case ProxyTypes.Socks4:
-                            SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
-                            ConnectSocks4(_socket, ConnectionInfo);
-                            break;
-                        case ProxyTypes.Socks5:
-                            SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
-                            ConnectSocks5(_socket, ConnectionInfo);
-                            break;
-                        case ProxyTypes.Http:
-                            SocketConnect(ConnectionInfo.ProxyHost, ConnectionInfo.ProxyPort);
-                            ConnectHttp(_socket, ConnectionInfo);
-                            break;
-                    }
+                    _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
@@ -1698,54 +1681,6 @@ namespace Renci.SshNet
 
         #endregion
 
-        /// <summary>
-        /// Establishes a socket connection to the specified host and port.
-        /// </summary>
-        /// <param name="host">The host name of the server to connect to.</param>
-        /// <param name="port">The port to connect to.</param>
-        /// <exception cref="SshOperationTimeoutException">The connection failed to establish within the configured <see cref="Renci.SshNet.ConnectionInfo.Timeout"/>.</exception>
-        /// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
-        private void SocketConnect(string host, int port)
-        {
-            var ipAddress = DnsAbstraction.GetHostAddresses(host)[0];
-            var ep = new IPEndPoint(ipAddress, port);
-
-            DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}:{1}'.", host, port));
-
-            _socket = SocketAbstraction.Connect(ep, ConnectionInfo.Timeout);
-
-            const int socketBufferSize = 2 * MaximumSshPacketSize;
-            _socket.SendBufferSize = socketBufferSize;
-            _socket.ReceiveBufferSize = socketBufferSize;
-        }
-
-        /// <summary>
-        /// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
-        /// </summary>
-        /// <param name="socket">The <see cref="Socket"/> to read from.</param>
-        /// <param name="buffer">An array of type <see cref="byte"/> that is the storage location for the received data.</param>
-        /// <param name="offset">The position in <paramref name="buffer"/> parameter to store the received data.</param>
-        /// <param name="length">The number of bytes to read.</param>
-        /// <returns>
-        /// The number of bytes read.
-        /// </returns>
-        /// <exception cref="SshConnectionException">The socket is closed.</exception>
-        /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
-        /// <exception cref="SocketException">The read failed.</exception>
-        private static int SocketRead(Socket socket, byte[] buffer, int offset, int length)
-        {
-            var bytesRead = SocketAbstraction.Read(socket, buffer, offset, length, InfiniteTimeSpan);
-            if (bytesRead == 0)
-            {
-                // when we're in the disconnecting state (either triggered by client or server), then the
-                // SshConnectionException will interrupt the message listener loop (if not already interrupted)
-                // and the exception itself will be ignored (in RaiseError)
-                throw new SshConnectionException("An established connection was aborted by the server.",
-                                                 DisconnectReason.ConnectionLost);
-            }
-            return bytesRead;
-        }
-
 #if FEATURE_SOCKET_POLL
         /// <summary>
         /// Gets a value indicating whether the socket is connected.
@@ -1896,13 +1831,16 @@ namespace Renci.SshNet
                             {
                                 DiagnosticAbstraction.Log(string.Format("[{0}] Shutting down socket.", ToHex(SessionId)));
 
-                                // interrupt any pending reads; should be done outside of socket read lock as we
-                                // actually want shutdown the socket to make sure blocking reads are interrupted
+                                // Interrupt any pending reads; should be done outside of socket read lock as we
+                                // actually want shutdown the socket to make sure blocking reads are interrupted.
                                 //
-                                // this may result in a SocketException (eg. An existing connection was forcibly
+                                // This may result in a SocketException (eg. An existing connection was forcibly
                                 // closed by the remote host) which we'll log and ignore as it means the socket
-                                // was already shut down
-                                _socket.Shutdown(SocketShutdown.Send);
+                                // was already shut down.
+                                //
+                                // We use SocketShutdown.Both instead of SocketShutdown.Send as a workaround to a
+                                // .NET Core issue on Linux & Mac OS X.
+                                _socket.Shutdown(SocketShutdown.Both);
                             }
                             catch (SocketException ex)
                             {
@@ -2026,392 +1964,6 @@ namespace Renci.SshNet
             }
         }
 
-        private static byte SocketReadByte(Socket socket)
-        {
-            var buffer = new byte[1];
-            SocketRead(socket, buffer, 0, 1);
-            return buffer[0];
-        }
-
-        private static void ConnectSocks4(Socket socket, ConnectionInfo connectionInfo)
-        {
-            var connectionRequest = CreateSocks4ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port, connectionInfo.ProxyUsername);
-            SocketAbstraction.Send(socket, connectionRequest);
-
-            //  Read null byte
-            if (SocketReadByte(socket) != 0)
-            {
-                throw new ProxyException("SOCKS4: Null is expected.");
-            }
-
-            //  Read response code
-            var code = SocketReadByte(socket);
-
-            switch (code)
-            {
-                case 0x5a:
-                    break;
-                case 0x5b:
-                    throw new ProxyException("SOCKS4: Connection rejected.");
-                case 0x5c:
-                    throw new ProxyException("SOCKS4: Client is not running identd or not reachable from the server.");
-                case 0x5d:
-                    throw new ProxyException("SOCKS4: Client's identd could not confirm the user ID string in the request.");
-                default:
-                    throw new ProxyException("SOCKS4: Not valid response.");
-            }
-
-            var dummyBuffer = new byte[6]; // field 3 (2 bytes) and field 4 (4) should be ignored
-            SocketRead(socket, dummyBuffer, 0, 6);
-        }
-
-        private static void ConnectSocks5(Socket socket, ConnectionInfo connectionInfo)
-        {
-            var greeting = new byte[]
-                {
-                    // SOCKS version number
-                    0x05,
-                    // Number of supported authentication methods
-                    0x02,
-                    // No authentication
-                    0x00,
-                    // Username/Password authentication
-                    0x02
-                };
-            SocketAbstraction.Send(socket, greeting);
-
-            var socksVersion = SocketReadByte(socket);
-            if (socksVersion != 0x05)
-                throw new ProxyException(string.Format("SOCKS Version '{0}' is not supported.", socksVersion));
-
-            var authenticationMethod = SocketReadByte(socket);
-            switch (authenticationMethod)
-            {
-                case 0x00:
-                    break;
-                case 0x02:
-                    // Create username/password authentication request
-                    var authenticationRequest = CreateSocks5UserNameAndPasswordAuthenticationRequest(connectionInfo.ProxyUsername, connectionInfo.ProxyPassword);
-                    // Send authentication request
-                    SocketAbstraction.Send(socket, authenticationRequest);
-                    // Read authentication result
-                    var authenticationResult = SocketAbstraction.Read(socket, 2, connectionInfo.Timeout);
-
-                    if (authenticationResult[0] != 0x01)
-                        throw new ProxyException("SOCKS5: Server authentication version is not valid.");
-                    if (authenticationResult[1] != 0x00)
-                        throw new ProxyException("SOCKS5: Username/Password authentication failed.");
-                    break;
-                case 0xFF:
-                    throw new ProxyException("SOCKS5: No acceptable authentication methods were offered.");
-            }
-
-            var connectionRequest = CreateSocks5ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port);
-            SocketAbstraction.Send(socket, connectionRequest);
-
-            //  Read Server SOCKS5 version
-            if (SocketReadByte(socket) != 5)
-            {
-                throw new ProxyException("SOCKS5: Version 5 is expected.");
-            }
-
-            //  Read response code
-            var status = SocketReadByte(socket);
-
-            switch (status)
-            {
-                case 0x00:
-                    break;
-                case 0x01:
-                    throw new ProxyException("SOCKS5: General failure.");
-                case 0x02:
-                    throw new ProxyException("SOCKS5: Connection not allowed by ruleset.");
-                case 0x03:
-                    throw new ProxyException("SOCKS5: Network unreachable.");
-                case 0x04:
-                    throw new ProxyException("SOCKS5: Host unreachable.");
-                case 0x05:
-                    throw new ProxyException("SOCKS5: Connection refused by destination host.");
-                case 0x06:
-                    throw new ProxyException("SOCKS5: TTL expired.");
-                case 0x07:
-                    throw new ProxyException("SOCKS5: Command not supported or protocol error.");
-                case 0x08:
-                    throw new ProxyException("SOCKS5: Address type not supported.");
-                default:
-                    throw new ProxyException("SOCKS5: Not valid response.");
-            }
-
-            //  Read reserved byte
-            if (SocketReadByte(socket) != 0)
-            {
-                throw new ProxyException("SOCKS5: 0 byte is expected.");
-            }
-
-            var addressType = SocketReadByte(socket);
-            switch (addressType)
-            {
-                case 0x01:
-                    var ipv4 = new byte[4];
-                    SocketRead(socket, ipv4, 0, 4);
-                    break;
-                case 0x04:
-                    var ipv6 = new byte[16];
-                    SocketRead(socket, ipv6, 0, 16);
-                    break;
-                default:
-                    throw new ProxyException(string.Format("Address type '{0}' is not supported.", addressType));
-            }
-
-            var port = new byte[2];
-
-            //  Read 2 bytes to be ignored
-            SocketRead(socket, port, 0, 2);
-        }
-
-        /// <summary>
-        /// https://tools.ietf.org/html/rfc1929
-        /// </summary>
-        private static byte[] CreateSocks5UserNameAndPasswordAuthenticationRequest(string username, string password)
-        {
-            if (username.Length > byte.MaxValue)
-                throw new ProxyException("Proxy username is too long.");
-            if (password.Length > byte.MaxValue)
-                throw new ProxyException("Proxy password is too long.");
-
-            var authenticationRequest = new byte
-                [
-                    // Version of the negotiation
-                    1 +
-                    // Length of the username
-                    1 +
-                    // Username
-                    username.Length +
-                    // Length of the password
-                    1 +
-                    // Password
-                    password.Length
-                ];
-
-            var index = 0;
-
-            // Version of the negiotiation
-            authenticationRequest[index++] = 0x01;
-
-            // Length of the username
-            authenticationRequest[index++] = (byte) username.Length;
-
-            // Username
-            SshData.Ascii.GetBytes(username, 0, username.Length, authenticationRequest, index);
-            index += username.Length;
-
-            // Length of the password
-            authenticationRequest[index++] = (byte) password.Length;
-
-            // Password
-            SshData.Ascii.GetBytes(password, 0, password.Length, authenticationRequest, index);
-
-            return authenticationRequest;
-        }
-
-        private static byte[] CreateSocks4ConnectionRequest(string hostname, ushort port, string username)
-        {
-            var addressBytes = GetSocks4DestinationAddress(hostname);
-
-            var connectionRequest = new byte
-                [
-                    // SOCKS version number
-                    1 +
-                    // Command code
-                    1 +
-                    // Port number
-                    2 +
-                    // IP address
-                    addressBytes.Length +
-                    // Username
-                    username.Length +
-                    // Null terminator
-                    1
-                ];
-
-            var index = 0;
-
-            // SOCKS version number
-            connectionRequest[index++] = 0x04;
-
-            // Command code
-            connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection
-
-            // Port number
-            Pack.UInt16ToBigEndian(port, connectionRequest, index);
-            index += 2;
-
-            // Address
-            Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length);
-            index += addressBytes.Length;
-
-            connectionRequest[index] = 0x00;
-
-            return connectionRequest;
-        }
-
-        private static byte[] CreateSocks5ConnectionRequest(string hostname, ushort port)
-        {
-            byte addressType;
-            var addressBytes = GetSocks5DestinationAddress(hostname, out addressType);
-
-            var connectionRequest = new byte
-                [
-                    // SOCKS version number
-                    1 +
-                    // Command code
-                    1 +
-                    // Reserved
-                    1 +
-                    // Address type
-                    1 +
-                    // Address
-                    addressBytes.Length +
-                    // Port number
-                    2
-                ];
-
-            var index = 0;
-
-            // SOCKS version number
-            connectionRequest[index++] = 0x05;
-
-            // Command code
-            connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection
-
-            // Reserved
-            connectionRequest[index++] = 0x00;
-
-            // Address type
-            connectionRequest[index++] = addressType;
-            
-            // Address
-            Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length);
-            index += addressBytes.Length;
-
-            // Port number
-            Pack.UInt16ToBigEndian(port, connectionRequest, index);
-
-            return connectionRequest;
-        }
-
-        private static byte[] GetSocks4DestinationAddress(string hostname)
-        {
-            var addresses = DnsAbstraction.GetHostAddresses(hostname);
-
-            for (var i = 0; i < addresses.Length; i++)
-            {
-                var address = addresses[i];
-                if (address.AddressFamily == AddressFamily.InterNetwork)
-                    return address.GetAddressBytes();
-            }
-
-            throw new ProxyException(string.Format("SOCKS4 only supports IPv4. No such address found for '{0}'.", hostname));
-        }
-
-        private static byte[] GetSocks5DestinationAddress(string hostname, out byte addressType)
-        {
-            var ip = DnsAbstraction.GetHostAddresses(hostname)[0];
-
-            byte[] address;
-
-            switch (ip.AddressFamily)
-            {
-                case AddressFamily.InterNetwork:
-                    addressType = 0x01; // IPv4
-                    address = ip.GetAddressBytes();
-                    break;
-                case AddressFamily.InterNetworkV6:
-                    addressType = 0x04; // IPv6
-                    address = ip.GetAddressBytes();
-                    break;
-                default:
-                    throw new ProxyException(string.Format("SOCKS5: IP address '{0}' is not supported.", ip));
-            }
-
-            return address;
-        }
-
-        private static void ConnectHttp(Socket socket, ConnectionInfo connectionInfo)
-        {
-            var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
-            var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");
-
-            SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format("CONNECT {0}:{1} HTTP/1.0\r\n", connectionInfo.Host, connectionInfo.Port)));
-
-            //  Sent proxy authorization is specified
-            if (!string.IsNullOrEmpty(connectionInfo.ProxyUsername))
-            {
-                var authorization = string.Format("Proxy-Authorization: Basic {0}\r\n",
-                                                  Convert.ToBase64String(SshData.Ascii.GetBytes(string.Format("{0}:{1}", connectionInfo.ProxyUsername, connectionInfo.ProxyPassword)))
-                                                  );
-                SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(authorization));
-            }
-
-            SocketAbstraction.Send(socket, SshData.Ascii.GetBytes("\r\n"));
-
-            HttpStatusCode? statusCode = null;
-            var contentLength = 0;
-
-            while (true)
-            {
-                var response = SocketReadLine(socket, connectionInfo.Timeout);
-                if (response == null)
-                    // server shut down socket
-                    break;
-
-                if (statusCode == null)
-                {
-                    var statusMatch = httpResponseRe.Match(response);
-                    if (statusMatch.Success)
-                    {
-                        var httpStatusCode = statusMatch.Result("${statusCode}");
-                        statusCode = (HttpStatusCode) int.Parse(httpStatusCode);
-                        if (statusCode != HttpStatusCode.OK)
-                        {
-                            var reasonPhrase = statusMatch.Result("${reasonPhrase}");
-                            throw new ProxyException(string.Format("HTTP: Status code {0}, \"{1}\"", httpStatusCode,
-                                reasonPhrase));
-                        }
-                    }
-
-                    continue;
-                }
-
-                // continue on parsing message headers coming from the server
-                var headerMatch = httpHeaderRe.Match(response);
-                if (headerMatch.Success)
-                {
-                    var fieldName = headerMatch.Result("${fieldName}");
-                    if (fieldName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
-                    {
-                        contentLength = int.Parse(headerMatch.Result("${fieldValue}"));
-                    }
-                    continue;
-                }
-
-                // check if we've reached the CRLF which separates request line and headers from the message body
-                if (response.Length == 0)
-                {
-                    //  read response body if specified
-                    if (contentLength > 0)
-                    {
-                        var contentBody = new byte[contentLength];
-                        SocketRead(socket, contentBody, 0, contentLength);
-                    }
-                    break;
-                }
-            }
-
-            if (statusCode == null)
-                throw new ProxyException("HTTP response does not contain status line.");
-        }
-
         /// <summary>
         /// Raises the <see cref="ErrorOccured"/> event.
         /// </summary>