Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Commit e0df730

Browse files
committed
#2139 Add ListenLocalhost and ListenAnyIP
1 parent 065e9bb commit e0df730

File tree

6 files changed

+177
-96
lines changed

6 files changed

+177
-96
lines changed

samples/SampleApp/Startup.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ public static Task Main(string[] args)
7676
listenOptions.UseConnectionLogging();
7777
});
7878

79+
options.ListenLocalhost(basePort + 2, listenOptions =>
80+
{
81+
listenOptions.UseHttps("testCert.pfx", "testPassword");
82+
});
83+
84+
options.ListenAnyIP(basePort + 3);
85+
7986
options.UseSystemd();
8087

8188
// The following section should be used to demo sockets

src/Kestrel.Core/Internal/AddressBinder.cs

Lines changed: 108 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Linq;
89
using System.Net;
@@ -11,6 +12,7 @@
1112
using Microsoft.AspNetCore.Hosting.Server.Features;
1213
using Microsoft.AspNetCore.Protocols;
1314
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
15+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
1416
using Microsoft.Extensions.Logging;
1517

1618
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
@@ -109,140 +111,152 @@ protected internal static bool TryCreateIPEndPoint(ServerAddress address, out IP
109111
return true;
110112
}
111113

112-
private static Task BindEndpointAsync(IPEndPoint endpoint, AddressBindContext context)
113-
=> BindEndpointAsync(new ListenOptions(endpoint), context);
114-
115-
private static async Task BindEndpointAsync(ListenOptions endpoint, AddressBindContext context)
114+
private static ListenOptions ParseAddress(string address, AddressBindContext context)
116115
{
117-
try
116+
var parsedAddress = ServerAddress.FromUrl(address);
117+
var https = false;
118+
119+
if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
118120
{
119-
await context.CreateBinding(endpoint).ConfigureAwait(false);
121+
https = true;
120122
}
121-
catch (AddressInUseException ex)
123+
else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
122124
{
123-
throw new IOException(CoreStrings.FormatEndpointAlreadyInUse(endpoint), ex);
125+
throw new InvalidOperationException(CoreStrings.FormatUnsupportedAddressScheme(address));
124126
}
125127

126-
context.ListenOptions.Add(endpoint);
128+
if (!string.IsNullOrEmpty(parsedAddress.PathBase))
129+
{
130+
throw new InvalidOperationException(CoreStrings.FormatConfigurePathBaseFromMethodCall($"{nameof(IApplicationBuilder)}.UsePathBase()"));
131+
}
132+
133+
ListenOptions options = null;
134+
if (parsedAddress.IsUnixPipe)
135+
{
136+
options = new ListenOptions(parsedAddress.UnixPipePath);
137+
}
138+
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
139+
{
140+
if (parsedAddress.Port == 0)
141+
{
142+
throw new InvalidOperationException(CoreStrings.DynamicPortOnLocalhostNotSupported);
143+
}
144+
145+
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
146+
options = new ListenOptions(ListenType.Localhost)
147+
{
148+
IPEndPoint = new IPEndPoint(IPAddress.Loopback, parsedAddress.Port),
149+
};
150+
}
151+
else if (TryCreateIPEndPoint(parsedAddress, out var endpoint))
152+
{
153+
options = new ListenOptions(endpoint);
154+
}
155+
else
156+
{
157+
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
158+
options = new ListenOptions(ListenType.AnyIP) { IPEndPoint = new IPEndPoint(IPAddress.IPv6Any, parsedAddress.Port) };
159+
}
160+
161+
if (https)
162+
{
163+
options.KestrelServerOptions = context.ServerOptions;
164+
context.DefaultHttpsProvider.ConfigureHttps(options);
165+
}
166+
167+
return options;
127168
}
128169

129-
private static async Task BindLocalhostAsync(ServerAddress address, AddressBindContext context, bool https)
170+
private static async Task BindAsync(ListenOptions options, AddressBindContext context)
130171
{
131-
if (address.Port == 0)
172+
if (options.Type == ListenType.SocketPath
173+
|| options.Type == ListenType.IPEndPoint)
174+
{
175+
await BindEndpointAsync(options, context).ConfigureAwait(false);
176+
context.Addresses.Add(options.GetDisplayName());
177+
}
178+
else if (options.Type == ListenType.Localhost)
179+
{
180+
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
181+
await BindLocalhostAsync(options, context).ConfigureAwait(false);
182+
}
183+
else if (options.Type == ListenType.AnyIP)
184+
{
185+
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
186+
try
187+
{
188+
options = options.CloneAs(ListenType.IPEndPoint);
189+
options.IPEndPoint = new IPEndPoint(IPAddress.IPv6Any, options.IPEndPoint.Port);
190+
await BindEndpointAsync(options, context).ConfigureAwait(false);
191+
}
192+
catch (Exception ex) when (!(ex is IOException))
193+
{
194+
context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(options.IPEndPoint.Port));
195+
196+
// for machines that do not support IPv6
197+
options.IPEndPoint = new IPEndPoint(IPAddress.Any, options.IPEndPoint.Port);
198+
await BindEndpointAsync(options, context).ConfigureAwait(false);
199+
}
200+
context.Addresses.Add(options.GetDisplayName());
201+
}
202+
else
132203
{
133-
throw new InvalidOperationException(CoreStrings.DynamicPortOnLocalhostNotSupported);
204+
throw new NotImplementedException(options.Type.ToString());
134205
}
206+
}
207+
208+
private static async Task BindLocalhostAsync(ListenOptions options, AddressBindContext context)
209+
{
210+
Debug.Assert(options.Type == ListenType.Localhost);
135211

136212
var exceptions = new List<Exception>();
137213

138214
try
139215
{
140-
var options = new ListenOptions(new IPEndPoint(IPAddress.Loopback, address.Port));
141-
await BindEndpointAsync(options, context).ConfigureAwait(false);
142-
143-
if (https)
144-
{
145-
options.KestrelServerOptions = context.ServerOptions;
146-
context.DefaultHttpsProvider.ConfigureHttps(options);
147-
}
216+
var v4Options = options.CloneAs(ListenType.IPEndPoint);
217+
v4Options.IPEndPoint = new IPEndPoint(IPAddress.Loopback, v4Options.IPEndPoint.Port);
218+
await BindEndpointAsync(v4Options, context).ConfigureAwait(false);
148219
}
149220
catch (Exception ex) when (!(ex is IOException))
150221
{
151-
context.Logger.LogWarning(0, CoreStrings.NetworkInterfaceBindingFailed, address, "IPv4 loopback", ex.Message);
222+
context.Logger.LogWarning(0, CoreStrings.NetworkInterfaceBindingFailed, options.GetDisplayName(), "IPv4 loopback", ex.Message);
152223
exceptions.Add(ex);
153224
}
154225

155226
try
156227
{
157-
var options = new ListenOptions(new IPEndPoint(IPAddress.IPv6Loopback, address.Port));
158-
await BindEndpointAsync(options, context).ConfigureAwait(false);
159-
160-
if (https)
161-
{
162-
options.KestrelServerOptions = context.ServerOptions;
163-
context.DefaultHttpsProvider.ConfigureHttps(options);
164-
}
228+
var v6Options = options.CloneAs(ListenType.IPEndPoint);
229+
v6Options.IPEndPoint = new IPEndPoint(IPAddress.IPv6Loopback, v6Options.IPEndPoint.Port);
230+
await BindEndpointAsync(v6Options, context).ConfigureAwait(false);
165231
}
166232
catch (Exception ex) when (!(ex is IOException))
167233
{
168-
context.Logger.LogWarning(0, CoreStrings.NetworkInterfaceBindingFailed, address, "IPv6 loopback", ex.Message);
234+
context.Logger.LogWarning(0, CoreStrings.NetworkInterfaceBindingFailed, options.GetDisplayName(), "IPv6 loopback", ex.Message);
169235
exceptions.Add(ex);
170236
}
171237

172238
if (exceptions.Count == 2)
173239
{
174-
throw new IOException(CoreStrings.FormatAddressBindingFailed(address), new AggregateException(exceptions));
240+
throw new IOException(CoreStrings.FormatAddressBindingFailed(options.GetDisplayName()), new AggregateException(exceptions));
175241
}
176242

177243
// If StartLocalhost doesn't throw, there is at least one listener.
178244
// The port cannot change for "localhost".
179-
context.Addresses.Add(address.ToString());
245+
context.Addresses.Add(options.GetDisplayName());
180246
}
181247

182-
private static async Task BindAddressAsync(string address, AddressBindContext context)
248+
private static async Task BindEndpointAsync(ListenOptions endpoint, AddressBindContext context)
183249
{
184-
var parsedAddress = ServerAddress.FromUrl(address);
185-
var https = false;
186-
187-
if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
188-
{
189-
https = true;
190-
}
191-
else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
192-
{
193-
throw new InvalidOperationException(CoreStrings.FormatUnsupportedAddressScheme(address));
194-
}
195-
196-
if (!string.IsNullOrEmpty(parsedAddress.PathBase))
197-
{
198-
throw new InvalidOperationException(CoreStrings.FormatConfigurePathBaseFromMethodCall($"{nameof(IApplicationBuilder)}.UsePathBase()"));
199-
}
200-
201-
ListenOptions options = null;
202-
if (parsedAddress.IsUnixPipe)
203-
{
204-
options = new ListenOptions(parsedAddress.UnixPipePath);
205-
await BindEndpointAsync(options, context).ConfigureAwait(false);
206-
context.Addresses.Add(options.GetDisplayName());
207-
}
208-
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
250+
try
209251
{
210-
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
211-
await BindLocalhostAsync(parsedAddress, context, https).ConfigureAwait(false);
252+
await context.CreateBinding(endpoint).ConfigureAwait(false);
212253
}
213-
else
254+
catch (AddressInUseException ex)
214255
{
215-
if (TryCreateIPEndPoint(parsedAddress, out var endpoint))
216-
{
217-
options = new ListenOptions(endpoint);
218-
await BindEndpointAsync(options, context).ConfigureAwait(false);
219-
}
220-
else
221-
{
222-
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
223-
try
224-
{
225-
options = new ListenOptions(new IPEndPoint(IPAddress.IPv6Any, parsedAddress.Port));
226-
await BindEndpointAsync(options, context).ConfigureAwait(false);
227-
}
228-
catch (Exception ex) when (!(ex is IOException))
229-
{
230-
context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(parsedAddress.Port));
231-
232-
// for machines that do not support IPv6
233-
options = new ListenOptions(new IPEndPoint(IPAddress.Any, parsedAddress.Port));
234-
await BindEndpointAsync(options, context).ConfigureAwait(false);
235-
}
236-
}
237-
238-
context.Addresses.Add(options.GetDisplayName());
256+
throw new IOException(CoreStrings.FormatEndpointAlreadyInUse(endpoint), ex);
239257
}
240258

241-
if (https && options != null)
242-
{
243-
options.KestrelServerOptions = context.ServerOptions;
244-
context.DefaultHttpsProvider.ConfigureHttps(options);
245-
}
259+
context.ListenOptions.Add(endpoint);
246260
}
247261

248262
private interface IStrategy
@@ -256,7 +270,7 @@ public async Task BindAsync(AddressBindContext context)
256270
{
257271
context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress);
258272

259-
await BindLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), context, https: false).ConfigureAwait(false);
273+
await AddressBinder.BindAsync(ParseAddress(Constants.DefaultServerAddress, context), context).ConfigureAwait(false);
260274
}
261275
}
262276

@@ -308,9 +322,7 @@ public virtual async Task BindAsync(AddressBindContext context)
308322
{
309323
foreach (var endpoint in _endpoints)
310324
{
311-
await BindEndpointAsync(endpoint, context).ConfigureAwait(false);
312-
313-
context.Addresses.Add(endpoint.GetDisplayName());
325+
await AddressBinder.BindAsync(endpoint, context).ConfigureAwait(false);
314326
}
315327
}
316328
}
@@ -328,7 +340,8 @@ public virtual async Task BindAsync(AddressBindContext context)
328340
{
329341
foreach (var address in _addresses)
330342
{
331-
await BindAddressAsync(address, context).ConfigureAwait(false);
343+
var options = ParseAddress(address, context);
344+
await AddressBinder.BindAsync(options, context).ConfigureAwait(false);
332345
}
333346
}
334347
}

src/Kestrel.Core/KestrelServerOptions.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,42 @@ public void Listen(IPEndPoint endPoint, Action<ListenOptions> configure)
105105
ListenOptions.Add(listenOptions);
106106
}
107107

108+
public void ListenLocalhost(int port) => ListenLocalhost(port, options => { });
109+
110+
public void ListenLocalhost(int port, Action<ListenOptions> configure)
111+
{
112+
if (configure == null)
113+
{
114+
throw new ArgumentNullException(nameof(configure));
115+
}
116+
117+
var listenOptions = new ListenOptions(ListenType.Localhost)
118+
{
119+
KestrelServerOptions = this,
120+
IPEndPoint = new IPEndPoint(IPAddress.Loopback, port),
121+
};
122+
configure(listenOptions);
123+
ListenOptions.Add(listenOptions);
124+
}
125+
126+
public void ListenAnyIP(int port) => ListenAnyIP(port, options => { });
127+
128+
public void ListenAnyIP(int port, Action<ListenOptions> configure)
129+
{
130+
if (configure == null)
131+
{
132+
throw new ArgumentNullException(nameof(configure));
133+
}
134+
135+
var listenOptions = new ListenOptions(ListenType.AnyIP)
136+
{
137+
KestrelServerOptions = this,
138+
IPEndPoint = new IPEndPoint(IPAddress.IPv6Any, port),
139+
};
140+
configure(listenOptions);
141+
ListenOptions.Add(listenOptions);
142+
}
143+
108144
/// <summary>
109145
/// Bind to given Unix domain socket path.
110146
/// </summary>

src/Kestrel.Core/ListenOptions.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public class ListenOptions : IEndPointInformation, IConnectionBuilder
2121
private FileHandleType _handleType;
2222
private readonly List<Func<ConnectionDelegate, ConnectionDelegate>> _components = new List<Func<ConnectionDelegate, ConnectionDelegate>>();
2323

24+
internal ListenOptions(ListenType type)
25+
{
26+
Type = type;
27+
}
28+
2429
internal ListenOptions(IPEndPoint endPoint)
2530
{
2631
Type = ListenType.IPEndPoint;
@@ -148,8 +153,11 @@ internal string GetDisplayName()
148153

149154
switch (Type)
150155
{
156+
case ListenType.AnyIP:
151157
case ListenType.IPEndPoint:
152158
return $"{scheme}://{IPEndPoint}";
159+
case ListenType.Localhost:
160+
return $"{scheme}://localhost:{IPEndPoint.Port}";
153161
case ListenType.SocketPath:
154162
return $"{scheme}://unix:{SocketPath}";
155163
case ListenType.FileHandle:
@@ -182,5 +190,20 @@ public ConnectionDelegate Build()
182190

183191
return app;
184192
}
193+
194+
// primarily used for cloning localhost to two IPEndpoints
195+
internal ListenOptions CloneAs(ListenType type)
196+
{
197+
var options = new ListenOptions(type)
198+
{
199+
HandleType = HandleType,
200+
IPEndPoint = IPEndPoint,
201+
KestrelServerOptions = KestrelServerOptions,
202+
NoDelay = NoDelay,
203+
Protocols = Protocols,
204+
};
205+
options.ConnectionAdapters.AddRange(ConnectionAdapters);
206+
return options;
207+
}
185208
}
186209
}

src/Kestrel.Transport.Abstractions/Internal/ListenType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
99
public enum ListenType
1010
{
1111
IPEndPoint,
12+
Localhost,
13+
AnyIP,
1214
SocketPath,
1315
FileHandle
1416
}

0 commit comments

Comments
 (0)