Skip to content

Commit 9b36c67

Browse files
authored
Allow Multiple Wildcard Prefix Server Names of the Same Length in SNI Configuration (#41402)
1 parent 41c76ce commit 9b36c67

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,17 @@ private LongestStringFirstComparer()
225225
public int Compare(string? x, string? y)
226226
{
227227
// Flip x and y to put the longest instead of the shortest string first in the SortedList.
228-
return y!.Length.CompareTo(x!.Length);
228+
// SortedList does not support duplicate entries, so fall back to
229+
// StringComparison.OrdinalIgnoreCase behavior for equal length strings.
230+
var lengthResult = y!.Length.CompareTo(x!.Length);
231+
if (lengthResult != 0)
232+
{
233+
return lengthResult;
234+
}
235+
else
236+
{
237+
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
238+
}
229239
}
230240
}
231241
}

src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,91 @@ public void ServerNameMatchingIsCaseInsensitive()
186186
Assert.Equal("WildcardPrefix", pathDictionary[aSubdomainOptions.ServerCertificate]);
187187
}
188188

189+
[Fact]
190+
public void MultipleWildcardPrefixServerNamesOfSameLengthAreAllowed()
191+
{
192+
var sniDictionary = new Dictionary<string, SniConfig>
193+
{
194+
{
195+
"*.a.example.org",
196+
new SniConfig
197+
{
198+
Certificate = new CertificateConfig
199+
{
200+
Path = "a"
201+
}
202+
}
203+
},
204+
{
205+
"*.b.example.org",
206+
new SniConfig
207+
{
208+
Certificate = new CertificateConfig
209+
{
210+
Path = "b"
211+
}
212+
}
213+
}
214+
};
215+
216+
var mockCertificateConfigLoader = new MockCertificateConfigLoader();
217+
var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
218+
219+
var sniOptionsSelector = new SniOptionsSelector(
220+
"TestEndpointName",
221+
sniDictionary,
222+
mockCertificateConfigLoader,
223+
fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
224+
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
225+
logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>());
226+
227+
var (aSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "c.a.example.org");
228+
Assert.Equal("a", pathDictionary[aSubdomainOptions.ServerCertificate]);
229+
230+
var (bSubdomainOptions, _) = sniOptionsSelector.GetOptions(new MockConnectionContext(), "c.b.example.org");
231+
Assert.Equal("b", pathDictionary[bSubdomainOptions.ServerCertificate]);
232+
}
233+
234+
[Fact]
235+
public void DuplicateWildcardPrefixServerNamesThrowsArgumentException()
236+
{
237+
var sniDictionary = new Dictionary<string, SniConfig>
238+
{
239+
{
240+
"*.example.org",
241+
new SniConfig
242+
{
243+
Certificate = new CertificateConfig
244+
{
245+
Path = "a"
246+
}
247+
}
248+
},
249+
{
250+
"*.EXAMPLE.org",
251+
new SniConfig
252+
{
253+
Certificate = new CertificateConfig
254+
{
255+
Path = "b"
256+
}
257+
}
258+
}
259+
};
260+
261+
var mockCertificateConfigLoader = new MockCertificateConfigLoader();
262+
var pathDictionary = mockCertificateConfigLoader.CertToPathDictionary;
263+
264+
var exception = Assert.Throws<ArgumentException>(() => new SniOptionsSelector(
265+
"TestEndpointName",
266+
sniDictionary,
267+
mockCertificateConfigLoader,
268+
fallbackHttpsOptions: new HttpsConnectionAdapterOptions(),
269+
fallbackHttpProtocols: HttpProtocols.Http1AndHttp2,
270+
logger: Mock.Of<ILogger<HttpsConnectionMiddleware>>()));
271+
Assert.Equal("An item with the same key has already been added. Key: .EXAMPLE.org (Parameter 'key')", exception.Message);
272+
}
273+
189274
[Fact]
190275
public void GetOptionsThrowsAnAuthenticationExceptionIfThereIsNoMatchingSniSection()
191276
{

0 commit comments

Comments
 (0)