From a09ba4f504f330bb2b033ea2bf9d8a0cb9a292cf Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:00:01 +0200 Subject: [PATCH 1/9] CSHARP-734: SOCKS5 Proxy Support --- .../Core/Configuration/ConnectionString.cs | 76 +++++++++++++++++ src/MongoDB.Driver/MongoUrl.cs | 28 +++++++ src/MongoDB.Driver/MongoUrlBuilder.cs | 82 +++++++++++++++++++ 3 files changed, 186 insertions(+) diff --git a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs index 79349c035cc..0236f0b68e7 100644 --- a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs +++ b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs @@ -92,6 +92,10 @@ public sealed class ConnectionString private string _replicaSet; private bool? _retryReads; private bool? _retryWrites; + private string _proxyHost; + private int? _proxyPort; + private string _proxyUsername; + private string _proxyPassword; private ConnectionStringScheme _scheme; private ServerMonitoringMode? _serverMonitoringMode; private TimeSpan? _serverSelectionTimeout; @@ -356,6 +360,26 @@ public string Password get { return _password; } } + /// + /// Gets the proxy host. + /// + public string ProxyHost => _proxyHost; + + /// + /// Gets the proxy port. + /// + public int? ProxyPort => _proxyPort; + + /// + /// Gets the proxy username. + /// + public string ProxyUsername => _proxyUsername; + + /// + /// Gets the proxy password. + /// + public string ProxyPassword => _proxyPassword; + /// /// Gets the read concern level. /// @@ -903,6 +927,29 @@ private void Parse() } } + if (string.IsNullOrEmpty(_proxyHost)) + { + if (_proxyPort is not null) + { + throw new MongoConfigurationException("proxyPort cannot be specified without proxyHost."); + } + + if (!string.IsNullOrEmpty(_proxyUsername)) + { + throw new MongoConfigurationException("proxyUsername cannot be specified without proxyHost."); + } + + if (!string.IsNullOrEmpty(_proxyPassword)) + { + throw new MongoConfigurationException("proxyPassword cannot be specified without proxyHost."); + } + } + + if (string.IsNullOrEmpty(_proxyUsername) != string.IsNullOrEmpty(_proxyPassword)) + { + throw new MongoConfigurationException("proxyUsername and proxyPassword must both be specified or neither."); + } + string ProtectConnectionString(string connectionString) { var protectedString = Regex.Replace(connectionString, @"(?<=://)[^/]*(?=@)", ""); @@ -995,6 +1042,35 @@ private void ParseOption(string name, string value) case "minpoolsize": _minPoolSize = ParseInt32(name, value); break; + case "proxyhost": + _proxyHost = value; + if (_proxyHost.Length == 0) + { + throw new MongoConfigurationException("proxyHost cannot be empty."); + } + break; + case "proxyport": + var proxyPortValue = ParseInt32(name, value); + if (proxyPortValue is < 0 or > 65535) + { + throw new MongoConfigurationException("proxyPort must be between 0 and 65535."); + } + _proxyPort = proxyPortValue; + break; + case "proxyusername": + _proxyUsername = value; + if (_proxyUsername.Length == 0) + { + throw new MongoConfigurationException("proxyUsername cannot be empty."); + } + break; + case "proxypassword": + _proxyPassword = value; + if (_proxyPassword.Length == 0) + { + throw new MongoConfigurationException("proxyPassword cannot be empty."); + } + break; case "readconcernlevel": _readConcernLevel = ParseEnum(name, value); break; diff --git a/src/MongoDB.Driver/MongoUrl.cs b/src/MongoDB.Driver/MongoUrl.cs index 2c1446fd57c..13a3e46a0d5 100644 --- a/src/MongoDB.Driver/MongoUrl.cs +++ b/src/MongoDB.Driver/MongoUrl.cs @@ -64,6 +64,10 @@ public class MongoUrl : IEquatable private readonly bool? _retryReads; private readonly bool? _retryWrites; private readonly TimeSpan _localThreshold; + private readonly string _proxyHost; + private readonly int? _proxyPort; + private readonly string _proxyUsername; + private readonly string _proxyPassword; private readonly ConnectionStringScheme _scheme; private readonly IEnumerable _servers; private readonly ServerMonitoringMode? _serverMonitoringMode; @@ -117,6 +121,10 @@ internal MongoUrl(MongoUrlBuilder builder) _maxConnectionPoolSize = builder.MaxConnectionPoolSize; _minConnectionPoolSize = builder.MinConnectionPoolSize; _password = builder.Password; + _proxyHost = builder.ProxyHost; + _proxyPort = builder.ProxyPort; + _proxyUsername = builder.ProxyUsername; + _proxyPassword = builder.ProxyPassword; _readConcernLevel = builder.ReadConcernLevel; _readPreference = builder.ReadPreference; _replicaSetName = builder.ReplicaSetName; @@ -358,6 +366,26 @@ public string Password get { return _password; } } + /// + /// Gets the proxy host. + /// + public string ProxyHost => _proxyHost; + + /// + /// Gets the proxy port. + /// + public int ProxyPort => _proxyPort; + + /// + /// Gets the proxy username. + /// + public string ProxyUsername => _proxyUsername; + + /// + /// Gets the proxy password. + /// + public string ProxyPassword => _proxyPassword; + /// /// Gets the read concern level. /// diff --git a/src/MongoDB.Driver/MongoUrlBuilder.cs b/src/MongoDB.Driver/MongoUrlBuilder.cs index 3858228cbf6..c50288823c2 100644 --- a/src/MongoDB.Driver/MongoUrlBuilder.cs +++ b/src/MongoDB.Driver/MongoUrlBuilder.cs @@ -60,6 +60,10 @@ public class MongoUrlBuilder private string _replicaSetName; private bool? _retryReads; private bool? _retryWrites; + private string _proxyHost; + private int? _proxyPort; + private string _proxyUsername; + private string _proxyPassword; private ConnectionStringScheme _scheme; private IEnumerable _servers; private ServerMonitoringMode? _serverMonitoringMode; @@ -104,6 +108,10 @@ public MongoUrlBuilder() _maxConnectionPoolSize = MongoDefaults.MaxConnectionPoolSize; _minConnectionPoolSize = MongoDefaults.MinConnectionPoolSize; _password = null; + _proxyHost = null; + _proxyPort = null; + _proxyUsername = null; + _proxyPassword = null; _readConcernLevel = null; _readPreference = null; _replicaSetName = null; @@ -438,6 +446,59 @@ public string Password set { _password = value; } } + /// + /// + /// + public string ProxyHost + { + get => _proxyHost; + set + { + _proxyHost = Ensure.IsNotNullOrEmpty(value, nameof(ProxyHost)); + } + } + + /// + /// + /// + /// + public int? ProxyPort + { + get => _proxyPort; + set + { + if (value is < 0 or > 65535) + { + throw new ArgumentOutOfRangeException(nameof(value), "ProxyPort must be between 0 and 65535."); + } + _proxyPort = value; + } + } + + /// + /// + /// + public string ProxyUsername + { + get => _proxyUsername; + set + { + _proxyUsername = Ensure.IsNotNullOrEmpty(value, nameof(ProxyUsername)); + } + } + + /// + /// + /// + public string ProxyPassword + { + get => _proxyPassword; + set + { + _proxyPassword = Ensure.IsNotNullOrEmpty(value, nameof(ProxyPassword)); + } + } + /// /// Gets or sets the read concern level. /// @@ -760,6 +821,7 @@ public MongoUrl ToMongoUrl() /// The canonical URL. public override string ToString() { + //TODO Need to add options here too StringBuilder url = new StringBuilder(); if (_scheme == ConnectionStringScheme.MongoDB) { @@ -980,6 +1042,22 @@ public override string ToString() { query.AppendFormat("retryWrites={0}&", JsonConvert.ToString(_retryWrites.Value)); } + if(!string.IsNullOrEmpty(_proxyHost)) + { + query.AppendFormat("proxyHost={0}&", _proxyHost); + } + if (_proxyPort.HasValue) + { + query.AppendFormat("proxyPort={0}&", _proxyPort); + } + if (!string.IsNullOrEmpty(_proxyUsername)) + { + query.AppendFormat("proxyUsername={0}&", _proxyUsername); + } + if (!string.IsNullOrEmpty(_proxyPassword)) + { + query.AppendFormat("proxyPassword={0}&", _proxyPassword); + } if (_srvMaxHosts.HasValue) { query.AppendFormat("srvMaxHosts={0}&", _srvMaxHosts); @@ -1026,6 +1104,10 @@ private void InitializeFromConnectionString(ConnectionString connectionString) _maxConnectionPoolSize = connectionString.MaxPoolSize.GetValueOrDefault(MongoDefaults.MaxConnectionPoolSize); _minConnectionPoolSize = connectionString.MinPoolSize.GetValueOrDefault(MongoDefaults.MinConnectionPoolSize); _password = connectionString.Password; + _proxyHost = connectionString.ProxyHost; + _proxyPort = connectionString.ProxyPort; + _proxyUsername = connectionString.ProxyUsername; + _proxyPassword = connectionString.ProxyPassword; _readConcernLevel = connectionString.ReadConcernLevel; if (connectionString.ReadPreference.HasValue || connectionString.ReadPreferenceTags != null || connectionString.MaxStaleness.HasValue) { From 669618430bcbde0d26fb2decf919b927bf130eae Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:02:22 +0200 Subject: [PATCH 2/9] Small fix --- src/MongoDB.Driver/MongoUrl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/MongoUrl.cs b/src/MongoDB.Driver/MongoUrl.cs index 13a3e46a0d5..912c0b15c49 100644 --- a/src/MongoDB.Driver/MongoUrl.cs +++ b/src/MongoDB.Driver/MongoUrl.cs @@ -374,7 +374,7 @@ public string Password /// /// Gets the proxy port. /// - public int ProxyPort => _proxyPort; + public int? ProxyPort => _proxyPort; /// /// Gets the proxy username. From b3020b5a27e7968c1afbcf28fcd977494114a2a8 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:54:28 +0200 Subject: [PATCH 3/9] Added options to MongoClient settings --- src/MongoDB.Driver/MongoClientSettings.cs | 121 ++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/MongoDB.Driver/MongoClientSettings.cs b/src/MongoDB.Driver/MongoClientSettings.cs index d7da362ca92..52ff666f3db 100644 --- a/src/MongoDB.Driver/MongoClientSettings.cs +++ b/src/MongoDB.Driver/MongoClientSettings.cs @@ -59,6 +59,10 @@ public class MongoClientSettings : IEquatable, IInheritable private TimeSpan _maxConnectionLifeTime; private int _maxConnectionPoolSize; private int _minConnectionPoolSize; + private string _proxyHost; + private int? _proxyPort; + private string _proxyUsername; + private string _proxyPassword; private ReadConcern _readConcern; private UTF8Encoding _readEncoding; private ReadPreference _readPreference; @@ -110,6 +114,10 @@ public MongoClientSettings() _maxConnectionLifeTime = MongoDefaults.MaxConnectionLifeTime; _maxConnectionPoolSize = MongoDefaults.MaxConnectionPoolSize; _minConnectionPoolSize = MongoDefaults.MinConnectionPoolSize; + _proxyHost = null; + _proxyPort = null; + _proxyUsername = null; + _proxyPassword = null; _readConcern = ReadConcern.Default; _readEncoding = null; _readPreference = ReadPreference.Primary; @@ -428,6 +436,64 @@ public int MinConnectionPoolSize } } + /// + /// Gets or sets the proxy host. + /// + public string ProxyHost + { + get => _proxyHost; + set + { + if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } + _proxyHost = Ensure.IsNotNullOrEmpty(value, nameof(ProxyHost)); + } + } + + /// + /// Gets or sets the proxy port. + /// + public int? ProxyPort + { + get => _proxyPort; + set + { + if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } + + if (value is < 0 or > 65535) + { + throw new MongoConfigurationException("ProxyPort must be between 0 and 65535."); + } + + _proxyPort = value; + } + } + + /// + /// Gets or sets the proxy username. + /// + public string ProxyUsername + { + get => _proxyUsername; + set + { + if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } + _proxyUsername = Ensure.IsNotNullOrEmpty(value, nameof(ProxyUsername)); + } + } + + /// + /// Gets or sets the proxy password. + /// + public string ProxyPassword + { + get => _proxyPassword; + set + { + if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } + _proxyPassword = Ensure.IsNotNullOrEmpty(value, nameof(ProxyPassword)); + } + } + /// /// Gets or sets the read concern. /// @@ -863,6 +929,10 @@ public static MongoClientSettings FromUrl(MongoUrl url) clientSettings.MaxConnectionLifeTime = url.MaxConnectionLifeTime; clientSettings.MaxConnectionPoolSize = ConnectionStringConversions.GetEffectiveMaxConnections(url.MaxConnectionPoolSize); clientSettings.MinConnectionPoolSize = url.MinConnectionPoolSize; + clientSettings.ProxyHost = url.ProxyHost; + clientSettings.ProxyPort = url.ProxyPort; + clientSettings.ProxyUsername = url.ProxyUsername; + clientSettings.ProxyPassword = url.ProxyPassword; clientSettings.ReadConcern = new ReadConcern(url.ReadConcernLevel); clientSettings.ReadEncoding = null; // ReadEncoding must be provided in code clientSettings.ReadPreference = (url.ReadPreference == null) ? ReadPreference.Primary : url.ReadPreference; @@ -920,6 +990,10 @@ public MongoClientSettings Clone() clone._maxConnectionLifeTime = _maxConnectionLifeTime; clone._maxConnectionPoolSize = _maxConnectionPoolSize; clone._minConnectionPoolSize = _minConnectionPoolSize; + clone._proxyHost = _proxyHost; + clone._proxyPort = _proxyPort; + clone._proxyUsername = _proxyUsername; + clone._proxyPassword = _proxyPassword; clone._readConcern = _readConcern; clone._readEncoding = _readEncoding; clone._readPreference = _readPreference; @@ -989,6 +1063,10 @@ public override bool Equals(object obj) _maxConnectionLifeTime == rhs._maxConnectionLifeTime && _maxConnectionPoolSize == rhs._maxConnectionPoolSize && _minConnectionPoolSize == rhs._minConnectionPoolSize && + _proxyHost == rhs._proxyHost && + _proxyPort == rhs._proxyPort && + _proxyUsername == rhs._proxyUsername && + _proxyPassword == rhs._proxyPassword && object.Equals(_readEncoding, rhs._readEncoding) && object.Equals(_readConcern, rhs._readConcern) && object.Equals(_readPreference, rhs._readPreference) && @@ -1076,6 +1154,10 @@ public override int GetHashCode() .Hash(_maxConnectionLifeTime) .Hash(_maxConnectionPoolSize) .Hash(_minConnectionPoolSize) + .Hash(_proxyHost) + .Hash(_proxyPort) + .Hash(_proxyUsername) + .Hash(_proxyPassword) .Hash(_readConcern) .Hash(_readEncoding) .Hash(_readPreference) @@ -1145,6 +1227,22 @@ public override string ToString() sb.AppendFormat("MaxConnectionLifeTime={0};", _maxConnectionLifeTime); sb.AppendFormat("MaxConnectionPoolSize={0};", _maxConnectionPoolSize); sb.AppendFormat("MinConnectionPoolSize={0};", _minConnectionPoolSize); + if (_proxyHost != null) + { + sb.AppendFormat("ProxyHost={0};", _proxyHost); + } + if (_proxyPort != null) + { + sb.AppendFormat("ProxyPort={0};", _proxyPort.Value); + } + if (_proxyUsername != null) + { + sb.AppendFormat("ProxyUsername={0};", _proxyUsername); + } + if (_proxyPassword != null) + { + sb.AppendFormat("ProxyPassword={0};", _proxyPassword); + } if (_readEncoding != null) { sb.Append("ReadEncoding=UTF8Encoding;"); @@ -1297,6 +1395,29 @@ private void ThrowIfSettingsAreInvalid() throw new InvalidOperationException("Load balanced mode cannot be used with direct connection."); } } + + if (string.IsNullOrEmpty(_proxyHost)) + { + if (_proxyPort is not null) + { + throw new InvalidOperationException("ProxyPort cannot be specified without ProxyHost."); + } + + if (!string.IsNullOrEmpty(_proxyUsername)) + { + throw new InvalidOperationException("ProxyUsername cannot be specified without ProxyHost."); + } + + if (!string.IsNullOrEmpty(_proxyPassword)) + { + throw new InvalidOperationException("ProxyPassword cannot be specified without ProxyHost."); + } + } + + if (string.IsNullOrEmpty(_proxyUsername) != string.IsNullOrEmpty(_proxyPassword)) + { + throw new InvalidOperationException("ProxyUsername and ProxyPassword must both be specified or neither."); + } } } } From 7931f70bae5bfa6df744a852ce2767bcefa132bc Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:54:42 +0200 Subject: [PATCH 4/9] Added uri options test for proxy options --- .../uri-options/tests/proxy-options.json | 139 ++++++++++++++++++ .../uri-options/tests/proxy-options.yml | 121 +++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 specifications/uri-options/tests/proxy-options.json create mode 100644 specifications/uri-options/tests/proxy-options.yml diff --git a/specifications/uri-options/tests/proxy-options.json b/specifications/uri-options/tests/proxy-options.json new file mode 100644 index 00000000000..585546ead7f --- /dev/null +++ b/specifications/uri-options/tests/proxy-options.json @@ -0,0 +1,139 @@ +{ + "tests": [ + { + "description": "proxyPort without proxyHost", + "uri": "mongodb://localhost/?proxyPort=1080", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "proxyUsername without proxyHost", + "uri": "mongodb://localhost/?proxyUsername=abc", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "proxyPassword without proxyHost", + "uri": "mongodb://localhost/?proxyPassword=def", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "all other proxy options without proxyHost", + "uri": "mongodb://localhost/?proxyPort=1080&proxyUsername=abc&proxyPassword=def", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "proxyUsername without proxyPassword", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "proxyPassword without proxyUsername", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyPassword=def", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "multiple proxyHost parameters", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyHost=localhost2", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "multiple proxyPort parameters", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=1234&proxyPort=12345", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "multiple proxyUsername parameters", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyUsername=def&proxyPassword=123", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "multiple proxyPassword parameters", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyPassword=123&proxyPassword=456", + "valid": false, + "warning": false, + "hosts": null, + "auth": null, + "options": null + }, + { + "description": "only host present", + "uri": "mongodb://localhost/?proxyHost=localhost", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "host and default port present", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=1080", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "host and non-default port present", + "uri": "mongodb://localhost/?proxyHost=localhost&proxyPort=12345", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "replicaset, host and non-default port present", + "uri": "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + }, + { + "description": "all options present", + "uri": "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345&proxyUsername=asdf&proxyPassword=qwerty", + "valid": true, + "warning": false, + "hosts": null, + "auth": null, + "options": {} + } + ] +} diff --git a/specifications/uri-options/tests/proxy-options.yml b/specifications/uri-options/tests/proxy-options.yml new file mode 100644 index 00000000000..a97863dd599 --- /dev/null +++ b/specifications/uri-options/tests/proxy-options.yml @@ -0,0 +1,121 @@ +tests: + - + description: "proxyPort without proxyHost" + uri: "mongodb://localhost/?proxyPort=1080" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "proxyUsername without proxyHost" + uri: "mongodb://localhost/?proxyUsername=abc" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "proxyPassword without proxyHost" + uri: "mongodb://localhost/?proxyPassword=def" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "all other proxy options without proxyHost" + uri: "mongodb://localhost/?proxyPort=1080&proxyUsername=abc&proxyPassword=def" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "proxyUsername without proxyPassword" + uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "proxyPassword without proxyUsername" + uri: "mongodb://localhost/?proxyHost=localhost&proxyPassword=def" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "multiple proxyHost parameters" + uri: "mongodb://localhost/?proxyHost=localhost&proxyHost=localhost2" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "multiple proxyPort parameters" + uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=1234&proxyPort=12345" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "multiple proxyUsername parameters" + uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyUsername=def&proxyPassword=123" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "multiple proxyPassword parameters" + uri: "mongodb://localhost/?proxyHost=localhost&proxyUsername=abc&proxyPassword=123&proxyPassword=456" + valid: false + warning: false + hosts: ~ + auth: ~ + options: ~ + - + description: "only host present" + uri: "mongodb://localhost/?proxyHost=localhost" + valid: true + warning: false + hosts: ~ + auth: ~ + options: {} + - + description: "host and default port present" + uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=1080" + valid: true + warning: false + hosts: ~ + auth: ~ + options: {} + - + description: "host and non-default port present" + uri: "mongodb://localhost/?proxyHost=localhost&proxyPort=12345" + valid: true + warning: false + hosts: ~ + auth: ~ + options: {} + - + description: "replicaset, host and non-default port present" + uri: "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345" + valid: true + warning: false + hosts: ~ + auth: ~ + options: {} + - + description: "all options present" + uri: "mongodb://rs1,rs2,rs3/?proxyHost=localhost&proxyPort=12345&proxyUsername=asdf&proxyPassword=qwerty" + valid: true + warning: false + hosts: ~ + auth: ~ + options: {} From 186df67e001269acfac3030be3a79b39f416b192 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:05:41 +0200 Subject: [PATCH 5/9] Added additional checks on parsing. --- .../Core/Configuration/ConnectionString.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs index 0236f0b68e7..eadc83aad9b 100644 --- a/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs +++ b/src/MongoDB.Driver/Core/Configuration/ConnectionString.cs @@ -1043,6 +1043,11 @@ private void ParseOption(string name, string value) _minPoolSize = ParseInt32(name, value); break; case "proxyhost": + if (!string.IsNullOrEmpty(_proxyHost)) + { + throw new MongoConfigurationException("Multiple proxyHost options are not allowed."); + } + _proxyHost = value; if (_proxyHost.Length == 0) { @@ -1050,6 +1055,11 @@ private void ParseOption(string name, string value) } break; case "proxyport": + if (_proxyPort != null) + { + throw new MongoConfigurationException("Multiple proxyPort options are not allowed."); + } + var proxyPortValue = ParseInt32(name, value); if (proxyPortValue is < 0 or > 65535) { @@ -1058,6 +1068,11 @@ private void ParseOption(string name, string value) _proxyPort = proxyPortValue; break; case "proxyusername": + if (!string.IsNullOrEmpty(_proxyUsername)) + { + throw new MongoConfigurationException("Multiple proxyUsername options are not allowed."); + } + _proxyUsername = value; if (_proxyUsername.Length == 0) { @@ -1065,6 +1080,11 @@ private void ParseOption(string name, string value) } break; case "proxypassword": + if (!string.IsNullOrEmpty(_proxyPassword)) + { + throw new MongoConfigurationException("Multiple proxyPassword options are not allowed."); + } + _proxyPassword = value; if (_proxyPassword.Length == 0) { From 567bfa78aaa6f7a08c57cfbb5fd94906545927ae Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:37:52 +0200 Subject: [PATCH 6/9] Small fix --- src/MongoDB.Driver/MongoClientSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MongoDB.Driver/MongoClientSettings.cs b/src/MongoDB.Driver/MongoClientSettings.cs index 52ff666f3db..90b4635e2ed 100644 --- a/src/MongoDB.Driver/MongoClientSettings.cs +++ b/src/MongoDB.Driver/MongoClientSettings.cs @@ -445,7 +445,7 @@ public string ProxyHost set { if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } - _proxyHost = Ensure.IsNotNullOrEmpty(value, nameof(ProxyHost)); + _proxyHost = value; } } @@ -477,7 +477,7 @@ public string ProxyUsername set { if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } - _proxyUsername = Ensure.IsNotNullOrEmpty(value, nameof(ProxyUsername)); + _proxyUsername = value; } } @@ -490,7 +490,7 @@ public string ProxyPassword set { if (_isFrozen) { throw new InvalidOperationException("MongoClientSettings is frozen."); } - _proxyPassword = Ensure.IsNotNullOrEmpty(value, nameof(ProxyPassword)); + _proxyPassword = value; } } From feaa948caff1980fa81de58b80326083eecc28c8 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:49:03 +0200 Subject: [PATCH 7/9] Added base implementation --- .../Core/Configuration/TcpStreamSettings.cs | 45 +++- .../Core/Connections/Socks5Helper.cs | 221 ++++++++++++++++++ .../Core/Connections/TcpStreamFactory.cs | 14 +- 3 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 src/MongoDB.Driver/Core/Connections/Socks5Helper.cs diff --git a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs index 5a6550fa592..e59e9e92f03 100644 --- a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs +++ b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs @@ -33,6 +33,10 @@ public class TcpStreamSettings private readonly int _sendBufferSize; private readonly Action _socketConfigurator; private readonly TimeSpan? _writeTimeout; + private readonly string _proxyHost; + private readonly int? _proxyPort; + private readonly string _proxyUsername; + private readonly string _proxyPassword; // constructors /// @@ -45,6 +49,10 @@ public class TcpStreamSettings /// Size of the send buffer. /// The socket configurator. /// The write timeout. + /// //TODO + /// + /// + /// public TcpStreamSettings( Optional addressFamily = default(Optional), Optional connectTimeout = default(Optional), @@ -52,7 +60,11 @@ public TcpStreamSettings( Optional receiveBufferSize = default(Optional), Optional sendBufferSize = default(Optional), Optional> socketConfigurator = default(Optional>), - Optional writeTimeout = default(Optional)) + Optional writeTimeout = default(Optional), + Optional proxyHost = default(Optional), + Optional proxyPort = default(Optional), + Optional proxyUsername = default(Optional), + Optional proxyPassword = default(Optional)) { _addressFamily = addressFamily.WithDefault(AddressFamily.InterNetwork); _connectTimeout = Ensure.IsInfiniteOrGreaterThanOrEqualToZero(connectTimeout.WithDefault(Timeout.InfiniteTimeSpan), "connectTimeout"); @@ -61,6 +73,10 @@ public TcpStreamSettings( _sendBufferSize = Ensure.IsGreaterThanZero(sendBufferSize.WithDefault(64 * 1024), "sendBufferSize"); _socketConfigurator = socketConfigurator.WithDefault(null); _writeTimeout = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(writeTimeout.WithDefault(null), "writeTimeout"); + _proxyHost = proxyHost.WithDefault(null); + _proxyPort = proxyPort.WithDefault(null); + _proxyUsername = proxyUsername.WithDefault(null); + _proxyPassword = proxyPassword.WithDefault(null); } internal TcpStreamSettings(TcpStreamSettings other) @@ -72,6 +88,10 @@ internal TcpStreamSettings(TcpStreamSettings other) _sendBufferSize = other.SendBufferSize; _socketConfigurator = other.SocketConfigurator; _writeTimeout = other.WriteTimeout; + _proxyHost = other._proxyHost; + _proxyPort = other._proxyPort; + _proxyUsername = other._proxyUsername; + _proxyPassword = other._proxyPassword; } // properties @@ -152,6 +172,28 @@ public TimeSpan? WriteTimeout get { return _writeTimeout; } } + //TODO Add xml docs + /// + /// + /// + public string ProxyHost => _proxyHost; + /// + /// + /// + public int? ProxyPort => _proxyPort; + /// + /// + /// + public string ProxyUsername => _proxyUsername; + /// + /// + /// + public string ProxyPassword => _proxyPassword; + + //TODO We can decide to remove this + internal bool UseProxy => !string.IsNullOrEmpty(_proxyHost) && _proxyPort.HasValue; + + // methods /// /// Returns a new TcpStreamSettings instance with some settings changed. @@ -172,6 +214,7 @@ public TcpStreamSettings With( Optional sendBufferSize = default(Optional), Optional> socketConfigurator = default(Optional>), Optional writeTimeout = default(Optional)) + //TODO Need to add proxy settings { return new TcpStreamSettings( addressFamily: addressFamily.WithDefault(_addressFamily), diff --git a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs new file mode 100644 index 00000000000..fda6f2b0c96 --- /dev/null +++ b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs @@ -0,0 +1,221 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using MongoDB.Driver.GridFS; + +namespace MongoDB.Driver.Core.Connections +{ + internal static class Socks5Helper + { + // Schemas for requests/responses are taken from the following RFCs: + // SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928 + // Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929 + + private const byte ProtocolVersion5 = 0x05; + private const byte SubnegotiationVersion = 0x01; + private const byte CmdConnect = 0x01; + private const byte MethodNoAuth = 0x00; + private const byte MethodUsernamePassword = 0x02; + private const byte AddressTypeIPv4 = 0x01; + private const byte AddressTypeDomain = 0x03; + private const byte AddressTypeIPv6 = 0x04; + private const byte Socks5Success = 0x00; + + private const int BufferSize = 512; + + public static void PerformSocks5Handshake(Stream stream, string targetHost, int targetPort, string proxyUsername, string proxyPassword, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(BufferSize); + try + { + var useAuth = !string.IsNullOrEmpty(proxyUsername) && !string.IsNullOrEmpty(proxyPassword); + + // Greeting request + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + buffer[0] = ProtocolVersion5; + + if (!useAuth) + { + buffer[1] = 1; + buffer[2] = MethodNoAuth; + } + else + { + buffer[1] = 2; + buffer[2] = MethodNoAuth; + buffer[3] = MethodUsernamePassword; + } + + stream.Write(buffer, 0, useAuth ? 4 : 3); + stream.Flush(); + + // Greeting response + // +----+--------+ + // |VER | METHOD | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + + stream.ReadBytes(buffer, 0,2, cancellationToken); + + VerifyProtocolVersion(buffer[0]); + + var method = buffer[1]; + if (method == MethodUsernamePassword) + { + if (!useAuth) + { + //We should not reach here + throw new IOException("SOCKS5 proxy requires authentication, but no credentials were provided."); + } + + // Authentication request + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + buffer[0] = SubnegotiationVersion; + var usernameLength = EncodeString(proxyUsername, buffer.AsSpan(2), nameof(proxyUsername)); + buffer[1] = usernameLength; + var passwordLength = EncodeString(proxyPassword, buffer.AsSpan(3 + usernameLength), nameof(proxyPassword)); + buffer[2 + usernameLength] = passwordLength; + + var authLength = 3 + usernameLength + passwordLength; + stream.Write(buffer, 0, authLength); + stream.Flush(); + + // Authentication response + // +----+--------+ + // |VER | STATUS | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + stream.ReadBytes(buffer, 0,2, cancellationToken); + if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5Success) + { + throw new IOException("SOCKS5 authentication failed."); + } + } + else if (method != MethodNoAuth) + { + throw new IOException("SOCKS5 proxy requires unsupported authentication method."); + } + + // Connect request + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + buffer[0] = ProtocolVersion5; + buffer[1] = CmdConnect; + buffer[2] = 0x00; + var addressLength = 0; + + if (IPAddress.TryParse(targetHost, out var ip)) + { + switch (ip.AddressFamily) + { + case AddressFamily.InterNetwork: + buffer[3] = AddressTypeIPv4; + ip.TryWriteBytes(buffer.AsSpan(4), out _); + addressLength = 4; + break; + case AddressFamily.InterNetworkV6: + buffer[3] = AddressTypeIPv6; + ip.TryWriteBytes(buffer.AsSpan(4), out _); + addressLength = 16; + break; + default: + throw new IOException("Invalid target host address family. Only IPv4 and IPv6 are supported."); + } + } + else + { + buffer[3] = AddressTypeDomain; + var hostLength = EncodeString(targetHost, buffer.AsSpan(5), nameof(targetHost)); + buffer[4] = hostLength; + addressLength = hostLength + 1; + } + + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)targetPort); + + stream.Write(buffer, 0, addressLength + 6); + stream.Flush(); + + // Connect response + // +----+-----+-------+------+----------+----------+ + // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + stream.ReadBytes(buffer, 0,5, cancellationToken); + VerifyProtocolVersion(buffer[0]); + if (buffer[1] != Socks5Success) + { + throw new IOException($"SOCKS5 connect failed with code 0x{buffer[1]:X2}"); + } + + var skip = buffer[3] switch + { + AddressTypeIPv4 => 4 + 2, + AddressTypeIPv6 => 16 + 2, + AddressTypeDomain => buffer[4] + 1 + 2, + _ => throw new IOException("Unknown address type in SOCKS5 reply.") + }; + + stream.ReadBytes(buffer, 0, skip, cancellationToken); + // Address and port in response are ignored + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static void VerifyProtocolVersion(byte version) + { + if (version != ProtocolVersion5) + { + throw new IOException("Invalid SOCKS version in method selection response."); + } + } + + private static byte EncodeString(ReadOnlySpan chars, Span buffer, string parameterName) + { + try + { + return checked((byte)Encoding.UTF8.GetBytes(chars, buffer)); + } + catch + { + throw new IOException($"The {parameterName} could not be encoded as UTF-8."); + } + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 9bb93eea097..5fcfebc982f 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -48,19 +48,23 @@ public TcpStreamFactory(TcpStreamSettings settings) // methods public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToken) { + EndPoint actualEndPoint; + + actualEndPoint = _settings.UseProxy ? new DnsEndPoint(_settings.ProxyHost, _settings.ProxyPort.Value) : endPoint; + #if NET472 - var socket = CreateSocket(endPoint); - Connect(socket, endPoint, cancellationToken); + var socket = CreateSocket(actualEndPoint); + Connect(socket, actualEndPoint, cancellationToken); return CreateNetworkStream(socket); #else - var resolved = ResolveEndPoints(endPoint); + var resolved = ResolveEndPoints(actualEndPoint); for (int i = 0; i < resolved.Length; i++) { try { var socket = CreateSocket(resolved[i]); Connect(socket, resolved[i], cancellationToken); - return CreateNetworkStream(socket); + var stream = CreateNetworkStream(socket); } catch { @@ -74,7 +78,7 @@ public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToke } // we should never get here... - throw new InvalidOperationException("Unabled to resolve endpoint."); + throw new InvalidOperationException("Unable to resolve endpoint."); #endif } From f8bd6a3547108e7b5eb7e44a696b8aec9d3cf4ab Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:25:15 +0200 Subject: [PATCH 8/9] Improvements plus initial tests --- src/MongoDB.Driver/ClusterRegistry.cs | 1 + .../Configuration/ClusterBuilderExtensions.cs | 9 ++++ .../Core/Configuration/TcpStreamSettings.cs | 17 +++++-- .../Core/Connections/Socks5Helper.cs | 41 ++++++++++++++- .../Core/Connections/TcpStreamFactory.cs | 30 +++++------ .../socks5-support/Socks5SupportProseTests.cs | 50 +++++++++++++++++++ 6 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs diff --git a/src/MongoDB.Driver/ClusterRegistry.cs b/src/MongoDB.Driver/ClusterRegistry.cs index 3359cd4b612..2bb28a139ff 100644 --- a/src/MongoDB.Driver/ClusterRegistry.cs +++ b/src/MongoDB.Driver/ClusterRegistry.cs @@ -172,6 +172,7 @@ private TcpStreamSettings ConfigureTcp(TcpStreamSettings settings, ClusterKey cl receiveBufferSize: clusterKey.ReceiveBufferSize, sendBufferSize: clusterKey.SendBufferSize, writeTimeout: clusterKey.SocketTimeout); + //TODO Maybe need to add proxy settings to clusterKey as well } internal IClusterInternal GetOrCreateCluster(ClusterKey clusterKey) diff --git a/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs b/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs index 3fe6c4a3da2..1b9cdd59009 100644 --- a/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs +++ b/src/MongoDB.Driver/Core/Configuration/ClusterBuilderExtensions.cs @@ -118,6 +118,15 @@ public static ClusterBuilder ConfigureWithConnectionString( builder = builder.ConfigureTcp(s => s.With(addressFamily: AddressFamily.InterNetworkV6)); } + if (connectionString.ProxyHost != null) + { + builder = builder.ConfigureTcp(s => s.With( + proxyHost: connectionString.ProxyHost, + proxyPort: connectionString.ProxyPort, + proxyUsername: connectionString.ProxyUsername, + proxyPassword: connectionString.ProxyPassword)); + } + if (connectionString.SocketTimeout != null) { builder = builder.ConfigureTcp(s => s.With( diff --git a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs index e59e9e92f03..c738bfedd31 100644 --- a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs +++ b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs @@ -205,6 +205,10 @@ public TimeSpan? WriteTimeout /// Size of the send buffer. /// The socket configurator. /// The write timeout. + /// //TODO Add docs + /// + /// + /// /// A new TcpStreamSettings instance. public TcpStreamSettings With( Optional addressFamily = default(Optional), @@ -213,8 +217,11 @@ public TcpStreamSettings With( Optional receiveBufferSize = default(Optional), Optional sendBufferSize = default(Optional), Optional> socketConfigurator = default(Optional>), - Optional writeTimeout = default(Optional)) - //TODO Need to add proxy settings + Optional writeTimeout = default(Optional), + Optional proxyHost = default(Optional), + Optional proxyPort = default(Optional), + Optional proxyUsername = default(Optional), + Optional proxyPassword = default(Optional)) { return new TcpStreamSettings( addressFamily: addressFamily.WithDefault(_addressFamily), @@ -223,7 +230,11 @@ public TcpStreamSettings With( receiveBufferSize: receiveBufferSize.WithDefault(_receiveBufferSize), sendBufferSize: sendBufferSize.WithDefault(_sendBufferSize), socketConfigurator: socketConfigurator.WithDefault(_socketConfigurator), - writeTimeout: writeTimeout.WithDefault(_writeTimeout)); + writeTimeout: writeTimeout.WithDefault(_writeTimeout), + proxyHost: proxyHost.WithDefault(_proxyHost), + proxyPort: proxyPort.WithDefault(_proxyPort), + proxyUsername: proxyUsername.WithDefault(_proxyUsername), + proxyPassword: proxyPassword.WithDefault(_proxyPassword)); } } } diff --git a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs index fda6f2b0c96..50e731803f9 100644 --- a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs +++ b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs @@ -21,6 +21,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using MongoDB.Driver.Core.Misc; using MongoDB.Driver.GridFS; namespace MongoDB.Driver.Core.Connections @@ -43,8 +44,11 @@ internal static class Socks5Helper private const int BufferSize = 512; - public static void PerformSocks5Handshake(Stream stream, string targetHost, int targetPort, string proxyUsername, string proxyPassword, CancellationToken cancellationToken) + //TODO Make an async version of this method + public static void PerformSocks5Handshake(Stream stream, EndPoint endPoint, string proxyUsername, string proxyPassword, CancellationToken cancellationToken) { + var (targetHost, targetPort) = endPoint.GetHostAndPort(); + var buffer = ArrayPool.Shared.Rent(BufferSize); try { @@ -100,9 +104,18 @@ public static void PerformSocks5Handshake(Stream stream, string targetHost, int // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ buffer[0] = SubnegotiationVersion; + //TODO Maybe it can be extracted to a method? +#if NET472 + var usernameLength = EncodeString(proxyUsername, buffer, 2, nameof(proxyUsername)); +#else var usernameLength = EncodeString(proxyUsername, buffer.AsSpan(2), nameof(proxyUsername)); +#endif buffer[1] = usernameLength; +#if NET472 + var passwordLength = EncodeString(proxyPassword, buffer, 3 + usernameLength, nameof(proxyPassword)); +#else var passwordLength = EncodeString(proxyPassword, buffer.AsSpan(3 + usernameLength), nameof(proxyPassword)); +#endif buffer[2 + usernameLength] = passwordLength; var authLength = 3 + usernameLength + passwordLength; @@ -137,18 +150,23 @@ public static void PerformSocks5Handshake(Stream stream, string targetHost, int buffer[2] = 0x00; var addressLength = 0; + //TODO Can we avoid doing this...? if (IPAddress.TryParse(targetHost, out var ip)) { switch (ip.AddressFamily) { case AddressFamily.InterNetwork: buffer[3] = AddressTypeIPv4; +#if !NET472 ip.TryWriteBytes(buffer.AsSpan(4), out _); +#endif addressLength = 4; break; case AddressFamily.InterNetworkV6: buffer[3] = AddressTypeIPv6; +#if !NET472 ip.TryWriteBytes(buffer.AsSpan(4), out _); +#endif addressLength = 16; break; default: @@ -158,7 +176,11 @@ public static void PerformSocks5Handshake(Stream stream, string targetHost, int else { buffer[3] = AddressTypeDomain; +#if NET472 + var hostLength = EncodeString(targetHost, buffer, 5, nameof(targetHost)); +#else var hostLength = EncodeString(targetHost, buffer.AsSpan(5), nameof(targetHost)); +#endif buffer[4] = hostLength; addressLength = hostLength + 1; } @@ -206,10 +228,24 @@ private static void VerifyProtocolVersion(byte version) } } - private static byte EncodeString(ReadOnlySpan chars, Span buffer, string parameterName) +#if NET472 + private static byte EncodeString(string input, byte[] buffer, int offset, string parameterName) { try { + var written = Encoding.UTF8.GetBytes(input, 0, input.Length, buffer, offset); + return checked((byte)written); + } + catch + { + throw new IOException($"The {parameterName} could not be encoded as UTF-8."); + } + } +#else + private static byte EncodeString(ReadOnlySpan chars, Span buffer, string parameterName) + { + try + { //TODO Maybe we should remove checked? return checked((byte)Encoding.UTF8.GetBytes(chars, buffer)); } catch @@ -217,5 +253,6 @@ private static byte EncodeString(ReadOnlySpan chars, Span buffer, st throw new IOException($"The {parameterName} could not be encoded as UTF-8."); } } +#endif } } \ No newline at end of file diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 5fcfebc982f..f57cf10f519 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -65,6 +65,12 @@ public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToke var socket = CreateSocket(resolved[i]); Connect(socket, resolved[i], cancellationToken); var stream = CreateNetworkStream(socket); + + //TODO Need to do the same for the async version and for net472 + if (_settings.UseProxy) + { + Socks5Helper.PerformSocks5Handshake(stream, endPoint, _settings.ProxyUsername, _settings.ProxyPassword, cancellationToken); + } } catch { @@ -262,20 +268,18 @@ private Socket CreateSocket(EndPoint endPoint) private EndPoint[] ResolveEndPoints(EndPoint initial) { - var dnsInitial = initial as DnsEndPoint; - if (dnsInitial == null) + if (initial is not DnsEndPoint dnsInitial) { - return new[] { initial }; + return [initial]; } - IPAddress address; - if (IPAddress.TryParse(dnsInitial.Host, out address)) + if (IPAddress.TryParse(dnsInitial.Host, out var address)) { - return new[] { new IPEndPoint(address, dnsInitial.Port) }; + return [new IPEndPoint(address, dnsInitial.Port)]; } var preferred = initial.AddressFamily; - if (preferred == AddressFamily.Unspecified || preferred == AddressFamily.Unknown) + if (preferred is AddressFamily.Unspecified or AddressFamily.Unknown) { preferred = _settings.AddressFamily; } @@ -289,20 +293,18 @@ private EndPoint[] ResolveEndPoints(EndPoint initial) private async Task ResolveEndPointsAsync(EndPoint initial) { - var dnsInitial = initial as DnsEndPoint; - if (dnsInitial == null) + if (initial is not DnsEndPoint dnsInitial) { - return new[] { initial }; + return [initial]; } - IPAddress address; - if (IPAddress.TryParse(dnsInitial.Host, out address)) + if (IPAddress.TryParse(dnsInitial.Host, out var address)) { - return new[] { new IPEndPoint(address, dnsInitial.Port) }; + return [new IPEndPoint(address, dnsInitial.Port)]; } var preferred = initial.AddressFamily; - if (preferred == AddressFamily.Unspecified || preferred == AddressFamily.Unknown) + if (preferred is AddressFamily.Unspecified or AddressFamily.Unknown) { preferred = _settings.AddressFamily; } diff --git a/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs new file mode 100644 index 00000000000..39ce99039df --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs @@ -0,0 +1,50 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using MongoDB.Bson; +using MongoDB.Driver.Core.TestHelpers.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace MongoDB.Driver.Tests.Specifications.socks5_support +{ + [Trait("Category", "Integration")] + public class Socks5SupportProseTests(ITestOutputHelper testOutputHelper) : LoggableTestClass(testOutputHelper) + { + + [Theory] + [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&directConnection=true", false)] + [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080", false)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", false)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&proxyUsername=nonexistentuser&proxyPassword=badauth", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd&directConnection=true", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=username&proxyPassword=p4ssw0rd", true)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true)] + public void TestConnectionStrings(string connectionString, bool expectedResult) + { + var client = new MongoClient(connectionString); + var database = client.GetDatabase("admin"); + + var command = new BsonDocument("hello", 1); + var result = database.RunCommand(command); + } + + } +} \ No newline at end of file From 3665a29076802dfcdcd520cab245ff311f5e160a Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 18 Jul 2025 08:23:25 +0200 Subject: [PATCH 9/9] Various corrections --- src/MongoDB.Driver/ClusterKey.cs | 20 +++++++++++++++++ src/MongoDB.Driver/ClusterRegistry.cs | 7 ++++-- .../Core/Configuration/TcpStreamSettings.cs | 22 +++++++++++++++++++ .../Core/Connections/Socks5Helper.cs | 6 ++--- .../Core/Connections/TcpStreamFactory.cs | 2 ++ src/MongoDB.Driver/MongoClientSettings.cs | 5 +++++ tests/MongoDB.Driver.Tests/ClusterKeyTests.cs | 8 +++++++ .../socks5-support/Socks5SupportProseTests.cs | 8 ++++--- 8 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/MongoDB.Driver/ClusterKey.cs b/src/MongoDB.Driver/ClusterKey.cs index d208a7b60e4..f0bc568ea6b 100644 --- a/src/MongoDB.Driver/ClusterKey.cs +++ b/src/MongoDB.Driver/ClusterKey.cs @@ -46,6 +46,10 @@ internal sealed class ClusterKey private readonly TimeSpan _maxConnectionLifeTime; private readonly int _maxConnectionPoolSize; private readonly int _minConnectionPoolSize; + private readonly string _proxyHost; + private readonly int? _proxyPort; + private readonly string _proxyUsername; + private readonly string _proxyPassword; private readonly int _receiveBufferSize; private readonly string _replicaSetName; private readonly ConnectionStringScheme _scheme; @@ -84,6 +88,10 @@ public ClusterKey( TimeSpan maxConnectionLifeTime, int maxConnectionPoolSize, int minConnectionPoolSize, + string proxyHost, + int? proxyPort, + string proxyUsername, + string proxyPassword, int receiveBufferSize, string replicaSetName, ConnectionStringScheme scheme, @@ -120,6 +128,10 @@ public ClusterKey( _maxConnectionLifeTime = maxConnectionLifeTime; _maxConnectionPoolSize = maxConnectionPoolSize; _minConnectionPoolSize = minConnectionPoolSize; + _proxyHost = proxyHost; + _proxyPort = proxyPort; + _proxyUsername = proxyUsername; + _proxyPassword = proxyPassword; _receiveBufferSize = receiveBufferSize; _replicaSetName = replicaSetName; _scheme = scheme; @@ -160,6 +172,10 @@ public ClusterKey( public TimeSpan MaxConnectionLifeTime { get { return _maxConnectionLifeTime; } } public int MaxConnectionPoolSize { get { return _maxConnectionPoolSize; } } public int MinConnectionPoolSize { get { return _minConnectionPoolSize; } } + public string ProxyHost { get { return _proxyHost; } } + public int? ProxyPort { get { return _proxyPort; } } + public string ProxyUsername { get { return _proxyUsername; } } + public string ProxyPassword { get { return _proxyPassword; } } public int ReceiveBufferSize { get { return _receiveBufferSize; } } public string ReplicaSetName { get { return _replicaSetName; } } public ConnectionStringScheme Scheme { get { return _scheme; } } @@ -215,6 +231,10 @@ public override bool Equals(object obj) _maxConnectionLifeTime == rhs._maxConnectionLifeTime && _maxConnectionPoolSize == rhs._maxConnectionPoolSize && _minConnectionPoolSize == rhs._minConnectionPoolSize && + _proxyHost == rhs._proxyHost && + _proxyPort == rhs._proxyPort && + _proxyUsername == rhs._proxyUsername && + _proxyPassword == rhs._proxyPassword && _receiveBufferSize == rhs._receiveBufferSize && _replicaSetName == rhs._replicaSetName && _scheme == rhs._scheme && diff --git a/src/MongoDB.Driver/ClusterRegistry.cs b/src/MongoDB.Driver/ClusterRegistry.cs index 2bb28a139ff..5332811765e 100644 --- a/src/MongoDB.Driver/ClusterRegistry.cs +++ b/src/MongoDB.Driver/ClusterRegistry.cs @@ -171,8 +171,11 @@ private TcpStreamSettings ConfigureTcp(TcpStreamSettings settings, ClusterKey cl readTimeout: clusterKey.SocketTimeout, receiveBufferSize: clusterKey.ReceiveBufferSize, sendBufferSize: clusterKey.SendBufferSize, - writeTimeout: clusterKey.SocketTimeout); - //TODO Maybe need to add proxy settings to clusterKey as well + writeTimeout: clusterKey.SocketTimeout, + proxyHost: clusterKey.ProxyHost, + proxyPort: clusterKey.ProxyPort, + proxyUsername: clusterKey.ProxyUsername, + proxyPassword: clusterKey.ProxyPassword); } internal IClusterInternal GetOrCreateCluster(ClusterKey clusterKey) diff --git a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs index c738bfedd31..dbd0e77e22b 100644 --- a/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs +++ b/src/MongoDB.Driver/Core/Configuration/TcpStreamSettings.cs @@ -79,6 +79,28 @@ public TcpStreamSettings( _proxyPassword = proxyPassword.WithDefault(null); } + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // public TcpStreamSettings( + // Optional addressFamily, + // Optional connectTimeout, + // Optional readTimeout, + // Optional receiveBufferSize, + // Optional sendBufferSize, + // Optional> socketConfigurator, + // Optional writeTimeout) + // { + // + // } + internal TcpStreamSettings(TcpStreamSettings other) { _addressFamily = other.AddressFamily; diff --git a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs index 50e731803f9..8e4b05d190d 100644 --- a/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs +++ b/src/MongoDB.Driver/Core/Connections/Socks5Helper.cs @@ -205,9 +205,9 @@ public static void PerformSocks5Handshake(Stream stream, EndPoint endPoint, stri var skip = buffer[3] switch { - AddressTypeIPv4 => 4 + 2, - AddressTypeIPv6 => 16 + 2, - AddressTypeDomain => buffer[4] + 1 + 2, + AddressTypeIPv4 => 5, + AddressTypeIPv6 => 17, + AddressTypeDomain => buffer[4] + 2, _ => throw new IOException("Unknown address type in SOCKS5 reply.") }; diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index f57cf10f519..2af5b33885e 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -71,6 +71,8 @@ public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToke { Socks5Helper.PerformSocks5Handshake(stream, endPoint, _settings.ProxyUsername, _settings.ProxyPassword, cancellationToken); } + + return stream; } catch { diff --git a/src/MongoDB.Driver/MongoClientSettings.cs b/src/MongoDB.Driver/MongoClientSettings.cs index 90b4635e2ed..ea2e5796ae8 100644 --- a/src/MongoDB.Driver/MongoClientSettings.cs +++ b/src/MongoDB.Driver/MongoClientSettings.cs @@ -1188,6 +1188,7 @@ public override int GetHashCode() /// A string representation of the settings. public override string ToString() { + //TODO Need to add proxy here if (_isFrozen) { return _frozenStringRepresentation; @@ -1310,6 +1311,10 @@ internal ClusterKey ToClusterKey() _maxConnectionLifeTime, _maxConnectionPoolSize, _minConnectionPoolSize, + _proxyHost, + _proxyPort, + _proxyUsername, + _proxyPassword, MongoDefaults.TcpReceiveBufferSize, // TODO: add ReceiveBufferSize to MongoClientSettings? _replicaSetName, _scheme, diff --git a/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs b/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs index 481da3442c7..ddb15dbd789 100644 --- a/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs +++ b/tests/MongoDB.Driver.Tests/ClusterKeyTests.cs @@ -259,6 +259,10 @@ private ClusterKey CreateSubject(string notEqualFieldName = null) maxConnectionLifeTime, maxConnectionPoolSize, minConnectionPoolSize, + null, //TODO Need to add correct proxy for tests + null, + null, + null, receiveBufferSize, replicaSetName, scheme, @@ -344,6 +348,10 @@ internal ClusterKey CreateSubjectWith( maxConnectionLifeTime, maxConnectionPoolSize, minConnectionPoolSize, + null, //TODO Add correct proxy for tests + null, + null, + null, receiveBufferSize, replicaSetName, scheme, diff --git a/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs index 39ce99039df..4fcf8919061 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/socks5-support/Socks5SupportProseTests.cs @@ -25,9 +25,9 @@ namespace MongoDB.Driver.Tests.Specifications.socks5_support public class Socks5SupportProseTests(ITestOutputHelper testOutputHelper) : LoggableTestClass(testOutputHelper) { - [Theory] - [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&directConnection=true", false)] - [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true)] + // [Theory] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&directConnection=true", false)] + // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081&directConnection=true", true)] // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080", false)] // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true)] // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1080&proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true", false)] @@ -39,11 +39,13 @@ public class Socks5SupportProseTests(ITestOutputHelper testOutputHelper) : Logga // [InlineData("mongodb:///?proxyHost=localhost&proxyPort=1081", true)] public void TestConnectionStrings(string connectionString, bool expectedResult) { + connectionString = connectionString.Replace("", "localhost:27017"); var client = new MongoClient(connectionString); var database = client.GetDatabase("admin"); var command = new BsonDocument("hello", 1); var result = database.RunCommand(command); + Assert.NotEmpty(result); } }