public async Task Invoke(HttpContext context) { var instanceLogger = AcmeCertificateManager.GetInstanceLogger(); if (instanceLogger == null) { throw new NullReferenceException("Instance Logger null on AcmeCertificationManger."); } try { context.Response.Headers.Add("Date", DateTime.UtcNow.ToJSONString()); var requestPath = context.Request.PathBase + context.Request.Path; _storage.Init(_settings, instanceLogger); instanceLogger.AddCustomEvent(Core.PlatformSupport.LogLevel.Verbose, "AcmeResponseMiddleware_Invoke", $"Received request", requestPath.Value.ToKVP("requestPath")); if (requestPath.StartsWithSegments(AcmeResponsePath, out PathString requestPathId)) { var challenge = requestPathId.Value.TrimStart('/'); instanceLogger.AddCustomEvent(Core.PlatformSupport.LogLevel.Verbose, "AcmeResponseMiddleware_Invoke", $"Received request", challenge.ToKVP("challenge"), requestPath.Value.ToKVP("requestPath")); var response = await _storage.GetResponseAsync(challenge); if (!string.IsNullOrEmpty(response)) { instanceLogger.AddCustomEvent(Core.PlatformSupport.LogLevel.Verbose, "AcmeResponseMiddleware_Invoke", "Found challenge and sent response", response.ToKVP("response"), challenge.ToKVP("challenge")); context.Response.ContentType = "text/plain"; context.Response.StatusCode = 200; await context.Response.WriteAsync(response); } else { instanceLogger.AddError("AcmeResponseMiddleware_Invoke", "Could not find challenge", challenge.ToKVP("challenge")); context.Response.StatusCode = 404; } } else { await _next.Invoke(context); } } catch (Exception ex) { instanceLogger.AddException("AcmeResponseMiddleware_Invoke", ex, context.Request.Path.ToString().ToKVP("path")); } }
public static async Task Main(string[] args) { var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; var isDevelopment = "Development".Equals(environment, StringComparison.OrdinalIgnoreCase); var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddInMemoryCollection(new Dictionary <string, string> // add default settings, that will be overridden by commandline { { "Id", "Webapp" }, { "Version", "1.0.0" }, { "ClusterId", "rrod-cluster" }, }) .AddCommandLine(args) .AddJsonFile("Webapp.settings.json", optional: true, reloadOnChange: true) .AddJsonFile($"Webapp.settings.{environment}.json", optional: true, reloadOnChange: true) .AddJsonFile("/run/config/Webapp.settings.json", optional: true, reloadOnChange: true) .AddDockerSecrets("/run/secrets", optional: true) .AddUserSecrets <Program>(optional: true) .AddEnvironmentVariables("RROD_") .Build(); var loggerFactory = new LoggerFactory() .AddConsole(config.GetSection("Logging")) .AddDebug(); var logger = loggerFactory.CreateLogger <Program>(); logger.LogWarning($"Starting Webapp in {environment} environment..."); foreach (var provider in config.Providers) { logger.LogInformation($"Config Provider {provider.GetType().Name}: {provider.GetChildKeys(Enumerable.Empty<string>(), null).Count()} settings"); } // ServicePointManager.CheckCertificateRevocationList = false; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; ServicePointManager.DefaultConnectionLimit = 20; int attempt = 0; int initializeAttemptsBeforeFailing = 7; IClusterClient clusterClient; while (true) { // Initialize orleans client clusterClient = new ClientBuilder() .ConfigureLogging(logging => { logging.AddConfiguration(config); }) .Configure <ClusterOptions>(options => { options.ClusterId = config["ClusterId"]; options.ServiceId = "rrod"; }) .AddAzureQueueStreams <AzureQueueDataAdapterV2>("Default", ob => { ob.Configure(options => { options.ConnectionString = config.GetConnectionString("DataConnectionString"); }); }) .ConfigureApplicationParts(parts => { parts.AddApplicationPart(typeof(ICounterGrain).Assembly).WithReferences(); parts.AddApplicationPart(typeof(AzureQueueDataAdapterV2).Assembly).WithReferences(); }) .UseAzureStorageClustering(options => options.ConnectionString = config.GetConnectionString("DataConnectionString")) .Build(); try { await clusterClient.Connect().ConfigureAwait(false); logger.LogInformation("Client successfully connected to silo host"); break; } catch (SiloUnavailableException) { attempt++; logger.LogWarning($"Attempt {attempt} of {initializeAttemptsBeforeFailing} failed to initialize the Orleans client."); if (attempt > initializeAttemptsBeforeFailing) { throw; } // Wait 4 seconds before retrying await Task.Delay(TimeSpan.FromSeconds(4)); } } var endpoints = config.GetSection("Http:Endpoints") .GetChildren() .ToDictionary(section => section.Key, section => { var endpoint = new EndpointConfiguration(); section.Bind(endpoint); return(endpoint); }); // if so, start a listener to respond to Acme (Let's Encrypt) requests, using a response received via an Orleans Cache Grain var hasHttps = endpoints.Any(endpoint => endpoint.Value.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase)); var needPort80 = endpoints.Any(endpoint => (endpoint.Value.Port ?? (endpoint.Value.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase) ? 443 : 80)) == 80); var certs = new Dictionary <string, X509Certificate2>(); if (hasHttps) { logger.LogWarning($"At least one https endpoint is present. Initialize Acme endpoint."); var acmeOptions = new AcmeOptions { GetChallengeResponse = async(challenge) => { var cacheGrain = clusterClient.GetGrain <ICacheGrain <string> >(challenge); var response = await cacheGrain.Get(); return(response.Value); }, SetChallengeResponse = async(challenge, response) => { var cacheGrain = clusterClient.GetGrain <ICacheGrain <string> >(challenge); await cacheGrain.Set(new Immutable <string>(response), TimeSpan.FromHours(2)); }, StoreCertificate = async(string domainName, byte[] certData) => { var certGrain = clusterClient.GetGrain <ICertGrain>(domainName); await certGrain.UpdateCertificate(certData); }, RetrieveCertificate = async(domainName) => { var certGrain = clusterClient.GetGrain <ICertGrain>(domainName); var certData = await certGrain.GetCertificate(); return(certData.Value); } }; var acmeHost = new WebHostBuilder() // .UseConfiguration(config) .ConfigureLogging((context, factory) => { factory.AddConfiguration(context.Configuration.GetSection("Logging")); factory.AddConsole(); factory.AddDebug(); }) .UseEnvironment(environment) .ConfigureServices(services => { services.AddSingleton <IClusterClient>(clusterClient); services.AddSingleton <ILoggerFactory>(loggerFactory); services.Configure <AcmeSettings>(config.GetSection(nameof(AcmeSettings))); // Register a certitificate manager, supplying methods to store and retreive certificates and acme challenge responses services.AddAcmeCertificateManager(acmeOptions); }) // .UseUrls("http://*:80") .PreferHostingUrls(false) .UseKestrel(options => { options.Listen(IPAddress.Any, 80); }) // .UseLoggerFactory(loggerFactory) .Configure(app => { app.UseAcmeResponse(); }) .Build(); try { await acmeHost.StartAsync(); } catch (Exception e) { logger.LogError("Error: can't start web listener for acme certificate renewal, probably the web address is in use by another process. Exception message is: " + e.Message); logger.LogError("Ignoring noncritical error (stop W3SVC or Skype to fix this), continuing..."); } var certificateManager = new AcmeCertificateManager(Options.Create(acmeOptions)); foreach (var endpoint in endpoints) { var endpointConfig = endpoint.Value; bool isHttpsEndpoint = endpointConfig.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase); var port = endpointConfig.Port ?? (isHttpsEndpoint ? 443 : 80); X509Certificate2 certificate = null; if (isHttpsEndpoint) { try { var domains = new List <string> { endpointConfig.Domain } .Concat(endpointConfig.Domains) .Where(ep => !string.IsNullOrEmpty(ep)) .Distinct() .ToArray(); logger.LogInformation($"Getting certificate for domain {domains.First()} on port {port}"); // Request a new certificate with Let's Encrypt and store it for next time try { certificate = await certificateManager.GetCertificate(domains); } catch (Exception e) { logger.LogCritical(e, $"Exception getting certificate for domain {domains.First()}. PfxPassword configured incorrectly?"); } if (certificate == null) { // It didn't work - create a temporary certificate so that we can still start with an untrusted certificate logger.LogCritical($"Error getting certificate for domain {domains.First()} (endpoint '{endpoint.Key}')"); // var certificateAuthorityCertificate = CertBuilder.CreateCertificateAuthorityCertificate("RrodCA", out var tmpCaPrivateKey); // certificate = CertBuilder.CreateSelfSignedCertificateBasedOnCertificateAuthorityPrivateKey(domains.First(), certificateAuthorityCertificate.Subject, tmpCaPrivateKey); certificate = CertHelper.BuildTlsSelfSignedServer(domains); } certs.Add(domains.First(), certificate); logger.LogInformation($"Certificate for domain {domains.First()}: {certificate != null}"); } catch (Exception e) { logger.LogCritical($"Kestrel startup: Exception getting certificate. {e.Message}"); } } } if (needPort80) { await acmeHost.StopAsync(); } } var webHost = new WebHostBuilder() // .UseConfiguration(config) .ConfigureServices(services => { services.AddSingleton <IConfiguration>(config); services.AddSingleton <IClusterClient>(clusterClient); services.AddSingleton <ILoggerFactory>(loggerFactory); }) .UseContentRoot(Directory.GetCurrentDirectory()) .UseEnvironment(environment) // .UseUrls(listenUrls.ToArray()) .PreferHostingUrls(false) .Configure(app => { app.UseAcmeResponse(); }) .UseStartup <Startup>() .UseKestrel(options => { foreach (var endpoint in endpoints) { var endpointConfig = endpoint.Value; bool isHttpsEndpoint = endpointConfig.Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase); var port = endpointConfig.Port ?? (isHttpsEndpoint ? 443 : 80); var ipAddresses = new List <IPAddress>(); var hosts = new List <string> { endpointConfig.Host } .Concat(endpointConfig.Hosts) .Where(ep => !string.IsNullOrEmpty(ep)) .Distinct(); foreach (var host in hosts) { if (host == "localhost") { ipAddresses.Add(IPAddress.IPv6Loopback); ipAddresses.Add(IPAddress.Loopback); } else if (IPAddress.TryParse(host, out var address)) { ipAddresses.Add(address); } else { logger.LogError($"Error parsing endpoint host: {host}"); } } foreach (var address in ipAddresses) { options.Listen(address, port, listenOptions => { if (isHttpsEndpoint) { var domains = new List <string> { endpointConfig.Domain } .Concat(endpointConfig.Domains) .Where(ep => !string.IsNullOrEmpty(ep)) .Distinct() .ToArray(); if (certs.TryGetValue(domains.First(), out var certificate)) { logger.LogInformation($"Kestrel config: Listen on address {address.ToString()}:{port}, certificate {(certificate == null ? "NULL" : certificate.Subject.ToString())}"); listenOptions.UseHttps(certificate); listenOptions.NoDelay = false; // listenOptions.UseConnectionLogging(); } else { logger.LogError($"No certificate for domain: {domains.First()}"); } } }); } } }) .Build(); webHost.Run(); }