using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using EmbedIO.Utilities; using Swan; using Swan.Logging; namespace EmbedIO { /// /// Contains options for configuring an instance of . /// public sealed class WebServerOptions : WebServerOptionsBase { private const string NetShLogSource = "NetSh"; private readonly List _urlPrefixes = new List(); private HttpListenerMode _mode = HttpListenerMode.EmbedIO; private X509Certificate2? _certificate; private string? _certificateThumbprint; private bool _autoLoadCertificate; private bool _autoRegisterCertificate; private StoreName _storeName = StoreName.My; private StoreLocation _storeLocation = StoreLocation.LocalMachine; /// /// Gets the URL prefixes. /// public IReadOnlyList UrlPrefixes => _urlPrefixes; /// /// Gets or sets the type of HTTP listener. /// /// This property is being set, /// and this instance's configuration is locked. /// public HttpListenerMode Mode { get => _mode; set { EnsureConfigurationNotLocked(); _mode = value; } } /// /// Gets or sets the X.509 certificate to use for SSL connections. /// /// This property is being set, /// and this instance's configuration is locked. public X509Certificate2? Certificate { get { if (AutoRegisterCertificate) return TryRegisterCertificate() ? _certificate : null; return _certificate ?? (AutoLoadCertificate ? LoadCertificate() : null); } set { EnsureConfigurationNotLocked(); _certificate = value; } } /// /// Gets or sets the thumbprint of the X.509 certificate to use for SSL connections. /// /// This property is being set, /// and this instance's configuration is locked. public string? CertificateThumbprint { get => _certificateThumbprint; set { EnsureConfigurationNotLocked(); // strip any non-hexadecimal values and make uppercase _certificateThumbprint = value == null ? null : Regex.Replace(value, @"[^\da-fA-F]", string.Empty).ToUpper(CultureInfo.InvariantCulture); } } /// /// Gets or sets a value indicating whether to automatically load the X.509 certificate. /// /// This property is being set, /// and this instance's configuration is locked. /// This property is being set to /// and the underlying operating system is not Windows. public bool AutoLoadCertificate { get => _autoLoadCertificate; set { EnsureConfigurationNotLocked(); if (value && SwanRuntime.OS != Swan.OperatingSystem.Windows) throw new PlatformNotSupportedException("AutoLoadCertificate functionality is only available under Windows."); _autoLoadCertificate = value; } } /// /// Gets or sets a value indicating whether to automatically bind the X.509 certificate /// to the port used for HTTPS. /// /// This property is being set, /// and this instance's configuration is locked. /// This property is being set to /// and the underlying operating system is not Windows. public bool AutoRegisterCertificate { get => _autoRegisterCertificate; set { EnsureConfigurationNotLocked(); if (value && SwanRuntime.OS != Swan.OperatingSystem.Windows) throw new PlatformNotSupportedException("AutoRegisterCertificate functionality is only available under Windows."); _autoRegisterCertificate = value; } } /// /// Gets or sets a value indicating the X.509 certificate store where to load the certificate from. /// /// This property is being set, /// and this instance's configuration is locked. /// public StoreName StoreName { get => _storeName; set { EnsureConfigurationNotLocked(); _storeName = value; } } /// /// Gets or sets a value indicating the location of the X.509 certificate store where to load the certificate from. /// /// This property is being set, /// and this instance's configuration is locked. /// public StoreLocation StoreLocation { get => _storeLocation; set { EnsureConfigurationNotLocked(); _storeLocation = value; } } /// /// Adds a URL prefix. /// /// The URL prefix. /// This instance's configuration is locked. /// is . /// /// is the empty string. /// - or - /// is already registered. /// public void AddUrlPrefix(string urlPrefix) { EnsureConfigurationNotLocked(); urlPrefix = Validate.NotNullOrEmpty(nameof(urlPrefix), urlPrefix); if (_urlPrefixes.Contains(urlPrefix)) throw new ArgumentException("URL prefix is already registered.", nameof(urlPrefix)); _urlPrefixes.Add(urlPrefix); } private X509Certificate2? LoadCertificate() { if (SwanRuntime.OS != Swan.OperatingSystem.Windows) return null; if (!string.IsNullOrWhiteSpace(_certificateThumbprint)) return GetCertificate(_certificateThumbprint); using var netsh = GetNetsh("show"); string? thumbprint = null; netsh.ErrorDataReceived += (s, e) => { if (string.IsNullOrWhiteSpace(e.Data)) return; e.Data.Error(NetShLogSource); }; netsh.OutputDataReceived += (s, e) => { if (string.IsNullOrWhiteSpace(e.Data)) return; e.Data.Debug(NetShLogSource); var line = e.Data.Trim(); if (line.StartsWith("Certificate Hash") && line.IndexOf(":", StringComparison.Ordinal) > -1) thumbprint = line.Split(':')[1].Trim(); }; if (!netsh.Start()) return null; netsh.BeginOutputReadLine(); netsh.BeginErrorReadLine(); netsh.WaitForExit(); return netsh.ExitCode == 0 && !string.IsNullOrEmpty(thumbprint) ? GetCertificate(thumbprint) : null; } private X509Certificate2? GetCertificate(string? thumbprint = null) { using var store = new X509Store(StoreName, StoreLocation); store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find( X509FindType.FindByThumbprint, thumbprint ?? _certificateThumbprint, false); return signingCert.Count == 0 ? null : signingCert[0]; } private bool AddCertificateToStore() { using var store = new X509Store(StoreName, StoreLocation); try { store.Open(OpenFlags.ReadWrite); store.Add(_certificate); return true; } catch { return false; } } private bool TryRegisterCertificate() { if (SwanRuntime.OS != Swan.OperatingSystem.Windows) return false; if (_certificate == null) throw new InvalidOperationException("A certificate is required to AutoRegister"); if (GetCertificate(_certificate.Thumbprint) == null && !AddCertificateToStore()) { throw new InvalidOperationException( "The provided certificate cannot be added to the default store, add it manually"); } using var netsh = GetNetsh("add", $"certhash={_certificate.Thumbprint} appid={{adaa04bb-8b63-4073-a12f-d6f8c0b4383f}}"); var sb = new StringBuilder(); void PushLine(object sender, DataReceivedEventArgs e) { if (string.IsNullOrWhiteSpace(e.Data)) return; sb.AppendLine(e.Data); e.Data.Error(NetShLogSource); } netsh.OutputDataReceived += PushLine; netsh.ErrorDataReceived += PushLine; if (!netsh.Start()) return false; netsh.BeginOutputReadLine(); netsh.BeginErrorReadLine(); netsh.WaitForExit(); return netsh.ExitCode == 0 ? true : throw new InvalidOperationException($"NetSh error: {sb}"); } private int GetSslPort() { var port = 443; foreach (var url in UrlPrefixes.Where(x => x.StartsWith("https:", StringComparison.OrdinalIgnoreCase))) { var match = Regex.Match(url, @":(\d+)"); if (match.Success && int.TryParse(match.Groups[1].Value, out port)) break; } return port; } private Process GetNetsh(string verb, string options = "") => new Process { StartInfo = new ProcessStartInfo { FileName = "netsh", CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, Arguments = $"http {verb} sslcert ipport=0.0.0.0:{GetSslPort()} {options}", }, }; } }