// 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> EndpointConfigurations { get; } = new Dictionary>(0, StringComparer.OrdinalIgnoreCase); // Actions that will be delayed until Load so that they aren't applied if the configuration loader is replaced. private IList EndpointsToAdd { get; } = new List(); /// /// Specifies a configuration Action to run when an endpoint with the given name is loaded from configuration. /// public KestrelConfigurationLoader Endpoint(string name, Action configureOptions) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } EndpointConfigurations[name] = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions)); return this; } /// /// Bind to given IP address and port. /// public KestrelConfigurationLoader Endpoint(IPAddress address, int port) => Endpoint(address, port, _ => { }); /// /// Bind to given IP address and port. /// public KestrelConfigurationLoader Endpoint(IPAddress address, int port, Action configure) { if (address == null) { throw new ArgumentNullException(nameof(address)); } return Endpoint(new IPEndPoint(address, port), configure); } /// /// Bind to given IP endpoint. /// public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint) => Endpoint(endPoint, _ => { }); /// /// Bind to given IP address and port. /// public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint, Action 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; } /// /// 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. /// public KestrelConfigurationLoader LocalhostEndpoint(int port) => LocalhostEndpoint(port, options => { }); /// /// 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. /// public KestrelConfigurationLoader LocalhostEndpoint(int port, Action configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } EndpointsToAdd.Add(() => { Options.ListenLocalhost(port, configure); }); return this; } /// /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. /// public KestrelConfigurationLoader AnyIPEndpoint(int port) => AnyIPEndpoint(port, options => { }); /// /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. /// public KestrelConfigurationLoader AnyIPEndpoint(int port, Action configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } EndpointsToAdd.Add(() => { Options.ListenAnyIP(port, configure); }); return this; } /// /// Bind to given Unix domain socket path. /// public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath) => UnixSocketEndpoint(socketPath, _ => { }); /// /// Bind to given Unix domain socket path. /// public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath, Action 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; } /// /// Open a socket file descriptor. /// public KestrelConfigurationLoader HandleEndpoint(ulong handle) => HandleEndpoint(handle, _ => { }); /// /// Open a socket file descriptor. /// public KestrelConfigurationLoader HandleEndpoint(ulong handle, Action 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>(); var certificate = FindDeveloperCertificateFile(configReader, logger); if (certificate != null) { logger.LocatedDevelopmentCertificate(certificate); Options.DefaultCertificate = certificate; } } } private X509Certificate2 FindDeveloperCertificateFile(ConfigurationReader configReader, ILogger 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(); 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(); 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); } } }