394 lines
15 KiB
C#
394 lines
15 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Microsoft.AspNetCore.Certificates.Generation;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.AspNetCore.Server.Kestrel
|
|
{
|
|
public class KestrelConfigurationLoader
|
|
{
|
|
private bool _loaded = false;
|
|
|
|
internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration)
|
|
{
|
|
Options = options ?? throw new ArgumentNullException(nameof(options));
|
|
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
|
ConfigurationReader = new ConfigurationReader(Configuration);
|
|
}
|
|
|
|
public KestrelServerOptions Options { get; }
|
|
public IConfiguration Configuration { get; }
|
|
internal ConfigurationReader ConfigurationReader { get; }
|
|
private IDictionary<string, Action<EndpointConfiguration>> EndpointConfigurations { get; }
|
|
= new Dictionary<string, Action<EndpointConfiguration>>(0, StringComparer.OrdinalIgnoreCase);
|
|
// Actions that will be delayed until Load so that they aren't applied if the configuration loader is replaced.
|
|
private IList<Action> EndpointsToAdd { get; } = new List<Action>();
|
|
|
|
/// <summary>
|
|
/// Specifies a configuration Action to run when an endpoint with the given name is loaded from configuration.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader Endpoint(string name, Action<EndpointConfiguration> configureOptions)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
throw new ArgumentNullException(nameof(name));
|
|
}
|
|
|
|
EndpointConfigurations[name] = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions));
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bind to given IP address and port.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader Endpoint(IPAddress address, int port) => Endpoint(address, port, _ => { });
|
|
|
|
/// <summary>
|
|
/// Bind to given IP address and port.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader Endpoint(IPAddress address, int port, Action<ListenOptions> configure)
|
|
{
|
|
if (address == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(address));
|
|
}
|
|
|
|
return Endpoint(new IPEndPoint(address, port), configure);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bind to given IP endpoint.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint) => Endpoint(endPoint, _ => { });
|
|
|
|
/// <summary>
|
|
/// Bind to given IP address and port.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint, Action<ListenOptions> configure)
|
|
{
|
|
if (endPoint == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(endPoint));
|
|
}
|
|
if (configure == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configure));
|
|
}
|
|
|
|
EndpointsToAdd.Add(() =>
|
|
{
|
|
Options.Listen(endPoint, configure);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported
|
|
/// for this type of endpoint.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader LocalhostEndpoint(int port) => LocalhostEndpoint(port, options => { });
|
|
|
|
/// <summary>
|
|
/// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported
|
|
/// for this type of endpoint.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader LocalhostEndpoint(int port, Action<ListenOptions> configure)
|
|
{
|
|
if (configure == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configure));
|
|
}
|
|
|
|
EndpointsToAdd.Add(() =>
|
|
{
|
|
Options.ListenLocalhost(port, configure);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader AnyIPEndpoint(int port) => AnyIPEndpoint(port, options => { });
|
|
|
|
/// <summary>
|
|
/// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader AnyIPEndpoint(int port, Action<ListenOptions> configure)
|
|
{
|
|
if (configure == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configure));
|
|
}
|
|
|
|
EndpointsToAdd.Add(() =>
|
|
{
|
|
Options.ListenAnyIP(port, configure);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bind to given Unix domain socket path.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath) => UnixSocketEndpoint(socketPath, _ => { });
|
|
|
|
/// <summary>
|
|
/// Bind to given Unix domain socket path.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath, Action<ListenOptions> configure)
|
|
{
|
|
if (socketPath == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(socketPath));
|
|
}
|
|
if (socketPath.Length == 0 || socketPath[0] != '/')
|
|
{
|
|
throw new ArgumentException(CoreStrings.UnixSocketPathMustBeAbsolute, nameof(socketPath));
|
|
}
|
|
if (configure == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configure));
|
|
}
|
|
|
|
EndpointsToAdd.Add(() =>
|
|
{
|
|
Options.ListenUnixSocket(socketPath, configure);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open a socket file descriptor.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader HandleEndpoint(ulong handle) => HandleEndpoint(handle, _ => { });
|
|
|
|
/// <summary>
|
|
/// Open a socket file descriptor.
|
|
/// </summary>
|
|
public KestrelConfigurationLoader HandleEndpoint(ulong handle, Action<ListenOptions> configure)
|
|
{
|
|
if (configure == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(configure));
|
|
}
|
|
|
|
EndpointsToAdd.Add(() =>
|
|
{
|
|
Options.ListenHandle(handle, configure);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
// Called from ApplyEndpointDefaults so it applies to even explicit Listen endpoints.
|
|
// Does not require a call to Load.
|
|
internal void ApplyConfigurationDefaults(ListenOptions listenOptions)
|
|
{
|
|
var defaults = ConfigurationReader.EndpointDefaults;
|
|
|
|
if (defaults.Protocols.HasValue)
|
|
{
|
|
listenOptions.Protocols = defaults.Protocols.Value;
|
|
}
|
|
}
|
|
|
|
public void Load()
|
|
{
|
|
if (_loaded)
|
|
{
|
|
// The loader has already been run.
|
|
return;
|
|
}
|
|
_loaded = true;
|
|
|
|
LoadDefaultCert(ConfigurationReader);
|
|
|
|
foreach (var endpoint in ConfigurationReader.Endpoints)
|
|
{
|
|
var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https);
|
|
Options.ApplyEndpointDefaults(listenOptions);
|
|
|
|
if (endpoint.Protocols.HasValue)
|
|
{
|
|
listenOptions.Protocols = endpoint.Protocols.Value;
|
|
}
|
|
|
|
// Compare to UseHttps(httpsOptions => { })
|
|
var httpsOptions = new HttpsConnectionAdapterOptions();
|
|
if (https)
|
|
{
|
|
// Defaults
|
|
Options.ApplyHttpsDefaults(httpsOptions);
|
|
|
|
// Specified
|
|
httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
|
|
?? httpsOptions.ServerCertificate;
|
|
|
|
// Fallback
|
|
Options.ApplyDefaultCert(httpsOptions);
|
|
}
|
|
|
|
if (EndpointConfigurations.TryGetValue(endpoint.Name, out var configureEndpoint))
|
|
{
|
|
var endpointConfig = new EndpointConfiguration(https, listenOptions, httpsOptions, endpoint.ConfigSection);
|
|
configureEndpoint(endpointConfig);
|
|
}
|
|
|
|
// EndpointDefaults or configureEndpoint may have added an https adapter.
|
|
if (https && !listenOptions.ConnectionAdapters.Any(f => f.IsHttps))
|
|
{
|
|
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
|
|
{
|
|
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
|
|
}
|
|
|
|
listenOptions.UseHttps(httpsOptions);
|
|
}
|
|
|
|
Options.ListenOptions.Add(listenOptions);
|
|
}
|
|
|
|
foreach (var action in EndpointsToAdd)
|
|
{
|
|
action();
|
|
}
|
|
}
|
|
|
|
private void LoadDefaultCert(ConfigurationReader configReader)
|
|
{
|
|
if (configReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
|
|
{
|
|
var defaultCert = LoadCertificate(defaultCertConfig, "Default");
|
|
if (defaultCert != null)
|
|
{
|
|
Options.DefaultCertificate = defaultCert;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var logger = Options.ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
|
|
var certificate = FindDeveloperCertificateFile(configReader, logger);
|
|
if (certificate != null)
|
|
{
|
|
logger.LocatedDevelopmentCertificate(certificate);
|
|
Options.DefaultCertificate = certificate;
|
|
}
|
|
}
|
|
}
|
|
|
|
private X509Certificate2 FindDeveloperCertificateFile(ConfigurationReader configReader, ILogger<KestrelServer> logger)
|
|
{
|
|
string certificatePath = null;
|
|
try
|
|
{
|
|
if (configReader.Certificates.TryGetValue("Development", out var certificateConfig) &&
|
|
certificateConfig.Path == null &&
|
|
certificateConfig.Password != null &&
|
|
TryGetCertificatePath(out certificatePath) &&
|
|
File.Exists(certificatePath))
|
|
{
|
|
var certificate = new X509Certificate2(certificatePath, certificateConfig.Password);
|
|
return IsDevelopmentCertificate(certificate) ? certificate : null;
|
|
}
|
|
else if (!File.Exists(certificatePath))
|
|
{
|
|
logger.FailedToLocateDevelopmentCertificateFile(certificatePath);
|
|
}
|
|
}
|
|
catch (CryptographicException)
|
|
{
|
|
logger.FailedToLoadDevelopmentCertificate(certificatePath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool IsDevelopmentCertificate(X509Certificate2 certificate)
|
|
{
|
|
if (!string.Equals(certificate.Subject, "CN=localhost", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var ext in certificate.Extensions)
|
|
{
|
|
if (string.Equals(ext.Oid.Value, CertificateManager.AspNetHttpsOid, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetCertificatePath(out string path)
|
|
{
|
|
var hostingEnvironment = Options.ApplicationServices.GetRequiredService<IHostingEnvironment>();
|
|
var appName = hostingEnvironment.ApplicationName;
|
|
|
|
// This will go away when we implement
|
|
// https://github.com/aspnet/Hosting/issues/1294
|
|
var appData = Environment.GetEnvironmentVariable("APPDATA");
|
|
var home = Environment.GetEnvironmentVariable("HOME");
|
|
var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
|
|
basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
|
|
path = basePath != null ? Path.Combine(basePath, $"{appName}.pfx") : null;
|
|
return path != null;
|
|
}
|
|
|
|
private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
|
|
{
|
|
if (certInfo.IsFileCert && certInfo.IsStoreCert)
|
|
{
|
|
throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
|
|
}
|
|
else if (certInfo.IsFileCert)
|
|
{
|
|
var env = Options.ApplicationServices.GetRequiredService<IHostingEnvironment>();
|
|
return new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path), certInfo.Password);
|
|
}
|
|
else if (certInfo.IsStoreCert)
|
|
{
|
|
return LoadFromStoreCert(certInfo);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
|
|
{
|
|
var subject = certInfo.Subject;
|
|
var storeName = certInfo.Store;
|
|
var location = certInfo.Location;
|
|
var storeLocation = StoreLocation.CurrentUser;
|
|
if (!string.IsNullOrEmpty(location))
|
|
{
|
|
storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
|
|
}
|
|
var allowInvalid = certInfo.AllowInvalid ?? false;
|
|
|
|
return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
|
|
}
|
|
}
|
|
}
|