Skip to content

Commit f38f60f

Browse files
committed
Map ListenOptions.Protocols from IConfiguration #2903
1 parent de5ccb5 commit f38f60f

File tree

5 files changed

+241
-18
lines changed

5 files changed

+241
-18
lines changed

src/Kestrel.Core/Internal/ConfigurationReader.cs

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
99
{
1010
internal class ConfigurationReader
1111
{
12+
private const string ProtocolsKey = "Protocols";
13+
private const string CertificatesKey = "Certificates";
14+
private const string CertificateKey = "Certificate";
15+
private const string EndpointDefaultsKey = "EndpointDefaults";
16+
private const string EndpointsKey = "Endpoints";
17+
private const string UrlKey = "Url";
18+
1219
private IConfiguration _configuration;
1320
private IDictionary<string, CertificateConfig> _certificates;
1421
private IList<EndpointConfig> _endpoints;
22+
private EndpointDefaults _endpointDefaults;
1523

1624
public ConfigurationReader(IConfiguration configuration)
1725
{
@@ -31,6 +39,19 @@ public IDictionary<string, CertificateConfig> Certificates
3139
}
3240
}
3341

42+
public EndpointDefaults EndpointDefaults
43+
{
44+
get
45+
{
46+
if (_endpointDefaults == null)
47+
{
48+
ReadEndpointDefaults();
49+
}
50+
51+
return _endpointDefaults;
52+
}
53+
}
54+
3455
public IEnumerable<EndpointConfig> Endpoints
3556
{
3657
get
@@ -48,29 +69,42 @@ private void ReadCertificates()
4869
{
4970
_certificates = new Dictionary<string, CertificateConfig>(0);
5071

51-
var certificatesConfig = _configuration.GetSection("Certificates").GetChildren();
72+
var certificatesConfig = _configuration.GetSection(CertificatesKey).GetChildren();
5273
foreach (var certificateConfig in certificatesConfig)
5374
{
5475
_certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
5576
}
5677
}
5778

79+
// "EndpointDefaults": {
80+
// "Protocols": "Http1AndHttp2",
81+
// }
82+
private void ReadEndpointDefaults()
83+
{
84+
var configSection = _configuration.GetSection(EndpointDefaultsKey);
85+
_endpointDefaults = new EndpointDefaults()
86+
{
87+
Protocols = ParseProtocols(configSection[ProtocolsKey])
88+
};
89+
}
90+
5891
private void ReadEndpoints()
5992
{
6093
_endpoints = new List<EndpointConfig>();
6194

62-
var endpointsConfig = _configuration.GetSection("Endpoints").GetChildren();
95+
var endpointsConfig = _configuration.GetSection(EndpointsKey).GetChildren();
6396
foreach (var endpointConfig in endpointsConfig)
6497
{
6598
// "EndpointName": {
66-
        // "Url": "https://*:5463",
67-
        // "Certificate": {
68-
          // "Path": "testCert.pfx",
69-
          // "Password": "testPassword"
70-
       // }
99+
// "Url": "https://*:5463",
100+
// "Protocols": "Http1AndHttp2",
101+
// "Certificate": {
102+
// "Path": "testCert.pfx",
103+
// "Password": "testPassword"
104+
// }
71105
// }
72-
73-
var url = endpointConfig["Url"];
106+
107+
var url = endpointConfig[UrlKey];
74108
if (string.IsNullOrEmpty(url))
75109
{
76110
throw new InvalidOperationException(CoreStrings.FormatEndpointMissingUrl(endpointConfig.Key));
@@ -80,16 +114,37 @@ private void ReadEndpoints()
80114
{
81115
Name = endpointConfig.Key,
82116
Url = url,
117+
Protocols = ParseProtocols(endpointConfig[ProtocolsKey]),
83118
ConfigSection = endpointConfig,
84-
Certificate = new CertificateConfig(endpointConfig.GetSection("Certificate")),
119+
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
85120
};
86121
_endpoints.Add(endpoint);
87122
}
88123
}
124+
125+
private static HttpProtocols? ParseProtocols(string protocols)
126+
{
127+
if (Enum.TryParse<HttpProtocols>(protocols, ignoreCase: true, out var result))
128+
{
129+
return result;
130+
}
131+
132+
return null;
133+
}
134+
}
135+
136+
// "EndpointDefaults": {
137+
// "Protocols": "Http1AndHttp2",
138+
// }
139+
internal class EndpointDefaults
140+
{
141+
public HttpProtocols? Protocols { get; set; }
142+
public IConfigurationSection ConfigSection { get; set; }
89143
}
90144

91145
// "EndpointName": {
92146
// "Url": "https://*:5463",
147+
// "Protocols": "Http1AndHttp2",
93148
// "Certificate": {
94149
// "Path": "testCert.pfx",
95150
// "Password": "testPassword"
@@ -99,6 +154,7 @@ internal class EndpointConfig
99154
{
100155
public string Name { get; set; }
101156
public string Url { get; set; }
157+
public HttpProtocols? Protocols { get; set; }
102158
public IConfigurationSection ConfigSection { get; set; }
103159
public CertificateConfig Certificate { get; set; }
104160
}

src/Kestrel.Core/KestrelConfigurationLoader.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel
2323
{
2424
public class KestrelConfigurationLoader
2525
{
26+
private bool _loaded = false;
27+
2628
internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration)
2729
{
2830
Options = options ?? throw new ArgumentNullException(nameof(options));
2931
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
32+
ConfigurationReader = new ConfigurationReader(Configuration);
3033
}
3134

3235
public KestrelServerOptions Options { get; }
3336
public IConfiguration Configuration { get; }
37+
internal ConfigurationReader ConfigurationReader { get; }
3438
private IDictionary<string, Action<EndpointConfiguration>> EndpointConfigurations { get; }
3539
= new Dictionary<string, Action<EndpointConfiguration>>(0, StringComparer.OrdinalIgnoreCase);
3640
// Actions that will be delayed until Load so that they aren't applied if the configuration loader is replaced.
@@ -197,24 +201,39 @@ public KestrelConfigurationLoader HandleEndpoint(ulong handle, Action<ListenOpti
197201
return this;
198202
}
199203

204+
// Called from ApplyEndpointDefaults so it applies to even explicit Listen endpoints.
205+
// Does not require a call to Load.
206+
internal void ApplyConfigurationDefaults(ListenOptions listenOptions)
207+
{
208+
var defaults = ConfigurationReader.EndpointDefaults;
209+
210+
if (defaults.Protocols.HasValue)
211+
{
212+
listenOptions.Protocols = defaults.Protocols.Value;
213+
}
214+
}
215+
200216
public void Load()
201217
{
202-
if (Options.ConfigurationLoader == null)
218+
if (_loaded)
203219
{
204220
// The loader has already been run.
205221
return;
206222
}
207-
Options.ConfigurationLoader = null;
208-
209-
var configReader = new ConfigurationReader(Configuration);
223+
_loaded = true;
210224

211-
LoadDefaultCert(configReader);
225+
LoadDefaultCert(ConfigurationReader);
212226

213-
foreach (var endpoint in configReader.Endpoints)
227+
foreach (var endpoint in ConfigurationReader.Endpoints)
214228
{
215229
var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https);
216230
Options.ApplyEndpointDefaults(listenOptions);
217231

232+
if (endpoint.Protocols.HasValue)
233+
{
234+
listenOptions.Protocols = endpoint.Protocols.Value;
235+
}
236+
218237
// Compare to UseHttps(httpsOptions => { })
219238
var httpsOptions = new HttpsConnectionAdapterOptions();
220239
if (https)

src/Kestrel.Core/KestrelServerOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public void ConfigureEndpointDefaults(Action<ListenOptions> configureOptions)
103103
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
104104
{
105105
listenOptions.KestrelServerOptions = this;
106+
ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions);
106107
EndpointDefaults(listenOptions);
107108
}
108109

test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ public void CallBuildTwice_OnlyRunsOnce()
8181

8282
Assert.Single(serverOptions.ListenOptions);
8383
Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port);
84-
Assert.Null(serverOptions.ConfigurationLoader);
84+
Assert.NotNull(serverOptions.ConfigurationLoader);
8585

8686
builder.Load();
8787

8888
Assert.Single(serverOptions.ListenOptions);
8989
Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port);
90-
Assert.Null(serverOptions.ConfigurationLoader);
90+
Assert.NotNull(serverOptions.ConfigurationLoader);
9191
}
9292

9393
[Fact]
@@ -131,6 +131,7 @@ public void ConfigureDefaultsAppliesToNewConfigureEndpoints()
131131
serverOptions.ConfigureEndpointDefaults(opt =>
132132
{
133133
opt.NoDelay = false;
134+
opt.Protocols = HttpProtocols.Http2;
134135
});
135136

136137
serverOptions.ConfigureHttpsDefaults(opt =>
@@ -153,11 +154,13 @@ public void ConfigureDefaultsAppliesToNewConfigureEndpoints()
153154
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
154155
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
155156
Assert.False(opt.ListenOptions.NoDelay);
157+
Assert.Equal(HttpProtocols.Http2, opt.ListenOptions.Protocols);
156158
})
157159
.LocalhostEndpoint(5002, opt =>
158160
{
159161
ran2 = true;
160162
Assert.False(opt.NoDelay);
163+
Assert.Equal(HttpProtocols.Http2, opt.Protocols);
161164
})
162165
.Load();
163166

@@ -316,6 +319,119 @@ public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPfxFileDoesNotEx
316319
}
317320
}
318321

322+
[Theory]
323+
[InlineData("http1", HttpProtocols.Http1)]
324+
[InlineData("http2", HttpProtocols.Http2)]
325+
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)]
326+
public void DefaultConfigSectionCanSetProtocols(string input, HttpProtocols expected)
327+
{
328+
var serverOptions = CreateServerOptions();
329+
var ranDefault = false;
330+
serverOptions.ConfigureEndpointDefaults(opt =>
331+
{
332+
Assert.Equal(expected, opt.Protocols);
333+
ranDefault = true;
334+
});
335+
336+
serverOptions.ConfigureHttpsDefaults(opt =>
337+
{
338+
opt.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
339+
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
340+
});
341+
342+
var ran1 = false;
343+
var ran2 = false;
344+
var ran3 = false;
345+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
346+
{
347+
new KeyValuePair<string, string>("EndpointDefaults:Protocols", input),
348+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
349+
}).Build();
350+
serverOptions.Configure(config)
351+
.Endpoint("End1", opt =>
352+
{
353+
Assert.True(opt.IsHttps);
354+
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
355+
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
356+
Assert.Equal(expected, opt.ListenOptions.Protocols);
357+
ran1 = true;
358+
})
359+
.LocalhostEndpoint(5002, opt =>
360+
{
361+
Assert.Equal(expected, opt.Protocols);
362+
ran2 = true;
363+
})
364+
.Load();
365+
serverOptions.ListenAnyIP(0, opt =>
366+
{
367+
Assert.Equal(expected, opt.Protocols);
368+
ran3 = true;
369+
});
370+
371+
Assert.True(ranDefault);
372+
Assert.True(ran1);
373+
Assert.True(ran2);
374+
Assert.True(ran3);
375+
}
376+
377+
[Theory]
378+
[InlineData("http1", HttpProtocols.Http1)]
379+
[InlineData("http2", HttpProtocols.Http2)]
380+
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)]
381+
public void EndpointConfigSectionCanSetProtocols(string input, HttpProtocols expected)
382+
{
383+
var serverOptions = CreateServerOptions();
384+
var ranDefault = false;
385+
serverOptions.ConfigureEndpointDefaults(opt =>
386+
{
387+
// Kestrel default.
388+
Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols);
389+
ranDefault = true;
390+
});
391+
392+
serverOptions.ConfigureHttpsDefaults(opt =>
393+
{
394+
opt.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
395+
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
396+
});
397+
398+
var ran1 = false;
399+
var ran2 = false;
400+
var ran3 = false;
401+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
402+
{
403+
new KeyValuePair<string, string>("Endpoints:End1:Protocols", input),
404+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
405+
}).Build();
406+
serverOptions.Configure(config)
407+
.Endpoint("End1", opt =>
408+
{
409+
Assert.True(opt.IsHttps);
410+
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
411+
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
412+
Assert.Equal(expected, opt.ListenOptions.Protocols);
413+
ran1 = true;
414+
})
415+
.LocalhostEndpoint(5002, opt =>
416+
{
417+
// Kestrel default.
418+
Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols);
419+
ran2 = true;
420+
})
421+
.Load();
422+
serverOptions.ListenAnyIP(0, opt =>
423+
{
424+
// Kestrel default.
425+
Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols);
426+
ran3 = true;
427+
});
428+
429+
Assert.True(ranDefault);
430+
Assert.True(ran1);
431+
Assert.True(ran2);
432+
Assert.True(ran3);
433+
}
434+
319435
private static string GetCertificatePath()
320436
{
321437
var appData = Environment.GetEnvironmentVariable("APPDATA");

0 commit comments

Comments
 (0)