Skip to content

Commit f734f18

Browse files
committed
Parse enum unions in config
In particular, `HttpProtocols` and `SslProtocols`. This should be more forward compatible than adding an explicit enum member for each combination of values. For #58088
1 parent 44a9f8a commit f734f18

File tree

2 files changed

+167
-12
lines changed

2 files changed

+167
-12
lines changed

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,37 +175,91 @@ private static Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniCo
175175

176176
private static HttpProtocols? ParseProtocols(string? protocols)
177177
{
178-
if (Enum.TryParse<HttpProtocols>(protocols, ignoreCase: true, out var result))
178+
if (protocols is null)
179+
{
180+
return null;
181+
}
182+
183+
if (protocols.Contains('|'))
184+
{
185+
var split = protocols.Split('|');
186+
var result = HttpProtocols.None;
187+
foreach (var protocol in split)
188+
{
189+
// Enum.TryParse handles trimming
190+
if (!TryParseProtocol(protocol, out var parsed))
191+
{
192+
return null;
193+
}
194+
result |= parsed;
195+
}
196+
return result;
197+
}
198+
else if (TryParseProtocol(protocols, out var result))
179199
{
180200
return result;
181201
}
182202

183203
return null;
204+
205+
static bool TryParseProtocol(string protocol, [NotNullWhen(true)] out HttpProtocols result)
206+
{
207+
if (Enum.TryParse<HttpProtocols>(protocol, ignoreCase: true, out var parsed))
208+
{
209+
result = parsed;
210+
return true;
211+
}
212+
result = default;
213+
return false;
214+
}
184215
}
185216

186217
private static SslProtocols? ParseSslProcotols(IConfigurationSection sslProtocols)
187218
{
188219
// Avoid trimming warning from IConfigurationSection.Get<string[]>()
189-
string[]? stringProtocols = null;
190220
var childrenSections = sslProtocols.GetChildren().ToArray();
191-
if (childrenSections.Length > 0)
221+
if (childrenSections.Length == 0)
222+
{
223+
return null;
224+
}
225+
226+
var result = SslProtocols.None;
227+
228+
foreach (var childrenSection in childrenSections)
192229
{
193-
stringProtocols = new string[childrenSections.Length];
194-
for (var i = 0; i < childrenSections.Length; i++)
230+
var stringProtocols = childrenSection.Value!;
231+
if (stringProtocols.Contains('|'))
195232
{
196-
stringProtocols[i] = childrenSections[i].Value!;
233+
var split = stringProtocols.Split('|');
234+
foreach (var stringProtocol in split)
235+
{
236+
// Enum.TryParse handles trimming
237+
if (!TryParseSslProtocol(stringProtocol, out var parsed))
238+
{
239+
// A bad value in any list clobbers all lists
240+
return SslProtocols.None;
241+
}
242+
result |= parsed;
243+
}
244+
}
245+
else if (TryParseSslProtocol(stringProtocols, out var parsed))
246+
{
247+
result |= parsed;
197248
}
198249
}
199250

200-
return stringProtocols?.Aggregate(SslProtocols.None, (acc, current) =>
251+
return result;
252+
253+
static bool TryParseSslProtocol(string protocol, [NotNullWhen(true)] out SslProtocols result)
201254
{
202-
if (Enum.TryParse(current, ignoreCase: true, out SslProtocols parsed))
255+
if (Enum.TryParse<SslProtocols>(protocol, ignoreCase: true, out var parsed))
203256
{
204-
return acc | parsed;
257+
result = parsed;
258+
return true;
205259
}
206-
207-
return acc;
208-
});
260+
result = default;
261+
return false;
262+
}
209263
}
210264

211265
internal static void ThrowIfContainsHttpsOnlyConfiguration(EndpointConfig endpoint)

src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,62 @@ public void ReadEndpointsSection_ReturnsCollection()
218218
Assert.True(cert4.AllowInvalid);
219219
}
220220

221+
[Fact]
222+
public void ReadEndpointWithSingleHttpProtocolSet_ReturnsCorrectValue()
223+
{
224+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
225+
{
226+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
227+
new KeyValuePair<string, string>("Endpoints:End1:Protocols", "Http1"),
228+
}).Build();
229+
var reader = new ConfigurationReader(config);
230+
231+
var endpoint = reader.Endpoints.First();
232+
Assert.Equal(HttpProtocols.Http1, endpoint.Protocols);
233+
}
234+
235+
[Fact]
236+
public void ReadEndpointWithMultipleHttpProtocolsSet_ReturnsCorrectValue()
237+
{
238+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
239+
{
240+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
241+
new KeyValuePair<string, string>("Endpoints:End1:Protocols", "Http1AndHttp2"),
242+
}).Build();
243+
var reader = new ConfigurationReader(config);
244+
245+
var endpoint = reader.Endpoints.First();
246+
Assert.Equal(HttpProtocols.Http1AndHttp2, endpoint.Protocols);
247+
}
248+
249+
[Fact]
250+
public void ReadEndpointWithUnionHttpProtocolsSet_ReturnsCorrectValue()
251+
{
252+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
253+
{
254+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
255+
new KeyValuePair<string, string>("Endpoints:End1:Protocols", "Http2 | Http1"),
256+
}).Build();
257+
var reader = new ConfigurationReader(config);
258+
259+
var endpoint = reader.Endpoints.First();
260+
Assert.Equal(HttpProtocols.Http1AndHttp2, endpoint.Protocols);
261+
}
262+
263+
[Fact]
264+
public void ReadEndpointWithUnionHttpProtocolsSet_ReturnsNullForBadPart()
265+
{
266+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
267+
{
268+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
269+
new KeyValuePair<string, string>("Endpoints:End1:Protocols", "Http2 | Http0"),
270+
}).Build();
271+
var reader = new ConfigurationReader(config);
272+
273+
var endpoint = reader.Endpoints.First();
274+
Assert.Null(endpoint.Protocols);
275+
}
276+
221277
[Fact]
222278
public void ReadEndpointWithSingleSslProtocolSet_ReturnsCorrectValue()
223279
{
@@ -251,6 +307,51 @@ public void ReadEndpointWithMultipleSslProtocolsSet_ReturnsCorrectValue()
251307
#pragma warning restore SYSLIB0039
252308
}
253309

310+
[Fact]
311+
public void ReadEndpointWithUnionSslProtocolSet_ReturnsCorrectValue()
312+
{
313+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
314+
{
315+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
316+
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls12 | Tls11"),
317+
}).Build();
318+
var reader = new ConfigurationReader(config);
319+
320+
var endpoint = reader.Endpoints.First();
321+
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
322+
Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, endpoint.SslProtocols);
323+
#pragma warning restore SYSLIB0039
324+
}
325+
326+
[Fact]
327+
public void ReadEndpointWithUnionSslProtocolSet_ReturnsNoneForBadPart()
328+
{
329+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
330+
{
331+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
332+
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls12 | Tls0"),
333+
}).Build();
334+
var reader = new ConfigurationReader(config);
335+
336+
var endpoint = reader.Endpoints.First();
337+
Assert.Equal(SslProtocols.None, endpoint.SslProtocols);
338+
}
339+
340+
[Fact]
341+
public void ReadEndpointWithUnionSslProtocolSet_ReturnsNoneForBadPartInAnyChild()
342+
{
343+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
344+
{
345+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
346+
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
347+
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:1", "Tls12 | Tls0"),
348+
}).Build();
349+
var reader = new ConfigurationReader(config);
350+
351+
var endpoint = reader.Endpoints.First();
352+
Assert.Equal(SslProtocols.None, endpoint.SslProtocols);
353+
}
354+
254355
[Fact]
255356
public void ReadEndpointWithSslProtocolSet_ReadsCaseInsensitive()
256357
{

0 commit comments

Comments
 (0)