Skip to content

Commit feaa948

Browse files
committed
Added base implementation
1 parent 567bfa7 commit feaa948

File tree

3 files changed

+274
-6
lines changed

3 files changed

+274
-6
lines changed

src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public class TcpStreamSettings
3333
private readonly int _sendBufferSize;
3434
private readonly Action<Socket> _socketConfigurator;
3535
private readonly TimeSpan? _writeTimeout;
36+
private readonly string _proxyHost;
37+
private readonly int? _proxyPort;
38+
private readonly string _proxyUsername;
39+
private readonly string _proxyPassword;
3640

3741
// constructors
3842
/// <summary>
@@ -45,14 +49,22 @@ public class TcpStreamSettings
4549
/// <param name="sendBufferSize">Size of the send buffer.</param>
4650
/// <param name="socketConfigurator">The socket configurator.</param>
4751
/// <param name="writeTimeout">The write timeout.</param>
52+
/// <param name="proxyHost">//TODO</param>
53+
/// <param name="proxyPort"></param>
54+
/// <param name="proxyUsername"></param>
55+
/// <param name="proxyPassword"></param>
4856
public TcpStreamSettings(
4957
Optional<AddressFamily> addressFamily = default(Optional<AddressFamily>),
5058
Optional<TimeSpan> connectTimeout = default(Optional<TimeSpan>),
5159
Optional<TimeSpan?> readTimeout = default(Optional<TimeSpan?>),
5260
Optional<int> receiveBufferSize = default(Optional<int>),
5361
Optional<int> sendBufferSize = default(Optional<int>),
5462
Optional<Action<Socket>> socketConfigurator = default(Optional<Action<Socket>>),
55-
Optional<TimeSpan?> writeTimeout = default(Optional<TimeSpan?>))
63+
Optional<TimeSpan?> writeTimeout = default(Optional<TimeSpan?>),
64+
Optional<string> proxyHost = default(Optional<string>),
65+
Optional<int?> proxyPort = default(Optional<int?>),
66+
Optional<string> proxyUsername = default(Optional<string>),
67+
Optional<string> proxyPassword = default(Optional<string>))
5668
{
5769
_addressFamily = addressFamily.WithDefault(AddressFamily.InterNetwork);
5870
_connectTimeout = Ensure.IsInfiniteOrGreaterThanOrEqualToZero(connectTimeout.WithDefault(Timeout.InfiniteTimeSpan), "connectTimeout");
@@ -61,6 +73,10 @@ public TcpStreamSettings(
6173
_sendBufferSize = Ensure.IsGreaterThanZero(sendBufferSize.WithDefault(64 * 1024), "sendBufferSize");
6274
_socketConfigurator = socketConfigurator.WithDefault(null);
6375
_writeTimeout = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(writeTimeout.WithDefault(null), "writeTimeout");
76+
_proxyHost = proxyHost.WithDefault(null);
77+
_proxyPort = proxyPort.WithDefault(null);
78+
_proxyUsername = proxyUsername.WithDefault(null);
79+
_proxyPassword = proxyPassword.WithDefault(null);
6480
}
6581

6682
internal TcpStreamSettings(TcpStreamSettings other)
@@ -72,6 +88,10 @@ internal TcpStreamSettings(TcpStreamSettings other)
7288
_sendBufferSize = other.SendBufferSize;
7389
_socketConfigurator = other.SocketConfigurator;
7490
_writeTimeout = other.WriteTimeout;
91+
_proxyHost = other._proxyHost;
92+
_proxyPort = other._proxyPort;
93+
_proxyUsername = other._proxyUsername;
94+
_proxyPassword = other._proxyPassword;
7595
}
7696

7797
// properties
@@ -152,6 +172,28 @@ public TimeSpan? WriteTimeout
152172
get { return _writeTimeout; }
153173
}
154174

175+
//TODO Add xml docs
176+
/// <summary>
177+
///
178+
/// </summary>
179+
public string ProxyHost => _proxyHost;
180+
/// <summary>
181+
///
182+
/// </summary>
183+
public int? ProxyPort => _proxyPort;
184+
/// <summary>
185+
///
186+
/// </summary>
187+
public string ProxyUsername => _proxyUsername;
188+
/// <summary>
189+
///
190+
/// </summary>
191+
public string ProxyPassword => _proxyPassword;
192+
193+
//TODO We can decide to remove this
194+
internal bool UseProxy => !string.IsNullOrEmpty(_proxyHost) && _proxyPort.HasValue;
195+
196+
155197
// methods
156198
/// <summary>
157199
/// Returns a new TcpStreamSettings instance with some settings changed.
@@ -172,6 +214,7 @@ public TcpStreamSettings With(
172214
Optional<int> sendBufferSize = default(Optional<int>),
173215
Optional<Action<Socket>> socketConfigurator = default(Optional<Action<Socket>>),
174216
Optional<TimeSpan?> writeTimeout = default(Optional<TimeSpan?>))
217+
//TODO Need to add proxy settings
175218
{
176219
return new TcpStreamSettings(
177220
addressFamily: addressFamily.WithDefault(_addressFamily),
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Buffers;
18+
using System.Buffers.Binary;
19+
using System.IO;
20+
using System.Net;
21+
using System.Net.Sockets;
22+
using System.Text;
23+
using System.Threading;
24+
using MongoDB.Driver.GridFS;
25+
26+
namespace MongoDB.Driver.Core.Connections
27+
{
28+
internal static class Socks5Helper
29+
{
30+
// Schemas for requests/responses are taken from the following RFCs:
31+
// SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928
32+
// Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929
33+
34+
private const byte ProtocolVersion5 = 0x05;
35+
private const byte SubnegotiationVersion = 0x01;
36+
private const byte CmdConnect = 0x01;
37+
private const byte MethodNoAuth = 0x00;
38+
private const byte MethodUsernamePassword = 0x02;
39+
private const byte AddressTypeIPv4 = 0x01;
40+
private const byte AddressTypeDomain = 0x03;
41+
private const byte AddressTypeIPv6 = 0x04;
42+
private const byte Socks5Success = 0x00;
43+
44+
private const int BufferSize = 512;
45+
46+
public static void PerformSocks5Handshake(Stream stream, string targetHost, int targetPort, string proxyUsername, string proxyPassword, CancellationToken cancellationToken)
47+
{
48+
var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
49+
try
50+
{
51+
var useAuth = !string.IsNullOrEmpty(proxyUsername) && !string.IsNullOrEmpty(proxyPassword);
52+
53+
// Greeting request
54+
// +----+----------+----------+
55+
// |VER | NMETHODS | METHODS |
56+
// +----+----------+----------+
57+
// | 1 | 1 | 1 to 255 |
58+
// +----+----------+----------+
59+
buffer[0] = ProtocolVersion5;
60+
61+
if (!useAuth)
62+
{
63+
buffer[1] = 1;
64+
buffer[2] = MethodNoAuth;
65+
}
66+
else
67+
{
68+
buffer[1] = 2;
69+
buffer[2] = MethodNoAuth;
70+
buffer[3] = MethodUsernamePassword;
71+
}
72+
73+
stream.Write(buffer, 0, useAuth ? 4 : 3);
74+
stream.Flush();
75+
76+
// Greeting response
77+
// +----+--------+
78+
// |VER | METHOD |
79+
// +----+--------+
80+
// | 1 | 1 |
81+
// +----+--------+
82+
83+
stream.ReadBytes(buffer, 0,2, cancellationToken);
84+
85+
VerifyProtocolVersion(buffer[0]);
86+
87+
var method = buffer[1];
88+
if (method == MethodUsernamePassword)
89+
{
90+
if (!useAuth)
91+
{
92+
//We should not reach here
93+
throw new IOException("SOCKS5 proxy requires authentication, but no credentials were provided.");
94+
}
95+
96+
// Authentication request
97+
// +----+------+----------+------+----------+
98+
// |VER | ULEN | UNAME | PLEN | PASSWD |
99+
// +----+------+----------+------+----------+
100+
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
101+
// +----+------+----------+------+----------+
102+
buffer[0] = SubnegotiationVersion;
103+
var usernameLength = EncodeString(proxyUsername, buffer.AsSpan(2), nameof(proxyUsername));
104+
buffer[1] = usernameLength;
105+
var passwordLength = EncodeString(proxyPassword, buffer.AsSpan(3 + usernameLength), nameof(proxyPassword));
106+
buffer[2 + usernameLength] = passwordLength;
107+
108+
var authLength = 3 + usernameLength + passwordLength;
109+
stream.Write(buffer, 0, authLength);
110+
stream.Flush();
111+
112+
// Authentication response
113+
// +----+--------+
114+
// |VER | STATUS |
115+
// +----+--------+
116+
// | 1 | 1 |
117+
// +----+--------+
118+
stream.ReadBytes(buffer, 0,2, cancellationToken);
119+
if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5Success)
120+
{
121+
throw new IOException("SOCKS5 authentication failed.");
122+
}
123+
}
124+
else if (method != MethodNoAuth)
125+
{
126+
throw new IOException("SOCKS5 proxy requires unsupported authentication method.");
127+
}
128+
129+
// Connect request
130+
// +----+-----+-------+------+----------+----------+
131+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
132+
// +----+-----+-------+------+----------+----------+
133+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
134+
// +----+-----+-------+------+----------+----------+
135+
buffer[0] = ProtocolVersion5;
136+
buffer[1] = CmdConnect;
137+
buffer[2] = 0x00;
138+
var addressLength = 0;
139+
140+
if (IPAddress.TryParse(targetHost, out var ip))
141+
{
142+
switch (ip.AddressFamily)
143+
{
144+
case AddressFamily.InterNetwork:
145+
buffer[3] = AddressTypeIPv4;
146+
ip.TryWriteBytes(buffer.AsSpan(4), out _);
147+
addressLength = 4;
148+
break;
149+
case AddressFamily.InterNetworkV6:
150+
buffer[3] = AddressTypeIPv6;
151+
ip.TryWriteBytes(buffer.AsSpan(4), out _);
152+
addressLength = 16;
153+
break;
154+
default:
155+
throw new IOException("Invalid target host address family. Only IPv4 and IPv6 are supported.");
156+
}
157+
}
158+
else
159+
{
160+
buffer[3] = AddressTypeDomain;
161+
var hostLength = EncodeString(targetHost, buffer.AsSpan(5), nameof(targetHost));
162+
buffer[4] = hostLength;
163+
addressLength = hostLength + 1;
164+
}
165+
166+
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)targetPort);
167+
168+
stream.Write(buffer, 0, addressLength + 6);
169+
stream.Flush();
170+
171+
// Connect response
172+
// +----+-----+-------+------+----------+----------+
173+
// |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT |
174+
// +----+-----+-------+------+----------+----------+
175+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
176+
// +----+-----+-------+------+----------+----------+
177+
stream.ReadBytes(buffer, 0,5, cancellationToken);
178+
VerifyProtocolVersion(buffer[0]);
179+
if (buffer[1] != Socks5Success)
180+
{
181+
throw new IOException($"SOCKS5 connect failed with code 0x{buffer[1]:X2}");
182+
}
183+
184+
var skip = buffer[3] switch
185+
{
186+
AddressTypeIPv4 => 4 + 2,
187+
AddressTypeIPv6 => 16 + 2,
188+
AddressTypeDomain => buffer[4] + 1 + 2,
189+
_ => throw new IOException("Unknown address type in SOCKS5 reply.")
190+
};
191+
192+
stream.ReadBytes(buffer, 0, skip, cancellationToken);
193+
// Address and port in response are ignored
194+
}
195+
finally
196+
{
197+
ArrayPool<byte>.Shared.Return(buffer);
198+
}
199+
}
200+
201+
private static void VerifyProtocolVersion(byte version)
202+
{
203+
if (version != ProtocolVersion5)
204+
{
205+
throw new IOException("Invalid SOCKS version in method selection response.");
206+
}
207+
}
208+
209+
private static byte EncodeString(ReadOnlySpan<char> chars, Span<byte> buffer, string parameterName)
210+
{
211+
try
212+
{
213+
return checked((byte)Encoding.UTF8.GetBytes(chars, buffer));
214+
}
215+
catch
216+
{
217+
throw new IOException($"The {parameterName} could not be encoded as UTF-8.");
218+
}
219+
}
220+
}
221+
}

src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,23 @@ public TcpStreamFactory(TcpStreamSettings settings)
4848
// methods
4949
public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToken)
5050
{
51+
EndPoint actualEndPoint;
52+
53+
actualEndPoint = _settings.UseProxy ? new DnsEndPoint(_settings.ProxyHost, _settings.ProxyPort.Value) : endPoint;
54+
5155
#if NET472
52-
var socket = CreateSocket(endPoint);
53-
Connect(socket, endPoint, cancellationToken);
56+
var socket = CreateSocket(actualEndPoint);
57+
Connect(socket, actualEndPoint, cancellationToken);
5458
return CreateNetworkStream(socket);
5559
#else
56-
var resolved = ResolveEndPoints(endPoint);
60+
var resolved = ResolveEndPoints(actualEndPoint);
5761
for (int i = 0; i < resolved.Length; i++)
5862
{
5963
try
6064
{
6165
var socket = CreateSocket(resolved[i]);
6266
Connect(socket, resolved[i], cancellationToken);
63-
return CreateNetworkStream(socket);
67+
var stream = CreateNetworkStream(socket);
6468
}
6569
catch
6670
{
@@ -74,7 +78,7 @@ public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToke
7478
}
7579

7680
// we should never get here...
77-
throw new InvalidOperationException("Unabled to resolve endpoint.");
81+
throw new InvalidOperationException("Unable to resolve endpoint.");
7882
#endif
7983
}
8084

0 commit comments

Comments
 (0)