Skip to content

Commit dc9c637

Browse files
committed
Refactor how connection is established to server.
1 parent cefdc20 commit dc9c637

File tree

11 files changed

+671
-461
lines changed

11 files changed

+671
-461
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Renci.SshNet.Abstractions;
2+
using Renci.SshNet.Common;
3+
using Renci.SshNet.Messages.Transport;
4+
using System;
5+
using System.Net;
6+
using System.Net.Sockets;
7+
8+
namespace Renci.SshNet.Connection
9+
{
10+
internal abstract class ConnectorBase : IConnector
11+
{
12+
public abstract Socket Connect(IConnectionInfo connectionInfo);
13+
14+
/// <summary>
15+
/// Establishes a socket connection to the specified host and port.
16+
/// </summary>
17+
/// <param name="host">The host name of the server to connect to.</param>
18+
/// <param name="port">The port to connect to.</param>
19+
/// <param name="timeout">The maximum time to wait for the connection to be established.</param>
20+
/// <exception cref="SshOperationTimeoutException">The connection failed to establish within the configured <see cref="ConnectionInfo.Timeout"/>.</exception>
21+
/// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
22+
protected Socket SocketConnect(string host, int port, TimeSpan timeout)
23+
{
24+
var ipAddress = DnsAbstraction.GetHostAddresses(host)[0];
25+
var ep = new IPEndPoint(ipAddress, port);
26+
27+
DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}:{1}'.", host, port));
28+
29+
var socket = SocketAbstraction.Connect(ep, timeout);
30+
31+
const int socketBufferSize = 2 * Session.MaximumSshPacketSize;
32+
socket.SendBufferSize = socketBufferSize;
33+
socket.ReceiveBufferSize = socketBufferSize;
34+
return socket;
35+
}
36+
37+
protected static byte SocketReadByte(Socket socket)
38+
{
39+
var buffer = new byte[1];
40+
SocketRead(socket, buffer, 0, 1);
41+
return buffer[0];
42+
}
43+
44+
/// <summary>
45+
/// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
46+
/// </summary>
47+
/// <param name="socket">The <see cref="Socket"/> to read from.</param>
48+
/// <param name="buffer">An array of type <see cref="byte"/> that is the storage location for the received data.</param>
49+
/// <param name="offset">The position in <paramref name="buffer"/> parameter to store the received data.</param>
50+
/// <param name="length">The number of bytes to read.</param>
51+
/// <returns>
52+
/// The number of bytes read.
53+
/// </returns>
54+
/// <exception cref="SshConnectionException">The socket is closed.</exception>
55+
/// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
56+
/// <exception cref="SocketException">The read failed.</exception>
57+
protected static int SocketRead(Socket socket, byte[] buffer, int offset, int length)
58+
{
59+
var bytesRead = SocketAbstraction.Read(socket, buffer, offset, length, Session.InfiniteTimeSpan);
60+
if (bytesRead == 0)
61+
{
62+
// when we're in the disconnecting state (either triggered by client or server), then the
63+
// SshConnectionException will interrupt the message listener loop (if not already interrupted)
64+
// and the exception itself will be ignored (in RaiseError)
65+
throw new SshConnectionException("An established connection was aborted by the server.",
66+
DisconnectReason.ConnectionLost);
67+
}
68+
return bytesRead;
69+
}
70+
}
71+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Net.Sockets;
2+
3+
namespace Renci.SshNet.Connection
4+
{
5+
internal class DirectConnector : ConnectorBase
6+
{
7+
public override Socket Connect(IConnectionInfo connectionInfo)
8+
{
9+
return SocketConnect(connectionInfo.Host, connectionInfo.Port, connectionInfo.Timeout);
10+
}
11+
}
12+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Renci.SshNet.Abstractions;
2+
using Renci.SshNet.Common;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net;
6+
using System.Net.Sockets;
7+
using System.Text.RegularExpressions;
8+
9+
namespace Renci.SshNet.Connection
10+
{
11+
internal class HttpConnector : ConnectorBase
12+
{
13+
public override Socket Connect(IConnectionInfo connectionInfo)
14+
{
15+
var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
16+
17+
var httpResponseRe = new Regex(@"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$");
18+
var httpHeaderRe = new Regex(@"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?");
19+
20+
SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format("CONNECT {0}:{1} HTTP/1.0\r\n", connectionInfo.Host, connectionInfo.Port)));
21+
22+
// Sent proxy authorization is specified
23+
if (!string.IsNullOrEmpty(connectionInfo.ProxyUsername))
24+
{
25+
var authorization = string.Format("Proxy-Authorization: Basic {0}\r\n",
26+
Convert.ToBase64String(SshData.Ascii.GetBytes(string.Format("{0}:{1}", connectionInfo.ProxyUsername, connectionInfo.ProxyPassword))));
27+
SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(authorization));
28+
}
29+
30+
SocketAbstraction.Send(socket, SshData.Ascii.GetBytes("\r\n"));
31+
32+
HttpStatusCode? statusCode = null;
33+
var contentLength = 0;
34+
35+
while (true)
36+
{
37+
var response = SocketReadLine(socket, connectionInfo.Timeout);
38+
if (response == null)
39+
{
40+
// server shut down socket
41+
break;
42+
}
43+
44+
if (statusCode == null)
45+
{
46+
var statusMatch = httpResponseRe.Match(response);
47+
if (statusMatch.Success)
48+
{
49+
var httpStatusCode = statusMatch.Result("${statusCode}");
50+
statusCode = (HttpStatusCode)int.Parse(httpStatusCode);
51+
if (statusCode != HttpStatusCode.OK)
52+
{
53+
var reasonPhrase = statusMatch.Result("${reasonPhrase}");
54+
throw new ProxyException(string.Format("HTTP: Status code {0}, \"{1}\"", httpStatusCode,
55+
reasonPhrase));
56+
}
57+
}
58+
59+
continue;
60+
}
61+
62+
// continue on parsing message headers coming from the server
63+
var headerMatch = httpHeaderRe.Match(response);
64+
if (headerMatch.Success)
65+
{
66+
var fieldName = headerMatch.Result("${fieldName}");
67+
if (fieldName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
68+
{
69+
contentLength = int.Parse(headerMatch.Result("${fieldValue}"));
70+
}
71+
continue;
72+
}
73+
74+
// check if we've reached the CRLF which separates request line and headers from the message body
75+
if (response.Length == 0)
76+
{
77+
// read response body if specified
78+
if (contentLength > 0)
79+
{
80+
var contentBody = new byte[contentLength];
81+
SocketRead(socket, contentBody, 0, contentLength);
82+
}
83+
break;
84+
}
85+
}
86+
87+
if (statusCode == null)
88+
{
89+
throw new ProxyException("HTTP response does not contain status line.");
90+
}
91+
92+
return socket;
93+
}
94+
95+
/// <summary>
96+
/// Performs a blocking read on the socket until a line is read.
97+
/// </summary>
98+
/// <param name="socket">The <see cref="Socket"/> to read from.</param>
99+
/// <param name="timeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param>
100+
/// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
101+
/// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
102+
/// <returns>
103+
/// The line read from the socket, or <c>null</c> when the remote server has shutdown and all data has been received.
104+
/// </returns>
105+
private static string SocketReadLine(Socket socket, TimeSpan timeout)
106+
{
107+
var encoding = SshData.Ascii;
108+
var buffer = new List<byte>();
109+
var data = new byte[1];
110+
111+
// read data one byte at a time to find end of line and leave any unhandled information in the buffer
112+
// to be processed by subsequent invocations
113+
do
114+
{
115+
var bytesRead = SocketAbstraction.Read(socket, data, 0, data.Length, timeout);
116+
if (bytesRead == 0)
117+
// the remote server shut down the socket
118+
break;
119+
120+
var b = data[0];
121+
122+
if (b == Session.LineFeed && buffer.Count > 1 && buffer[buffer.Count - 1] == Session.CarriageReturn)
123+
{
124+
// Return line without CR
125+
return encoding.GetString(buffer.ToArray(), 0, buffer.Count - 1);
126+
}
127+
128+
buffer.Add(b);
129+
}
130+
while (true);
131+
132+
if (buffer.Count == 0)
133+
{
134+
return null;
135+
}
136+
137+
return encoding.GetString(buffer.ToArray(), 0, buffer.Count);
138+
}
139+
}
140+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Net.Sockets;
2+
3+
namespace Renci.SshNet.Connection
4+
{
5+
internal interface IConnector
6+
{
7+
Socket Connect(IConnectionInfo connectionInfo);
8+
}
9+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Renci.SshNet.Abstractions;
2+
using Renci.SshNet.Common;
3+
using System;
4+
using System.Net.Sockets;
5+
6+
namespace Renci.SshNet.Connection
7+
{
8+
internal class Socks4Connector : ConnectorBase
9+
{
10+
public override Socket Connect(IConnectionInfo connectionInfo)
11+
{
12+
var socket = SocketConnect(connectionInfo.ProxyHost, connectionInfo.ProxyPort, connectionInfo.Timeout);
13+
14+
var connectionRequest = CreateSocks4ConnectionRequest(connectionInfo.Host, (ushort)connectionInfo.Port, connectionInfo.ProxyUsername);
15+
SocketAbstraction.Send(socket, connectionRequest);
16+
17+
// Read null byte
18+
if (SocketReadByte(socket) != 0)
19+
{
20+
throw new ProxyException("SOCKS4: Null is expected.");
21+
}
22+
23+
// Read response code
24+
var code = SocketReadByte(socket);
25+
26+
switch (code)
27+
{
28+
case 0x5a:
29+
break;
30+
case 0x5b:
31+
throw new ProxyException("SOCKS4: Connection rejected.");
32+
case 0x5c:
33+
throw new ProxyException("SOCKS4: Client is not running identd or not reachable from the server.");
34+
case 0x5d:
35+
throw new ProxyException("SOCKS4: Client's identd could not confirm the user ID string in the request.");
36+
default:
37+
throw new ProxyException("SOCKS4: Not valid response.");
38+
}
39+
40+
var dummyBuffer = new byte[6]; // field 3 (2 bytes) and field 4 (4) should be ignored
41+
SocketRead(socket, dummyBuffer, 0, 6);
42+
43+
return socket;
44+
}
45+
46+
private static byte[] CreateSocks4ConnectionRequest(string hostname, ushort port, string username)
47+
{
48+
var addressBytes = GetSocks4DestinationAddress(hostname);
49+
50+
var connectionRequest = new byte
51+
[
52+
// SOCKS version number
53+
1 +
54+
// Command code
55+
1 +
56+
// Port number
57+
2 +
58+
// IP address
59+
addressBytes.Length +
60+
// Username
61+
username.Length +
62+
// Null terminator
63+
1
64+
];
65+
66+
var index = 0;
67+
68+
// SOCKS version number
69+
connectionRequest[index++] = 0x04;
70+
71+
// Command code
72+
connectionRequest[index++] = 0x01; // establish a TCP/IP stream connection
73+
74+
// Port number
75+
Pack.UInt16ToBigEndian(port, connectionRequest, index);
76+
index += 2;
77+
78+
// Address
79+
Buffer.BlockCopy(addressBytes, 0, connectionRequest, index, addressBytes.Length);
80+
index += addressBytes.Length;
81+
82+
connectionRequest[index] = 0x00;
83+
84+
return connectionRequest;
85+
}
86+
87+
private static byte[] GetSocks4DestinationAddress(string hostname)
88+
{
89+
var addresses = DnsAbstraction.GetHostAddresses(hostname);
90+
91+
for (var i = 0; i < addresses.Length; i++)
92+
{
93+
var address = addresses[i];
94+
if (address.AddressFamily == AddressFamily.InterNetwork)
95+
{
96+
return address.GetAddressBytes();
97+
}
98+
}
99+
100+
throw new ProxyException(string.Format("SOCKS4 only supports IPv4. No such address found for '{0}'.", hostname));
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)