Commit 2b9a2a5f authored by Marc Gravell's avatar Marc Gravell Committed by GitHub

Merge pull request #603 from JonCole/TlsConfig

Add SslProtocols property to ConfigurationOptions
parents 0476eb83 3d37c4cf
......@@ -62,27 +62,28 @@ Configuration Options
The `ConfigurationOptions` object has a wide range of properties, all of which are fully documented in intellisense. Some of the more common options to use include:
| Configuration string | `ConfigurationOptions` | Default | Meaning |
| ---------------------- | ---------------------- | ---------------------------- | -------------------------------------------------------------------------------- |
| abortConnect={bool} | `AbortOnConnectFail` | `true` (`false` on Azure) | If true, `Connect` will not create a connection while no servers are available |
| allowAdmin={bool} | `AllowAdmin` | `false` | Enables a range of commands that are considered risky |
| channelPrefix={string} | `ChannelPrefix` | `null` | Optional channel prefix for all pub/sub operations |
| connectRetry={int} | `ConnectRetry` | `3` | The number of times to repeat connect attempts during initial `Connect` |
| connectTimeout={int} | `ConnectTimeout` | `5000` | Timeout (ms) for connect operations |
| configChannel={string} | `ConfigurationChannel` | `__Booksleeve_MasterChanged` | Broadcast channel name for communicating configuration changes |
| defaultDatabase={int} | `DefaultDatabase` | `null` | Default database index, from `0` to `databases - 1` |
| keepAlive={int} | `KeepAlive` | `-1` | Time (seconds) at which to send a message to help keep sockets alive |
| name={string} | `ClientName` | `null` | Identification for the connection within redis |
| password={string} | `Password` | `null` | Password for the redis server |
| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
| serviceName={string} | `ServiceName` | `null` | Not currently implemented (intended for use with sentinel) |
| ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used |
| sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate |
| syncTimeout={int} | `SyncTimeout` | `1000` | Time (ms) to allow for synchronous operations |
| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous master scenario |
| version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) |
| writeBuffer={int} | `WriteBuffer` | `4096` | Size of the output buffer |
| Configuration string | `ConfigurationOptions` | Default | Meaning |
| ---------------------- | ---------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| abortConnect={bool} | `AbortOnConnectFail` | `true` (`false` on Azure) | If true, `Connect` will not create a connection while no servers are available |
| allowAdmin={bool} | `AllowAdmin` | `false` | Enables a range of commands that are considered risky |
| channelPrefix={string} | `ChannelPrefix` | `null` | Optional channel prefix for all pub/sub operations |
| connectRetry={int} | `ConnectRetry` | `3` | The number of times to repeat connect attempts during initial `Connect` |
| connectTimeout={int} | `ConnectTimeout` | `5000` | Timeout (ms) for connect operations |
| configChannel={string} | `ConfigurationChannel` | `__Booksleeve_MasterChanged` | Broadcast channel name for communicating configuration changes |
| defaultDatabase={int} | `DefaultDatabase` | `null` | Default database index, from `0` to `databases - 1` |
| keepAlive={int} | `KeepAlive` | `-1` | Time (seconds) at which to send a message to help keep sockets alive |
| name={string} | `ClientName` | `null` | Identification for the connection within redis |
| password={string} | `Password` | `null` | Password for the redis server |
| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
| serviceName={string} | `ServiceName` | `null` | Not currently implemented (intended for use with sentinel) |
| ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used |
| sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate |
| sslProtocols={enum} | `SslProtocols` | `null` | Ssl/Tls versions supported when using an encrypted connection. Use '\|' to provide multiple values. |
| syncTimeout={int} | `SyncTimeout` | `1000` | Time (ms) to allow for synchronous operations |
| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous master scenario |
| version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) |
| writeBuffer={int} | `WriteBuffer` | `4096` | Size of the output buffer |
Additional code-only options:
- ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = LinearRetry(ConnectTimeout);`
......
......@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Authentication;
using System.Threading.Tasks;
using NUnit.Framework;
using StackExchange.Redis;
......@@ -187,6 +188,42 @@ public void CreateDisconnectedNonsenseConnection_DNS()
}
}
[Test]
public void SslProtocols_SingleValue()
{
var log = new StringWriter();
var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11");
Assert.AreEqual(SslProtocols.Tls11, options.SslProtocols.Value);
}
[Test]
public void SslProtocols_MultipleValues()
{
var log = new StringWriter();
var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11|Tls12");
Assert.AreEqual(SslProtocols.Tls11|SslProtocols.Tls12, options.SslProtocols.Value);
}
[Test]
public void SslProtocols_UsingIntegerValue()
{
var log = new StringWriter();
// The below scenario is for cases where the *targeted*
// .NET framework version (e.g. .NET 4.0) doesn't define an enum value (e.g. Tls11)
// but the OS has been patched with support
int integerValue = (int)(SslProtocols.Tls11 | SslProtocols.Tls12);
var options = ConfigurationOptions.Parse("myhost,sslProtocols=" + integerValue);
Assert.AreEqual(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.Value);
}
[Test]
public void SslProtocols_InvalidValue()
{
var log = new StringWriter();
Assert.Throws<ArgumentOutOfRangeException>(() => ConfigurationOptions.Parse("myhost,sslProtocols=InvalidSslProtocol"));
}
[Test]
public void ConfigurationOptionsDefaultForAzure()
{
......
......@@ -5,6 +5,7 @@
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
......@@ -66,6 +67,17 @@ internal static Proxy ParseProxy(string key, string value)
return tmp;
}
internal static SslProtocols ParseSslProtocols(string key, string value)
{
SslProtocols tmp;
//Flags expect commas as separators, but we need to use '|' since commas are already used in the connection string to mean something else
value = value?.Replace("|", ",");
if (!Enum.TryParse(value, true, out tmp)) throw new ArgumentOutOfRangeException("Keyword '" + key + "' requires an SslProtocol value (multiple values separated by '|').");
return tmp;
}
internal static void Unknown(string key)
{
throw new ArgumentException("Keyword '" + key + "' is not supported");
......@@ -78,6 +90,8 @@ internal static void Unknown(string key)
ConfigChannel = "configChannel", AbortOnConnectFail = "abortConnect", ResolveDns = "resolveDns",
ChannelPrefix = "channelPrefix", Proxy = "proxy", ConnectRetry = "connectRetry",
ConfigCheckSeconds = "configCheckSeconds", ResponseTimeout = "responseTimeout", DefaultDatabase = "defaultDatabase";
internal const string SslProtocols = "sslProtocols";
private static readonly Dictionary<string, string> normalizedOptions = new[]
{
AllowAdmin, SyncTimeout,
......@@ -87,6 +101,7 @@ internal static void Unknown(string key)
ConfigChannel, AbortOnConnectFail, ResolveDns,
ChannelPrefix, Proxy, ConnectRetry,
ConfigCheckSeconds, DefaultDatabase,
SslProtocols,
}.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase);
public static string TryNormalize(string value)
......@@ -140,7 +155,7 @@ public static string TryNormalize(string value)
/// Indicates whether admin operations should be allowed
/// </summary>
public bool AllowAdmin { get { return allowAdmin.GetValueOrDefault(); } set { allowAdmin = value; } }
/// <summary>
/// Indicates whether the connection should be encrypted
/// </summary>
......@@ -156,6 +171,11 @@ public static string TryNormalize(string value)
/// </summary>
public bool Ssl { get { return ssl.GetValueOrDefault(); } set { ssl = value; } }
/// <summary>
/// Configures which Ssl/TLS protocols should be allowed. If not set, defaults are chosen by the .NET framework.
/// </summary>
public SslProtocols? SslProtocols { get; set; }
/// <summary>
/// Automatically encodes and decodes channels
/// </summary>
......@@ -357,8 +377,11 @@ public ConfigurationOptions Clone()
connectRetry = connectRetry,
configCheckSeconds = configCheckSeconds,
responseTimeout = responseTimeout,
defaultDatabase = defaultDatabase,
defaultDatabase = defaultDatabase,
ReconnectRetryPolicy = reconnectRetryPolicy,
#if !CORE_CLR
SslProtocols = SslProtocols,
#endif
};
foreach (var item in endpoints)
options.endpoints.Add(item);
......@@ -622,6 +645,11 @@ private void DoParse(string configuration, bool ignoreUnknown)
case OptionKeys.DefaultDatabase:
defaultDatabase = OptionKeys.ParseInt32(key, value);
break;
#if !CORE_CLR
case OptionKeys.SslProtocols:
SslProtocols = OptionKeys.ParseSslProtocols(key, value);
break;
#endif
default:
if (!string.IsNullOrEmpty(key) && key[0] == '$')
{
......@@ -660,7 +688,7 @@ private bool GetDefaultAbortOnConnectFailSetting()
return true;
}
private bool IsAzureEndpoint()
{
var result = false;
......
using System;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace StackExchange.Redis
{
......@@ -122,5 +125,33 @@ public static string[] ToStringArray(this RedisValue[] values)
if (values.Length == 0) return nix;
return ConvertHelper.ConvertAll(values, x => (string)x);
}
internal static void AuthenticateAsClient(this SslStream ssl, string host, SslProtocols? allowedProtocols)
{
if (!allowedProtocols.HasValue)
{
//Default to the sslProtocols defined by the .NET Framework
AuthenticateAsClientUsingDefaultProtocols(ssl, host);
return;
}
var certificateCollection = new X509CertificateCollection();
const bool checkCertRevocation = true;
#if CORE_CLR
ssl.AuthenticateAsClientAsync(host, certificateCollection, allowedProtocols.Value, checkCertRevocation)
.GetAwaiter().GetResult();
#else
ssl.AuthenticateAsClient(host, certificateCollection, allowedProtocols.Value, checkCertRevocation);
#endif
}
private static void AuthenticateAsClientUsingDefaultProtocols(SslStream ssl, string host)
{
#if CORE_CLR
ssl.AuthenticateAsClientAsync(host).GetAwaiter().GetResult();
#else
ssl.AuthenticateAsClient(host);
#endif
}
}
}
......@@ -783,11 +783,9 @@ SocketMode ISocketCallback.Connected(Stream stream, TextWriter log)
);
try
{
#if CORE_CLR
ssl.AuthenticateAsClientAsync(host).GetAwaiter().GetResult();
#else
ssl.AuthenticateAsClient(host);
#endif
ssl.AuthenticateAsClient(host, config.SslProtocols);
Multiplexer.LogLocked(log, $"SSL connection established successfully using protocol: {ssl.SslProtocol}");
}
catch (AuthenticationException authexception)
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment