diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index 3f39de8..4ce6fdd 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/Dockerfile b/Dockerfile index f106dcd..e5e7b34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/dotnet:2.1-sdk-alpine AS build +FROM microsoft/dotnet:2.2-sdk-alpine AS build # Set the working directory witin the container WORKDIR /src @@ -17,8 +17,8 @@ RUN dotnet publish -c release ./src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.R # Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime-alpine AS app +FROM microsoft/dotnet:2.2-aspnetcore-runtime-alpine AS app WORKDIR /app -COPY --from=build /src/src/LetsEncrypt.Azure.Runner/bin/release/netcoreapp2.1/publish . +COPY --from=build /src/src/LetsEncrypt.Azure.Runner/bin/release/netcoreapp2.2/publish . ENTRYPOINT ["dotnet", "LetsEncrypt.Azure.Runner.dll"] \ No newline at end of file diff --git a/examples/LetsEncrypt.Azure.FunctionV2/Helper.cs b/examples/LetsEncrypt.Azure.FunctionV2/Helper.cs index 37efe73..f816e18 100644 --- a/examples/LetsEncrypt.Azure.FunctionV2/Helper.cs +++ b/examples/LetsEncrypt.Azure.FunctionV2/Helper.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.InteropServices; using System.Threading.Tasks; namespace LetsEncrypt.Azure.FunctionV2 @@ -14,11 +16,11 @@ namespace LetsEncrypt.Azure.FunctionV2 public class Helper { /// - /// Requests a Let's Encrypt wild card certificate using DNS challenge. + /// Requests a Let's Encrypt wild card certificate using DNS challenge. /// The DNS provider used is Azure DNS. /// The certificate is saved to Azure Key Vault. - /// The Certificate is finally install to an Azure App Service. - /// Configuration values are stored in Environment Variables. + /// The Certificate is finally install to an Azure App Service. + /// Configuration values are stored in Environment Variables. /// /// /// @@ -26,7 +28,7 @@ public static async Task InstallOrRenewCertificate(ILogger log) { var vaultBaseUrl = $"https://{Environment.GetEnvironmentVariable("Vault")}.vault.azure.net/"; log.LogInformation("C# HTTP trigger function processed a request."); - var Configuration = new ConfigurationBuilder() + var configuration = new ConfigurationBuilder() .AddAzureKeyVault(vaultBaseUrl) //Use MSI to get token .AddEnvironmentVariables() .Build(); @@ -34,33 +36,43 @@ public static async Task InstallOrRenewCertificate(ILogger log) //Create the Key Vault client var kvClient = new KeyVaultClient((authority, resource, scope) => tokenProvider.KeyVaultTokenCallback(authority, resource, scope), new MessageLoggingHandler(log)); + ValidationContext validationContext; IServiceCollection serviceCollection = new ServiceCollection(); - - serviceCollection.AddSingleton(log) - .Configure(options => options.MinLevel = LogLevel.Information); - var certificateConsumer = Configuration.GetValue("CertificateConsumer"); + serviceCollection + .AddSingleton(log) + .Configure(options => options.MinLevel = LogLevel.Information); + var certificateConsumer = configuration.GetValue("CertificateConsumer"); if (string.IsNullOrEmpty(certificateConsumer)) { - serviceCollection.AddAzureAppService(Configuration.GetSection("AzureAppService").Get()); + var webAppSettings = configuration.GetSection("AzureAppService").Get(); + validationContext = new ValidationContext(webAppSettings); + Validator.ValidateObject(webAppSettings, validationContext); + serviceCollection.AddAzureAppService(webAppSettings); } else if (certificateConsumer.Equals("NullCertificateConsumer")) { serviceCollection.AddNullCertificateConsumer(); } - serviceCollection.AddSingleton(kvClient) - .AddKeyVaultCertificateStore(vaultBaseUrl); - + serviceCollection + .AddSingleton(kvClient) + .AddKeyVaultCertificateStore(vaultBaseUrl); - serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get()); + var dnsProviderConfig = configuration.GetSection("DnsSettings").Get(); + validationContext = new ValidationContext(dnsProviderConfig); + Validator.ValidateObject(dnsProviderConfig, validationContext); + serviceCollection + .AddAcmeClient(dnsProviderConfig); var serviceProvider = serviceCollection.BuildServiceProvider(); var app = serviceProvider.GetService(); - var dnsRequest = Configuration.GetSection("AcmeDnsRequest").Get(); + var dnsRequest = configuration.GetSection("AcmeDnsRequest").Get(); + validationContext = new ValidationContext(dnsRequest); + Validator.ValidateObject(dnsRequest, validationContext); - await app.Run(dnsRequest, Configuration.GetValue("RenewXNumberOfDaysBeforeExpiration") ?? 22); + await app.Run(dnsRequest, configuration.GetValue("RenewXNumberOfDaysBeforeExpiration") ?? 22); } } } diff --git a/examples/LetsEncrypt.Azure.FunctionV2/LetsEncrypt.Azure.FunctionV2.csproj b/examples/LetsEncrypt.Azure.FunctionV2/LetsEncrypt.Azure.FunctionV2.csproj index 7377a86..687ee97 100644 --- a/examples/LetsEncrypt.Azure.FunctionV2/LetsEncrypt.Azure.FunctionV2.csproj +++ b/examples/LetsEncrypt.Azure.FunctionV2/LetsEncrypt.Azure.FunctionV2.csproj @@ -1,11 +1,12 @@  - netcoreapp2.1 + netcoreapp2.2 v2 + - + diff --git a/examples/LetsEncrypt.Azure.FunctionV2/RequestWildcardCertificate.cs b/examples/LetsEncrypt.Azure.FunctionV2/RequestWildcardCertificate.cs index ab1c2b3..d00ae14 100644 --- a/examples/LetsEncrypt.Azure.FunctionV2/RequestWildcardCertificate.cs +++ b/examples/LetsEncrypt.Azure.FunctionV2/RequestWildcardCertificate.cs @@ -30,11 +30,11 @@ public static async Task Run( await Helper.InstallOrRenewCertificate(log); return new OkResult(); - } catch(Exception ex) + } + catch (Exception ex) { log.LogError(ex.ToString()); return new ExceptionResult(ex, true); - } } } diff --git a/src/LetsEncrypt.Azure.Core.Test/AcmeClientTest.cs b/src/LetsEncrypt.Azure.Core.Test/AcmeClientTest.cs index 1792606..c70cb3a 100644 --- a/src/LetsEncrypt.Azure.Core.Test/AcmeClientTest.cs +++ b/src/LetsEncrypt.Azure.Core.Test/AcmeClientTest.cs @@ -20,25 +20,20 @@ public class AcmeClientTest { private readonly ILogger logger; - public AcmeClientTest() - { + public AcmeClientTest() { } - } + public AcmeClientTest(ILogger logger) => this.logger = logger; - public AcmeClientTest(ILogger logger) - { - this.logger = logger; - } [TestMethod] public async Task TestEndToEndAzure() { - var config = TestHelper.AzureDnsSettings; + var settings = TestHelper.AzureDnsSettings; - var manager = new AcmeClient(new AzureDnsProvider(config), new DnsLookupService(), null, this.logger); + var manager = new AcmeClient(new AzureDnsProvider(settings), new DnsLookupService(), new NullCertificateStore(), this.logger); - var dnsRequest = new AcmeDnsRequest() + IAcmeDnsRequest dnsRequest = new AcmeDnsRequest() { - Host = "*.ai4bots.com", + Hosts = "*.ai4bots.com", PFXPassword = "Pass@word", RegistrationEmail = "mail@sjkp.dk", AcmeEnvironment = new LetsEncryptStagingV2(), @@ -56,32 +51,32 @@ public async Task TestEndToEndAzure() Assert.IsNotNull(res); - File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate); - - var pass = new System.Security.SecureString(); - Array.ForEach(dnsRequest.PFXPassword.ToCharArray(), c => + string hostsPlusSeparated = AcmeClient.GetHostsPlusSeparated(dnsRequest.Hosts); + File.WriteAllBytes($"{hostsPlusSeparated}.pfx", res.CertificateInfo.PfxCertificate); + using (var pass = new System.Security.SecureString()) { - pass.AppendChar(c); - }); - File.WriteAllBytes($"exported-{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.Certificate.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pkcs12, pass)); + Array.ForEach(dnsRequest.PFXPassword.ToCharArray(), c => + { + pass.AppendChar(c); + }); + File.WriteAllBytes($"exported-{hostsPlusSeparated}.pfx", res.CertificateInfo.Certificate.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pkcs12, pass)); + var certService = new AzureWebAppService(new[] { TestHelper.AzureWebAppSettings }); - var certService = new AzureWebAppService(new[] { TestHelper.AzureWebAppSettings }); - - await certService.Install(res); + await certService.Install(res); + } } [TestMethod] public async Task TestEndToEndUnoEuro() { - var dnsProvider = TestHelper.UnoEuroDnsProvider; var manager = new AcmeClient(dnsProvider, new DnsLookupService(), new NullCertificateStore()); var dnsRequest = new AcmeDnsRequest() { - Host = "*.tiimo.dk", + Hosts = "*.tiimo.dk", PFXPassword = "Pass@word", RegistrationEmail = "mail@sjkp.dk", AcmeEnvironment = new LetsEncryptStagingV2(), @@ -99,7 +94,7 @@ public async Task TestEndToEndUnoEuro() Assert.IsNotNull(res); - File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate); + File.WriteAllBytes($"{dnsRequest.Hosts.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate); } @@ -113,7 +108,7 @@ public async Task TestEndToEndGoDaddy() var dnsRequest = new AcmeDnsRequest() { - Host = "*.åbningstider.info", + Hosts = "*.åbningstider.info", PFXPassword = "Pass@word", RegistrationEmail = "mail@sjkp.dk", AcmeEnvironment = new LetsEncryptStagingV2(), @@ -131,7 +126,7 @@ public async Task TestEndToEndGoDaddy() Assert.IsNotNull(res); - File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate); + File.WriteAllBytes($"{dnsRequest.Hosts.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate); var certService = new AzureWebAppService(new[] { TestHelper.AzureWebAppSettings }); diff --git a/src/LetsEncrypt.Azure.Core.Test/AzureDnsServiceTest.cs b/src/LetsEncrypt.Azure.Core.Test/AzureDnsServiceTest.cs index 71c000e..e3ec386 100644 --- a/src/LetsEncrypt.Azure.Core.Test/AzureDnsServiceTest.cs +++ b/src/LetsEncrypt.Azure.Core.Test/AzureDnsServiceTest.cs @@ -1,9 +1,11 @@ using LetsEncrypt.Azure.Core.V2; using LetsEncrypt.Azure.Core.V2.DnsProviders; +using LetsEncrypt.Azure.Core.V2.Models; using Microsoft.Extensions.Configuration; using Microsoft.Rest.Azure.Authentication; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Linq; using System.Threading.Tasks; namespace Letsencrypt.Azure.Core.Test @@ -11,21 +13,24 @@ namespace Letsencrypt.Azure.Core.Test [TestClass] public class AzureDnsServiceTest { + private const string Domain = "ai4bots.com"; + private const string HostName = "*." + Domain; + [TestMethod] public async Task AzureDnsTest() { var config = TestHelper.AzureDnsSettings; - + var service = new AzureDnsProvider(config); var id = Guid.NewGuid().ToString(); - await service.PersistChallenge("_acme-challenge", id); + await service.PersistChallenge(zoneName: Domain, recordSetName: "_acme-challenge", recordValue: id); - var exists = await new DnsLookupService().Exists("*.ai4bots.com", id, service.MinimumTtl); + var exists = await new DnsLookupService().Exists(HostName, id, service.MinimumTtl); Assert.IsTrue(exists); - await service.Cleanup("_acme-challenge"); - } + await service.Cleanup(Domain, "_acme-challenge"); + } } } diff --git a/src/LetsEncrypt.Azure.Core.Test/GoDaddyDnsProviderTest.cs b/src/LetsEncrypt.Azure.Core.Test/GoDaddyDnsProviderTest.cs index 09c163f..68b5549 100644 --- a/src/LetsEncrypt.Azure.Core.Test/GoDaddyDnsProviderTest.cs +++ b/src/LetsEncrypt.Azure.Core.Test/GoDaddyDnsProviderTest.cs @@ -20,7 +20,7 @@ public class GoDaddyDnsProviderTest public GoDaddyDnsProviderTest() { - this.Configuration = new ConfigurationBuilder() + this.Configuration = new ConfigurationBuilder() .AddUserSecrets() .Build(); @@ -37,13 +37,13 @@ public GoDaddyDnsProviderTest() public async Task TestPersistChallenge() { var id = Guid.NewGuid().ToString(); - await DnsService.PersistChallenge("_acme-challenge", id); + await DnsService.PersistChallenge(zoneName: Domain, recordSetName: "_acme-challenge", recordValue: id); var exists = await new DnsLookupService().Exists("*." + Domain, id); Assert.IsTrue(exists); - await DnsService.Cleanup("_acme-challenge"); + await DnsService.Cleanup(Domain, "_acme-challenge"); } } } diff --git a/src/LetsEncrypt.Azure.Core.Test/Letsencrypt.Azure.Core.Test.csproj b/src/LetsEncrypt.Azure.Core.Test/Letsencrypt.Azure.Core.Test.csproj index 6690da8..6b98097 100644 --- a/src/LetsEncrypt.Azure.Core.Test/Letsencrypt.Azure.Core.Test.csproj +++ b/src/LetsEncrypt.Azure.Core.Test/Letsencrypt.Azure.Core.Test.csproj @@ -1,22 +1,25 @@  - netcoreapp2.1 + netcoreapp2.2 + true + win-x64 false 5F9376DE-25E6-4FFF-8462-D95812FCE06C - - - - - - - - - + + + + + + + + + + @@ -27,5 +30,5 @@ Always - + diff --git a/src/LetsEncrypt.Azure.Core.Test/LoggingTest.cs b/src/LetsEncrypt.Azure.Core.Test/LoggingTest.cs index 557213d..5d301e0 100644 --- a/src/LetsEncrypt.Azure.Core.Test/LoggingTest.cs +++ b/src/LetsEncrypt.Azure.Core.Test/LoggingTest.cs @@ -12,13 +12,13 @@ public class LoggingTest public async Task TestLogging() { ILoggerFactory loggerFactory = new LoggerFactory() - .AddConsole() + .AddConsole() .AddDebug(); var logger = loggerFactory.CreateLogger(); logger.LogInformation("Initial message"); - + var client = new AcmeClientTest(logger); await client.TestEndToEndAzure(); - } + } } } diff --git a/src/LetsEncrypt.Azure.Core.Test/TestHelper.cs b/src/LetsEncrypt.Azure.Core.Test/TestHelper.cs index 679e0b2..a223e07 100644 --- a/src/LetsEncrypt.Azure.Core.Test/TestHelper.cs +++ b/src/LetsEncrypt.Azure.Core.Test/TestHelper.cs @@ -28,10 +28,11 @@ static TestHelper() clientId = config["clientId"]; secret = config["clientSecret"]; } + public static AzureDnsSettings AzureDnsSettings { get - { + { return new AzureDnsSettings("dns", "ai4bots.com", AzureServicePrincipal, new AzureSubscription() { AzureRegion = "AzureGlobalCloud", @@ -46,8 +47,8 @@ public static UnoEuroDnsProvider UnoEuroDnsProvider get { var config = new ConfigurationBuilder() - .AddUserSecrets() - .Build(); + .AddUserSecrets() + .Build(); return new UnoEuroDnsProvider(new UnoEuroDnsSettings() { @@ -67,11 +68,11 @@ public static UnoEuroDnsProvider UnoEuroDnsProvider public static AzureWebAppSettings AzureWebAppSettings { get - { + { return new AzureWebAppSettings("webappcfmv5fy7lcq7o", "LetsEncrypt-SiteExtension2", AzureServicePrincipal, new AzureSubscription() { Tenant = tenantId, - SubscriptionId = "3f09c367-93e0-4b61-bbe5-dcb5c686bf8a", + SubscriptionId = subscriptionId, AzureRegion = "AzureGlobalCloud" }); } diff --git a/src/LetsEncrypt.Azure.Core.Test/UnoEuroDnsProviderTest.cs b/src/LetsEncrypt.Azure.Core.Test/UnoEuroDnsProviderTest.cs index 7f54776..294d54c 100644 --- a/src/LetsEncrypt.Azure.Core.Test/UnoEuroDnsProviderTest.cs +++ b/src/LetsEncrypt.Azure.Core.Test/UnoEuroDnsProviderTest.cs @@ -18,19 +18,20 @@ public async Task CreateRecord() var config = new ConfigurationBuilder() .AddUserSecrets() .Build(); - + + string domain = config["domain"]; var dnsProvider = new UnoEuroDnsProvider(new UnoEuroDnsSettings() { AccountName = config["accountName"], ApiKey = config["apiKey"], - Domain = config["domain"] + Domain = domain }); //Test create new - await dnsProvider.PersistChallenge("_acme-challenge", Guid.NewGuid().ToString()); + await dnsProvider.PersistChallenge(zoneName: domain, recordSetName: "_acme-challenge", recordValue: Guid.NewGuid().ToString()); //Test Update existing - await dnsProvider.PersistChallenge("_acme-challenge", Guid.NewGuid().ToString()); + await dnsProvider.PersistChallenge(zoneName: domain, recordSetName: "_acme-challenge", recordValue: Guid.NewGuid().ToString()); //Test clean up - await dnsProvider.Cleanup("_acme-challenge"); + await dnsProvider.Cleanup(domain, "_acme-challenge"); } @@ -39,14 +40,16 @@ public async Task UnoEuroDnsTest() { var service = TestHelper.UnoEuroDnsProvider; - var id = Guid.NewGuid().ToString(); - await service.PersistChallenge("_acme-challenge", id); + const string Domain = "tiimo.dk"; + const string HostName = "*." + Domain; + var id = Guid.NewGuid().ToString(); + await service.PersistChallenge(zoneName: Domain, recordSetName: "_acme-challenge", recordValue: id); - var exists = await new DnsLookupService().Exists("*.tiimo.dk", id, service.MinimumTtl); + var exists = await new DnsLookupService().Exists(HostName, id, service.MinimumTtl); Assert.IsTrue(exists); - await service.Cleanup("_acme-challenge"); + await service.Cleanup(Domain, "_acme-challenge"); } diff --git a/src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs b/src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs index a237df6..e10e414 100644 --- a/src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs +++ b/src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs @@ -7,16 +7,20 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Net.Http; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; namespace LetsEncrypt.Azure.Core.V2 { public class AcmeClient { + private readonly HttpClient http = new HttpClient(); private readonly IDnsProvider dnsProvider; private readonly DnsLookupService dnsLookupService; private readonly ICertificateStore certificateStore; @@ -28,12 +32,12 @@ public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, I this.dnsProvider = dnsProvider; this.dnsLookupService = dnsLookupService; this.certificateStore = certifcateStore; - this.logger = logger ?? NullLogger.Instance; - + this.logger = logger ?? NullLogger.Instance; } + /// - /// Request a certificate from lets encrypt using the DNS challenge, placing the challenge record in Azure DNS. - /// The certifiacte is not assigned, but just returned. + /// Request a certificate from lets encrypt using the DNS challenge, placing the challenge record in Azure DNS. + /// The certifiacte is not assigned, but just returned. /// /// /// @@ -44,33 +48,49 @@ public async Task RequestDnsChallengeCertificate(IAcmeD var acmeContext = await GetOrCreateAcmeContext(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail); var idn = new IdnMapping(); - var order = await acmeContext.NewOrder(new[] { "*." + idn.GetAscii(acmeConfig.Host.Substring(2)) }); - var a = await order.Authorizations(); - var authz = a.First(); - var challenge = await authz.Dns(); - var dnsTxt = acmeContext.AccountKey.DnsTxt(challenge.Token); - logger.LogInformation("Got DNS challenge token {Token}", dnsTxt); + var orderHosts = (from host in acmeConfig.Hosts + let asciiHost = idn.GetAscii(host) + let asciiDomain = asciiHost.StartsWith("*.") + ? asciiHost.Substring(2) + : asciiHost + select (Host: asciiHost, Domain: asciiDomain)) + .ToImmutableArray(); + var order = await acmeContext.NewOrder(orderHosts.Select(h => h.Host).ToImmutableArray()); + var authorizations = (await order.Authorizations()).ToImmutableArray(); + var tasks = new List(authorizations.Length); + var dnsTxts = new List(authorizations.Length); + // TODO: Consider parallelizing + for (var i = 0; i < authorizations.Length; i++) /*tasks.Add(Task.Factory.StartNew(async state =>*/ + { + //var (authorization, host) = (Tuple)state; + var (authorization, zoneName) = (authorizations[i], orderHosts[i].Domain); + var challenge = await authorization.Dns(); + var dnsTxt = acmeContext.AccountKey.DnsTxt(challenge.Token); + logger.LogInformation("Got DNS challenge token {Token}", dnsTxt); - ///add dns entry - await this.dnsProvider.PersistChallenge("_acme-challenge", dnsTxt); + ///add dns entry + await this.dnsProvider.PersistChallenge(zoneName, "_acme-challenge", dnsTxt); + await Task.Delay(500); - if (!(await this.dnsLookupService.Exists(acmeConfig.Host, dnsTxt, this.dnsProvider.MinimumTtl))) - { - throw new TimeoutException($"Unable to validate that _acme-challenge was stored in txt _acme-challenge record after {this.dnsProvider.MinimumTtl} seconds"); - } + if (!(await this.dnsLookupService.Exists(zoneName, dnsTxt, this.dnsProvider.MinimumTtl))) + { + throw new TimeoutException($"Unable to validate that _acme-challenge was stored in txt _acme-challenge record after {this.dnsProvider.MinimumTtl} seconds"); + } - - Challenge chalResp = await challenge.Validate(); - while (chalResp.Status == ChallengeStatus.Pending || chalResp.Status == ChallengeStatus.Processing) - { - logger.LogInformation("Dns challenge response status {ChallengeStatus} more info at {ChallengeStatusUrl} retrying in 5 sec", chalResp.Status, chalResp.Url.ToString()); - await Task.Delay(5000); - chalResp = await challenge.Resource(); - } + Challenge chalResp = await challenge.Validate(); + while (chalResp.Status == ChallengeStatus.Pending || chalResp.Status == ChallengeStatus.Processing) + { + logger.LogInformation("Dns challenge response status {ChallengeStatus} more info at {ChallengeStatusUrl} retrying in 5 sec", chalResp.Status, chalResp.Url.ToString()); + await Task.Delay(5000); + chalResp = await challenge.Resource(); + } - logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url); + logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url); + }/*, Tuple.Create(authorizations[i], orderHosts[i].BaseHost), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).Unwrap()); + await Task.WhenAll(tasks); + tasks.Clear()*/; - var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Host); + var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Hosts); var cert = await order.Generate(new Certes.CsrInfo { CountryName = acmeConfig.CsrInfo.CountryName, @@ -83,27 +103,39 @@ public async Task RequestDnsChallengeCertificate(IAcmeD var certPem = cert.ToPem(); + string hostsPlusSeparated = GetHostsPlusSeparated(acmeConfig.Hosts); var pfxBuilder = cert.ToPfx(privateKey); - var pfx = pfxBuilder.Build(acmeConfig.Host, acmeConfig.PFXPassword); + var pfx = pfxBuilder.Build(hostsPlusSeparated, acmeConfig.PFXPassword); - await this.dnsProvider.Cleanup(dnsTxt); + for (var i = 0; i < dnsTxts.Count; i++) + { + tasks.Add(this.dnsProvider.Cleanup(orderHosts[i].Domain, dnsTxts[i])); + } + await Task.WhenAll(tasks); + tasks.Clear(); return new CertificateInstallModel() { CertificateInfo = new CertificateInfo() { +#pragma warning disable DF0100 // Marks return values that hides the IDisposable implementation of return value. Certificate = new X509Certificate2(pfx, acmeConfig.PFXPassword, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable), - Name = $"{acmeConfig.Host} {DateTime.Now}", +#pragma warning restore DF0100 // Marks return values that hides the IDisposable implementation of return value. + Name = $"{acmeConfig.Hosts} {DateTime.Now}", Password = acmeConfig.PFXPassword, PfxCertificate = pfx }, - Host = acmeConfig.Host + Hosts = acmeConfig.Hosts }; } - private async Task GetOrCreateKey(Uri acmeDirectory, string host) + internal static string GetHostsPlusSeparated(ImmutableArray hosts) + => string.Join("+", hosts.Select(h => h.StartsWith("*") ? h.Substring(1) : h)); + + private async Task GetOrCreateKey(Uri acmeDirectory, ImmutableArray hosts) { - string secretName = $"privatekey{host}--{acmeDirectory.Host}"; + string hostsPlusSeparated = GetHostsPlusSeparated(hosts); + string secretName = $"privatekey-{hostsPlusSeparated}--{acmeDirectory.Host}"; var key = await this.certificateStore.GetSecret(secretName); if (string.IsNullOrEmpty(key)) { @@ -129,19 +161,15 @@ private async Task GetOrCreateAcmeContext(Uri acmeDirectoryUri, str var pemKey = acme.AccountKey.ToPem(); await certificateStore.SaveSecret(filename, pemKey); await Task.Delay(10000); //Wait a little before using the new account. - acme = new AcmeContext(acmeDirectoryUri, acme.AccountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient())); + acme = new AcmeContext(acmeDirectoryUri, acme.AccountKey, new AcmeHttpClient(acmeDirectoryUri, http)); } else { var accountKey = KeyFactory.FromPem(secret); - acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient())); + acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, http)); } return acme; } - - - - } } diff --git a/src/LetsEncrypt.Azure.Core.V2/AssemblyInfo.cs b/src/LetsEncrypt.Azure.Core.V2/AssemblyInfo.cs new file mode 100644 index 0000000..5f323da --- /dev/null +++ b/src/LetsEncrypt.Azure.Core.V2/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Letsencrypt.Azure.Core.Test")] \ No newline at end of file diff --git a/src/LetsEncrypt.Azure.Core.V2/AzureBlobStorage.cs b/src/LetsEncrypt.Azure.Core.V2/AzureBlobStorage.cs index 5b24f6a..4d62271 100644 --- a/src/LetsEncrypt.Azure.Core.V2/AzureBlobStorage.cs +++ b/src/LetsEncrypt.Azure.Core.V2/AzureBlobStorage.cs @@ -1,5 +1,5 @@ -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; +using Microsoft.Azure.Storage; +using Microsoft.Azure.Storage.Blob; using System.IO; using System.Threading.Tasks; @@ -26,9 +26,7 @@ private async Task GetBlob(string v) var container = client.GetContainerReference("letsencrypt"); await container.CreateIfNotExistsAsync(); - var blob = container.GetBlockBlobReference(v); - return blob; } @@ -52,7 +50,7 @@ public async Task Read(string v) { await data.CopyToAsync(ms); return ms.ToArray(); - } + } } public async Task Write(string v, byte[] data) diff --git a/src/LetsEncrypt.Azure.Core.V2/AzureHelper.cs b/src/LetsEncrypt.Azure.Core.V2/AzureHelper.cs index ff892b9..af79d71 100644 --- a/src/LetsEncrypt.Azure.Core.V2/AzureHelper.cs +++ b/src/LetsEncrypt.Azure.Core.V2/AzureHelper.cs @@ -22,18 +22,16 @@ public static AzureCredentials GetAzureCredentials(AzureServicePrincipal service } if (servicePrincipal.UseManagendIdentity) - { + { return new AzureCredentials(new MSILoginInformation(MSIResourceType.AppService), Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion)); } - return new AzureCredentials(servicePrincipal.ServicePrincipalLoginInformation, azureSubscription.Tenant, Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion)); } - public static RestClient GetRestClient(AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription) - { + { var credentials = GetAzureCredentials(servicePrincipal, azureSubscription); return RestClient .Configure() diff --git a/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/AzureWebAppService.cs b/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/AzureWebAppService.cs index 44c6915..f2b418b 100644 --- a/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/AzureWebAppService.cs +++ b/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/AzureWebAppService.cs @@ -21,9 +21,11 @@ public AzureWebAppService(AzureWebAppSettings[] settings, ILogger.Instance; } + public async Task Install(ICertificateInstallModel model) { - logger.LogInformation("Starting installation of certificate {Thumbprint} for {Host}", model.CertificateInfo.Certificate.Thumbprint, model.Host); + string hostsComaSeparated = string.Join(",", model.Hosts); + logger.LogInformation("Starting installation of certificate {Thumbprint} for {Host}", model.CertificateInfo.Certificate.Thumbprint, hostsComaSeparated); var cert = model.CertificateInfo; foreach (var setting in this.settings) { @@ -42,13 +44,21 @@ public async Task Install(ICertificateInstallModel model) var existingCerts = await appServiceManager.AppServiceCertificates.ListByResourceGroupAsync(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName); if (existingCerts.All(_ => _.Thumbprint != cert.Certificate.Thumbprint)) { - await appServiceManager.AppServiceCertificates.Define(model.Host + "-" + cert.Certificate.Thumbprint).WithRegion(s.RegionName).WithExistingResourceGroup(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName).WithPfxByteArray(model.CertificateInfo.PfxCertificate).WithPfxPassword(model.CertificateInfo.Password).CreateAsync(); + await appServiceManager + .AppServiceCertificates + .Define($"{hostsComaSeparated}-{cert.Certificate.Thumbprint}") + .WithRegion(s.RegionName) + .WithExistingResourceGroup(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName) + .WithPfxByteArray(model.CertificateInfo.PfxCertificate) + .WithPfxPassword(model.CertificateInfo.Password) + .CreateAsync(); } - - var sslStates = siteOrSlot.HostNameSslStates; - var domainSslMappings = new List>(sslStates.Where(_ => _.Key.Contains($".{model.Host.Substring(2)}"))); + var domainSslMappings = new List>( + sslStates.Where(_ => + model.Hosts.Any(h => _.Key == h + || _.Key.Contains($".{h}")))); if (domainSslMappings.Any()) { @@ -75,6 +85,7 @@ public async Task Install(ICertificateInstallModel model) } } } + // TODO: Catch errors one by one catch (Exception e) { logger.LogCritical(e, "Unable to install certificate for '{WebApp}'", setting.WebAppName); @@ -114,8 +125,6 @@ private async Task RemoveCertificate(IAppServiceManager webSiteClient, IAppServi { await webSiteClient.AppServiceCertificates.DeleteByResourceGroupAsync(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName, s.Name); } - - } } diff --git a/src/LetsEncrypt.Azure.Core.V2/DnsLookupService.cs b/src/LetsEncrypt.Azure.Core.V2/DnsLookupService.cs index 55504c5..9843652 100644 --- a/src/LetsEncrypt.Azure.Core.V2/DnsLookupService.cs +++ b/src/LetsEncrypt.Azure.Core.V2/DnsLookupService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; @@ -19,56 +20,45 @@ public DnsLookupService(ILogger logger = null) this.logger = logger ?? NullLogger.Instance; } - public async Task Exists(string hostname, string dnsTxt, int timeout = 60) + public async Task Exists(string zoneName, string dnsTxt, int timeout = 60) { - logger.LogInformation("Starting dns precheck validation for hostname: {HostName} challenge: {Challenge} and timeout {Timeout}", hostname, dnsTxt, timeout); + logger.LogInformation("Starting dns precheck validation for hostname: {HostName} challenge: {Challenge} and timeout {Timeout}", zoneName, dnsTxt, timeout); var idn = new IdnMapping(); - hostname = idn.GetAscii(GetNoneWildcardDomain(hostname)); - var dnsClient = GetDnsClient(hostname); + zoneName = idn.GetAscii(zoneName); + var dnsClient = GetDnsClient(zoneName); var startTime = DateTime.UtcNow; - string queriedDns = ""; - //Lets encrypt checks a random authoritative server, thus we need to ensure that all respond with the challenge. - foreach (var ns in dnsClient.NameServers) + bool result = false; + do { - logger.LogInformation("Validating dns challenge exists on name server {NameServer}", ns.ToString()); - do + var servers = dnsClient.NameServers.Select(s => s.Endpoint.Address).ToImmutableArray(); + logger.LogInformation("Validating dns challenge exists on name servers {NameServers}", string.Join(", ", servers)); + var dnsRes = dnsClient.QueryServer(servers, $"_acme-challenge.{zoneName}", QueryType.TXT); + result = dnsRes.Answers.TxtRecords().FirstOrDefault()?.Text.Any(r => r == dnsTxt) ?? false; + if (!result) { - var dnsRes = dnsClient.QueryServer(new[] { ns.Endpoint.Address }, $"_acme-challenge.{hostname}", QueryType.TXT); - queriedDns = dnsRes.Answers.TxtRecords().FirstOrDefault()?.Text.FirstOrDefault(); - if (queriedDns != dnsTxt) - { - logger.LogInformation("Challenge record was {existingTxt} should have been {Challenge}, retrying again in 5 seconds", queriedDns, dnsTxt); - await Task.Delay(5000); - } + logger.LogInformation("Challenge record missing, retrying again in 5 seconds"); + await Task.Delay(5000); + } - } while (queriedDns != dnsTxt && (DateTime.UtcNow - startTime).TotalSeconds < timeout); - } + } while (!result && (DateTime.UtcNow - startTime).TotalSeconds < timeout); - return queriedDns == dnsTxt; + return result; } - private static LookupClient GetDnsClient(params string[] hostnames) + private static LookupClient GetDnsClient(string zoneName) { - LookupClient generalClient = new LookupClient(); - LookupClient dnsClient = null; generalClient.UseCache = false; - foreach (var hostname in hostnames) - { - var ns = generalClient.Query(hostname, QueryType.NS); - var ip = ns.Answers.NsRecords().Select(s => generalClient.GetHostEntry(s.NSDName.Value)); - - dnsClient = new LookupClient(ip.SelectMany(i => i.AddressList).Where(s => s.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).ToArray()); - dnsClient.UseCache = false; - - } + var ns = generalClient.Query(zoneName, QueryType.NS); + var ip = ns.Answers.NsRecords().Select(s => generalClient.GetHostEntry(s.NSDName.Value)); - return dnsClient; - } + var nameServers = ip.SelectMany(i => i.AddressList) + .Where(s => s.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + .ToArray(); + LookupClient dnsClient = new LookupClient(nameServers); + dnsClient.UseCache = false; - public static string GetNoneWildcardDomain(string hostname) - { - return hostname.Replace("*.", ""); + return dnsClient; } } } diff --git a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/AzureDnsProvider.cs b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/AzureDnsProvider.cs index 5c9fa73..a81db5c 100644 --- a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/AzureDnsProvider.cs +++ b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/AzureDnsProvider.cs @@ -18,31 +18,32 @@ public class AzureDnsProvider : IDnsProvider public AzureDnsProvider(AzureDnsSettings settings) { var restClient = AzureHelper.GetRestClient(settings.AzureServicePrincipal, settings.AzureSubscription); - - + +#pragma warning disable DF0020 // Marks undisposed objects assinged to a field, originated in an object creation. this.client = new DnsManagementClient(restClient); +#pragma warning restore DF0020 // Marks undisposed objects assinged to a field, originated in an object creation. this.client.SubscriptionId = settings.AzureSubscription.SubscriptionId; this.settings = settings; } public int MinimumTtl => 60; - public async Task Cleanup(string recordSetName) + public async Task Cleanup(string recordSetName, string zoneName) { - var existingRecords = await SafeGetExistingRecords(recordSetName); + var existingRecords = await SafeGetExistingRecords(recordSetName, zoneName); - await this.client.RecordSets.DeleteAsync(this.settings.ResourceGroupName, this.settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT); + await this.client.RecordSets.DeleteAsync(this.settings.ResourceGroupName, zoneName, GetRelativeRecordSetName(recordSetName, zoneName), RecordType.TXT); } - public async Task PersistChallenge(string recordSetName, string recordValue) + public async Task PersistChallenge(string zoneName, string recordSetName, string recordValue) { List records = new List() { new TxtRecord() { Value = new[] { recordValue } } }; - if ((await client.RecordSets.ListByTypeAsync(settings.ResourceGroupName, settings.ZoneName, RecordType.TXT)).Any()) + if ((await client.RecordSets.ListByTypeAsync(settings.ResourceGroupName, zoneName, RecordType.TXT)).Any()) { - var existingRecords = await SafeGetExistingRecords(recordSetName); + var existingRecords = await SafeGetExistingRecords(recordSetName, zoneName); if (existingRecords != null) { if (existingRecords.TxtRecords.Any(s => s.Value.Contains(recordValue))) @@ -55,23 +56,21 @@ public async Task PersistChallenge(string recordSetName, string recordValue) } } } - await this.client.RecordSets.CreateOrUpdateAsync(this.settings.ResourceGroupName, this.settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT, new RecordSetInner() + await this.client.RecordSets.CreateOrUpdateAsync(this.settings.ResourceGroupName, zoneName, GetRelativeRecordSetName(recordSetName, zoneName), RecordType.TXT, new RecordSetInner() { TxtRecords = records, TTL = MinimumTtl }); } - private string GetRelativeRecordSetName(string dnsTxt) - { - return dnsTxt.Replace($".{this.settings.ZoneName}", ""); - } + private string GetRelativeRecordSetName(string dnsTxt, string zoneName) + => dnsTxt.Replace($".{zoneName}", ""); - private async Task SafeGetExistingRecords(string recordSetName) + private async Task SafeGetExistingRecords(string recordSetName, string zoneName) { try { - return await client.RecordSets.GetAsync(settings.ResourceGroupName, settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT); + return await client.RecordSets.GetAsync(settings.ResourceGroupName, zoneName, GetRelativeRecordSetName(recordSetName, zoneName), RecordType.TXT); } catch (CloudException cex) diff --git a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/GoDaddyDnsProvider.cs b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/GoDaddyDnsProvider.cs index 49a2d58..98457f3 100644 --- a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/GoDaddyDnsProvider.cs +++ b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/GoDaddyDnsProvider.cs @@ -21,12 +21,12 @@ public GoDaddyDnsProvider(GoDaddyDnsSettings settings) public int MinimumTtl => 600; - public Task Cleanup(string recordSetName) + public Task Cleanup(string zoneName, string recordSetName) { return Task.FromResult(0); } - public async Task PersistChallenge(string recordSetName, string recordValue) + public async Task PersistChallenge(string zoneName, string recordSetName, string recordValue) { var body = await httpClient.GetStringAsync($"records/TXT/{recordSetName}"); var acmeChallengeRecord = JsonConvert.DeserializeObject(body); @@ -40,10 +40,12 @@ public async Task PersistChallenge(string recordSetName, string recordValue) type = "TXT" }}; - var res = await this.httpClient.PutAsync($"records/TXT/{recordSetName}", new StringContent(JsonConvert.SerializeObject(acmeChallengeRecord), Encoding.UTF8, "application/json")); - body = await res.Content.ReadAsStringAsync(); - res.EnsureSuccessStatusCode(); - + using (var stringContent = new StringContent(JsonConvert.SerializeObject(acmeChallengeRecord), Encoding.UTF8, "application/json")) + using (var res = await this.httpClient.PutAsync($"records/TXT/{recordSetName}", stringContent)) + { + body = await res.Content.ReadAsStringAsync(); + res.EnsureSuccessStatusCode(); + } } public class GoDaddyDnsSettings @@ -54,7 +56,6 @@ public class GoDaddyDnsSettings public string Domain { get; set; } } - public class DnsRecord { public string data { get; set; } @@ -62,6 +63,5 @@ public class DnsRecord public int ttl { get; set; } public string type { get; set; } } - } } diff --git a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/IDnsProvider.cs b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/IDnsProvider.cs index 3827fb7..36e8e9c 100644 --- a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/IDnsProvider.cs +++ b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/IDnsProvider.cs @@ -4,8 +4,8 @@ namespace LetsEncrypt.Azure.Core.V2.DnsProviders { public interface IDnsProvider { - Task PersistChallenge(string recordSetName, string recordValue); - Task Cleanup(string recordSetName); + Task PersistChallenge(string zoneName, string recordSetName, string recordValue); + Task Cleanup(string recordSetName, string zoneName); /// /// The minimum ttl value in seconds, that the provider supports. diff --git a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/UnoEuroDnsProvider.cs b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/UnoEuroDnsProvider.cs index ad6d7d0..e139ce5 100644 --- a/src/LetsEncrypt.Azure.Core.V2/DnsProviders/UnoEuroDnsProvider.cs +++ b/src/LetsEncrypt.Azure.Core.V2/DnsProviders/UnoEuroDnsProvider.cs @@ -15,21 +15,25 @@ public class UnoEuroDnsProvider : IDnsProvider public UnoEuroDnsProvider(UnoEuroDnsSettings settings) { +#pragma warning disable DF0020 // Marks undisposed objects assinged to a field, originated in an object creation. this.httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri($"https://api.unoeuro.com/1/{settings.AccountName}/{settings.ApiKey}/my/products/{settings.Domain}/dns/records/"); +#pragma warning restore DF0020 // Marks undisposed objects assinged to a field, originated in an object creation. + httpClient.BaseAddress = new Uri($"https://api.unoeuro.com/1/{settings.AccountName}/{settings.ApiKey}/my/products/{settings.Domain}/dns/records/"); } - public async Task Cleanup(string recordSetName) + public async Task Cleanup(string zoneName, string recordSetName) { DnsRecord acmeChallengeRecord = await GetRecord(recordSetName); if (acmeChallengeRecord != null) - { - var res = await this.httpClient.DeleteAsync($"{acmeChallengeRecord.record_id}"); - res.EnsureSuccessStatusCode(); - } + using (var res = await this.httpClient.DeleteAsync($"{acmeChallengeRecord.record_id}")) + { +#pragma warning disable DF0001 // Marks undisposed anonymous objects from method invocations. + res.EnsureSuccessStatusCode(); +#pragma warning restore DF0001 // Marks undisposed anonymous objects from method invocations. + } } - public async Task PersistChallenge(string recordSetName, string recordValue) + public async Task PersistChallenge(string zoneName, string recordSetName, string recordValue) { DnsRecord acmeChallengeRecord = await GetRecord(recordSetName); @@ -44,10 +48,14 @@ public async Task PersistChallenge(string recordSetName, string recordValue) data = recordValue, acmeChallengeRecord.priority }; - StringContent content = CreateRequestBody(update); - var res = await httpClient.PutAsync($"{acmeChallengeRecord.record_id}", content); - var s = res.Content.ReadAsStringAsync(); - res.EnsureSuccessStatusCode(); + using (StringContent content = CreateRequestBody(update)) + using (var res = await httpClient.PutAsync($"{acmeChallengeRecord.record_id}", content)) + { + var s = res.Content.ReadAsStringAsync(); +#pragma warning disable DF0001 // Marks undisposed anonymous objects from method invocations. + res.EnsureSuccessStatusCode(); +#pragma warning restore DF0001 // Marks undisposed anonymous objects from method invocations. + } } else { @@ -59,9 +67,13 @@ public async Task PersistChallenge(string recordSetName, string recordValue) data = recordValue, priority = 0 }; - //Create - var res = await httpClient.PostAsync("", CreateRequestBody(acmeChallengeRecord)); - res.EnsureSuccessStatusCode(); + using (StringContent content = CreateRequestBody(acmeChallengeRecord)) + using (var res = await httpClient.PostAsync("", content)) + { +#pragma warning disable DF0001 // Marks undisposed anonymous objects from method invocations. + res.EnsureSuccessStatusCode(); +#pragma warning restore DF0001 // Marks undisposed anonymous objects from method invocations. + } } } diff --git a/src/LetsEncrypt.Azure.Core.V2/LetsEncrypt.Azure.Core.V2.csproj b/src/LetsEncrypt.Azure.Core.V2/LetsEncrypt.Azure.Core.V2.csproj index 0fd82ed..052fe78 100644 --- a/src/LetsEncrypt.Azure.Core.V2/LetsEncrypt.Azure.Core.V2.csproj +++ b/src/LetsEncrypt.Azure.Core.V2/LetsEncrypt.Azure.Core.V2.csproj @@ -2,27 +2,23 @@ netstandard2.0 - Library for easy retrieval of Let's Encrypt wildcard certificates using version 2 api. Support easy install to Azure Web Apps and storage in Azure Key Vault or Blob Storage. + Library for easy retrieval of Let's Encrypt wildcard certificates using version 2 api. Support easy install to Azure Web Apps and storage in Azure Key Vault or Blob Storage. false - + - - - - - - - - - - - - - C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.ComponentModel.DataAnnotations.dll - + + + + + + + + + + diff --git a/src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs b/src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs index 461cf74..2d77cb4 100644 --- a/src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs +++ b/src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; +using System.Linq; using System.Threading.Tasks; namespace LetsEncrypt.Azure.Core.V2 @@ -13,7 +14,6 @@ public class LetsencryptService private readonly AcmeClient acmeClient; private readonly ICertificateStore certificateStore; private readonly ICertificateConsumer certificateConsumer; - private readonly AzureWebAppService azureWebAppService; private readonly ILogger logger; public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, ICertificateConsumer certificateConsumer, ILogger logger = null) @@ -23,13 +23,15 @@ public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateSt this.certificateConsumer = certificateConsumer; this.logger = logger ?? NullLogger.Instance; } - public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration) + + public async Task Run(IAcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration) { try { CertificateInstallModel model = null; - - var certname = acmeDnsRequest.Host.Substring(2) + "-" + acmeDnsRequest.AcmeEnvironment.Name; + + string hostsPlusSeparated = AcmeClient.GetHostsPlusSeparated(acmeDnsRequest.Hosts); + var certname = $"{hostsPlusSeparated}-{acmeDnsRequest.AcmeEnvironment.Name}"; var cert = await certificateStore.GetCertificate(certname, acmeDnsRequest.PFXPassword); if (cert == null || cert.Certificate.NotAfter < DateTime.UtcNow.AddDays(renewXNumberOfDaysBeforeExpiration)) //Cert doesnt exist or expires in less than renewXNumberOfDaysBeforeExpiration days, lets renew. { @@ -44,7 +46,7 @@ public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBefor model = new CertificateInstallModel() { CertificateInfo = cert, - Host = acmeDnsRequest.Host + Hosts = acmeDnsRequest.Hosts }; } await certificateConsumer.Install(model); @@ -52,7 +54,7 @@ public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBefor logger.LogInformation("Removing expired certificates"); var expired = await certificateConsumer.CleanUp(); logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray())); - + } catch (Exception e) { diff --git a/src/LetsEncrypt.Azure.Core.V2/LetsencryptServiceCollectionExtensions.cs b/src/LetsEncrypt.Azure.Core.V2/LetsencryptServiceCollectionExtensions.cs index 5b8408b..cdddeaf 100644 --- a/src/LetsEncrypt.Azure.Core.V2/LetsencryptServiceCollectionExtensions.cs +++ b/src/LetsEncrypt.Azure.Core.V2/LetsencryptServiceCollectionExtensions.cs @@ -60,21 +60,19 @@ public static IServiceCollection AddAcmeClient(this IServiceCollec return serviceCollection .AddTransient() - .AddTransient() + .AddTransient() .AddSingleton(dnsProviderConfig.GetType(), dnsProviderConfig) - .AddTransient(); + .AddTransient(); } public static IServiceCollection AddNullCertificateConsumer(this IServiceCollection serviceCollection) { - + return serviceCollection .AddTransient() .AddTransient(); } - - public static IServiceCollection AddAzureAppService(this IServiceCollection serviceCollection, params AzureWebAppSettings[] settings) { if (settings == null || settings.Length == 0) diff --git a/src/LetsEncrypt.Azure.Core.V2/MessageHandler.cs b/src/LetsEncrypt.Azure.Core.V2/MessageHandler.cs index d709e63..435f102 100644 --- a/src/LetsEncrypt.Azure.Core.V2/MessageHandler.cs +++ b/src/LetsEncrypt.Azure.Core.V2/MessageHandler.cs @@ -35,19 +35,17 @@ protected override async Task SendAsync(HttpRequestMessage { responseMessage = await response.Content.ReadAsByteArrayAsync(); } - + await OutgoingMessageAsync(corrId, requestInfo, responseMessage); return response; } - protected abstract Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message); protected abstract Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message); } - public class MessageLoggingHandler : MessageHandler { private readonly ILogger logger; @@ -56,13 +54,13 @@ public MessageLoggingHandler(ILogger logger) { this.logger = logger; } + protected override async Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message) { await Task.Run(() => logger.LogInformation(string.Format("{0} - Request: {1}\r\n{2}", correlationId, requestInfo, message != null ? Encoding.UTF8.GetString(message) : String.Empty))); } - protected override async Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message) { await Task.Run(() => diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/AcmeDnsRequest.cs b/src/LetsEncrypt.Azure.Core.V2/Models/AcmeDnsRequest.cs index 60958c3..ea44911 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/AcmeDnsRequest.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/AcmeDnsRequest.cs @@ -1,47 +1,101 @@ using Certes.Acme; using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Text; namespace LetsEncrypt.Azure.Core.V2.Models { + public class DomainComparer : IComparer + { + public int Compare(string x, string y) + { + var xParts = x.Split('.').Reverse().ToImmutableArray(); + var yParts = y.Split('.').Reverse().ToImmutableArray(); + var result = xParts.Zip(yParts, (xPart, yPart) => string.Compare(xPart, yPart)).FirstOrDefault(r => r != 0); + if (result == 0) + result = xParts.Length - yParts.Length; + return result; + } + } + public class AcmeDnsRequest : IAcmeDnsRequest { + private readonly static IComparer domainComparer = new DomainComparer(); + /// - /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates. + /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates. /// + [Required] public string RegistrationEmail { get; set; } /// - /// The ACME environment, use or or provide you own ACME compatible endpoint by implementing . + /// The ACME environment, use or or provide you + /// own ACME compatible endpoint by implementing . /// + [Required] public AcmeEnvironment AcmeEnvironment { get; set; } + private string hosts; + private ImmutableArray hostsList = ImmutableArray.Empty; + private ImmutableArray domainsList = ImmutableArray.Empty; + /// - /// The host name to request a certificate for e.g. *.example.com + /// The host names to request a certificate for delimited by coma e.g. *.example1.com,*.example2.com /// - public string Host { get; set; } + [Required] + public string Hosts + { + get { return hosts; } + set + { + string RemoveWildcard(string host) => host.StartsWith("*.") ? host.Substring(2) : host; + + this.hosts = value; + string[] hosts = this.hosts.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + hostsList = hosts.OrderBy(h => h, domainComparer).Distinct().ToImmutableArray(); + domainsList = hostsList + .Select(RemoveWildcard) + .Distinct() + .ToImmutableArray(); + } + } + + ImmutableArray IAcmeDnsRequest.Hosts => hostsList; + ImmutableArray IAcmeDnsRequest.Domains => domainsList; + + [Required] public string PFXPassword { get; set; } + [Required] public CsrInfo CsrInfo { get; set; } } public interface IAcmeDnsRequest { /// - /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates. + /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates. /// string RegistrationEmail { get; } + /// - /// The ACME environment, use or or provide you own ACME compatible endpoint by implementing . + /// The ACME environment, use or or provide you own ACME + /// compatible endpoint by implementing . /// AcmeEnvironment AcmeEnvironment { get; } /// - /// The host name to request a certificate for e.g. *.example.com + /// A list of host names to request a certificate for without a wildcard symbol e.g. example.com + /// + ImmutableArray Hosts { get; } + + /// + /// A list of domain zones for which certificate will be requested /// - string Host { get; } + ImmutableArray Domains { get; } string PFXPassword { get; } @@ -51,10 +105,15 @@ public interface IAcmeDnsRequest public class CsrInfo { public string CountryName { get; set; } + public string State { get; set; } + public string Locality { get; set; } + public string Organization { get; set; } - public string OrganizationUnit { get; set; } + + public string OrganizationUnit { get; set; } + public string CommonName { get; set; } } @@ -62,22 +121,15 @@ public class AcmeEnvironment { public Uri BaseUri { get; set; } - public AcmeEnvironment() - { + public AcmeEnvironment() { } - } + public AcmeEnvironment(Uri uri) { this.BaseUri = uri; } - public AcmeEnvironment(Uri uri) - { - this.BaseUri = uri; - } protected string name; + public string Name { - get - { - return name; - } + get => name; set { if ("production".Equals(value, StringComparison.InvariantCultureIgnoreCase)) @@ -97,16 +149,12 @@ public string Name public class LetsEncryptStagingV2 : AcmeEnvironment { public LetsEncryptStagingV2() : base(WellKnownServers.LetsEncryptStagingV2) - { - this.name = "staging"; - } + => this.name = "staging"; } public class LetsEncryptV2 : AcmeEnvironment { public LetsEncryptV2() : base(WellKnownServers.LetsEncryptV2) - { - this.name = "production"; - } + => this.name = "production"; } } diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/AzureDnsSettings.cs b/src/LetsEncrypt.Azure.Core.V2/Models/AzureDnsSettings.cs index 629e341..ff8b5e0 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/AzureDnsSettings.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/AzureDnsSettings.cs @@ -1,33 +1,35 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Text; namespace LetsEncrypt.Azure.Core.V2.Models { public class AzureDnsSettings - { + { public AzureDnsSettings() { this.RelativeRecordSetName = "@"; } - public AzureDnsSettings(string resourceGroupName, string zoneName, AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription, string relativeRecordName = "@") + public AzureDnsSettings(string resourceGroupName, AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription, string relativeRecordName = "@") { this.AzureSubscription = azureSubscription; this.AzureServicePrincipal = servicePrincipal; this.ResourceGroupName = resourceGroupName; - this.ZoneName = zoneName; this.RelativeRecordSetName = resourceGroupName; } - public AzureServicePrincipal AzureServicePrincipal {get;set;} - public AzureSubscription AzureSubscription { get; set; } - - public string ResourceGroupName { get; set; } + [Required] + public AzureServicePrincipal AzureServicePrincipal { get; set; } - public string RelativeRecordSetName { get; set; } + [Required] + public AzureSubscription AzureSubscription { get; set; } - public string ZoneName { get; set; } + [Required] + public string ResourceGroupName { get; set; } + public string RelativeRecordSetName { get; set; } } } diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/AzureServicePrincipal.cs b/src/LetsEncrypt.Azure.Core.V2/Models/AzureServicePrincipal.cs index f38f7fb..3382e58 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/AzureServicePrincipal.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/AzureServicePrincipal.cs @@ -5,7 +5,7 @@ namespace LetsEncrypt.Azure.Core.V2.Models { - public class AzureServicePrincipal + public class AzureServicePrincipal { public bool UseManagendIdentity { get; set; } public string ClientId { get; set; } diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/AzureSubscription.cs b/src/LetsEncrypt.Azure.Core.V2/Models/AzureSubscription.cs index 76ef381..9b14f12 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/AzureSubscription.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/AzureSubscription.cs @@ -1,13 +1,18 @@ -namespace LetsEncrypt.Azure.Core.V2.Models +using System.ComponentModel.DataAnnotations; + +namespace LetsEncrypt.Azure.Core.V2.Models { public class AzureSubscription { + [Required] public string Tenant { get; set; } + [Required] public string SubscriptionId { get; set; } /// /// Should be AzureGlobalCloud, AzureChinaCloud, AzureUSGovernment or AzureGermanCloud /// - public string AzureRegion { get; set; } + [Required] + public string AzureRegion { get; set; } } } \ No newline at end of file diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/AzureWebAppSettings.cs b/src/LetsEncrypt.Azure.Core.V2/Models/AzureWebAppSettings.cs index e9276df..1037b1e 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/AzureWebAppSettings.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/AzureWebAppSettings.cs @@ -1,11 +1,11 @@ -namespace LetsEncrypt.Azure.Core.V2.Models +using System.ComponentModel.DataAnnotations; + +namespace LetsEncrypt.Azure.Core.V2.Models { public class AzureWebAppSettings { - public AzureWebAppSettings() - { + public AzureWebAppSettings() { } - } public AzureWebAppSettings(string webappName, string resourceGroup, AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription, string siteSlotName = null, string servicePlanResourceGroupName = null, bool useIPBasedSSL = false) { this.WebAppName = webappName; @@ -16,7 +16,11 @@ public AzureWebAppSettings(string webappName, string resourceGroup, AzureService this.ServicePlanResourceGroupName = servicePlanResourceGroupName; this.UseIPBasedSSL = useIPBasedSSL; } + + [Required] public string WebAppName { get; set; } + + [Required] public string ResourceGroupName { get; set; } public string ServicePlanResourceGroupName { get; set; } @@ -25,8 +29,10 @@ public AzureWebAppSettings(string webappName, string resourceGroup, AzureService public bool UseIPBasedSSL { get; set; } + [Required] public AzureServicePrincipal AzureServicePrincipal { get; set; } + [Required] public AzureSubscription AzureSubscription { get; set; } } } diff --git a/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInstallModel.cs b/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInstallModel.cs index 7b68b05..ffdeeb8 100644 --- a/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInstallModel.cs +++ b/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInstallModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Text; namespace LetsEncrypt.Azure.Core.V2.Models @@ -12,24 +13,18 @@ public class CertificateInstallModel : ICertificateInstallModel /// /// Certificate info. /// - public CertificateInfo CertificateInfo - { - get; set; - } + public CertificateInfo CertificateInfo { get; set; } /// - /// The primary host name. + /// The primary host name. /// - public string Host - { - get; set; - } + public ImmutableArray Hosts { get; set; } } public interface ICertificateInstallModel { CertificateInfo CertificateInfo { get; set; } - string Host { get; set; } - } + ImmutableArray Hosts { get; set; } + } } diff --git a/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.json b/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.json index 7a0c588..9ff8d87 100644 --- a/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.json +++ b/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.json @@ -36,12 +36,6 @@ "description": "The name of the web application that should have the SSL Cert assigned" } }, - "dnsZoneName": { - "type": "string", - "metadata": { - "description": "The DNS zone name in azure, e.g. yourwebsite.com" - } - }, "dnsResourceGroupName": { "type": "string", "metadata": { @@ -97,10 +91,10 @@ }, "defaultValue": "staging" }, - "certificateDomain": { + "certificateDomains": { "type": "string", "metadata": { - "description": "The domain name to request a certificate for e.g. *.yourdomain.com" + "description": "The coma separated domain names to request a certificate for e.g. *.yourdomain.com" } }, "pfxPass": { @@ -261,7 +255,6 @@ "AzureAppService__AzureSubscription__Tenant": "[subscription().tenantId]", "AzureAppService__AzureSubscription__SubscriptionId": "[subscription().subscriptionId]", "AzureAppService__AzureSubscription__AzureRegion": "AzureGlobalCloud", - "DnsSettings__ZoneName": "[parameters('dnsZoneName')]", "DnsSettings__ResourceGroupName": "[parameters('dnsResourceGroupName')]", "DnsSettings__AzureServicePrincipal__UseManagendIdentity": "true", "DnsSettings__AzureSubscription__Tenant": "[subscription().tenantId]", @@ -270,7 +263,7 @@ "AcmeDnsRequest__CsrInfo__Organization": "[parameters('csrOrganisation')]", "AcmeDnsRequest__RegistrationEmail": "[parameters('acmeRegistrationEmail')]", "AcmeDnsRequest__AcmeEnvironment__Name": "[parameters('acmeEnvironment')]", - "AcmeDnsRequest__Host": "[parameters('certificateDomain')]", + "AcmeDnsRequest__Hosts": "[parameters('certificateDomains')]", "AcmeDnsRequest__PFXPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('vaultName'), variables('pfxPass'))).secretUriWithVersion, ')')]", "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(concat('microsoft.insights/components/', variables('appInsightsName'))).InstrumentationKey]", "WEBSITE_RUN_FROM_PACKAGE": "[parameters('runFromPackage')]", diff --git a/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.parameters.json b/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.parameters.json index 272758d..d54b839 100644 --- a/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.parameters.json +++ b/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.parameters.json @@ -7,11 +7,10 @@ "hostingPlanName": { "value": "letsencryptrunner" }, "targetWebAppResourceGroupName": { "value": "LetsEncrypt.Wildcard.Function" }, "targetWebAppName": { "value": "sjkpletsencrypt" }, - "dnsZoneName": { "value": "ai4bots.com" }, "dnsResourceGroupName": { "value": "dns" }, "csrOrganisation": { "value": "sjkp" }, "acmeRegistrationEmail": { "value": "mail@sjkp.dk" }, - "certificateDomain": { "value": "*.ai4bots.com" }, + "certificateDomains": { "value": "*.ai4bots.com" }, "runFromPackage": { "value": "https://letsencryptazure.blob.core.windows.net/releases/126.zip" } } } diff --git a/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.json b/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.json deleted file mode 100644 index fc24e36..0000000 --- a/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Name of the Vault" - } - }, - "tenantId": { - "type": "string", - "metadata": { - "description": "Tenant Id of the subscription. Get using Get-AzureRmSubscription cmdlet or Get Subscription API" - } - }, - "objectId": { - "type": "string", - "metadata": { - "description": "Object Id of the AD user. Get using Get-AzureRmADUser or Get-AzureRmADServicePrincipal cmdlets" - } - }, - "keysPermissions": { - "type": "array", - "defaultValue": ["all"], - "metadata": { - "description": "Permissions to keys in the vault. Valid values are: all, create, import, update, get, list, delete, backup, restore, encrypt, decrypt, wrapkey, unwrapkey, sign, and verify." - } - }, - "secretsPermissions": { - "type": "array", - "defaultValue": ["all"], - "metadata": { - "description": "Permissions to secrets in the vault. Valid values are: all, get, set, list, and delete." - } - }, - "skuName": { - "type": "string", - "defaultValue": "Standard", - "allowedValues": [ - "Standard", - "Premium" - ], - "metadata": { - "description": "SKU for the vault" - } - }, - "enableVaultForDeployment": { - "type": "bool", - "defaultValue": false, - "allowedValues": [ - true, - false - ], - "metadata": { - "description": "Specifies if the vault is enabled for a VM deployment" - } - }, - "enableVaultForDiskEncryption": { - "type": "bool", - "defaultValue": false, - "allowedValues": [ - true, - false - ], - "metadata": { - "description": "Specifies if the azure platform has access to the vault for enabling disk encryption scenarios." - } - }, - "enabledForTemplateDeployment": { - "type": "bool", - "defaultValue": false, - "allowedValues": [ - true, - false - ], - "metadata": { - "description": "Specifies whether Azure Resource Manager is permitted to retrieve secrets from the key vault." - } - }, - "appName": { - "type": "string", - "metadata": { - "description": "The name of the function app that you wish to create." - } - }, - "storageAccountType": { - "type": "string", - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_ZRS", - "Premium_LRS" - ], - "metadata": { - "description": "Storage Account type" - } - } - }, - "variables": { - "functionAppName": "[parameters('appName')]", - "hostingPlanName": "[parameters('appName')]", - "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]", - "armResourceProvideServicePrincipalId": "abfa0a7c-a6b6-4736-8310-5855508787cd" - }, - "resources": [ - { - "type": "Microsoft.KeyVault/vaults", - "name": "[parameters('keyVaultName')]", - "apiVersion": "2015-06-01", - "location": "[resourceGroup().location]", - "properties": { - "enabledForDeployment": "[parameters('enableVaultForDeployment')]", - "enabledForDiskEncryption": "[parameters('enableVaultForDiskEncryption')]", - "enabledForTemplateDeployment": "[parameters('enabledForTemplateDeployment')]", - "tenantId": "[parameters('tenantId')]", - "accessPolicies": [ - { - "tenantId": "[parameters('tenantId')]", - "objectId": "[parameters('objectId')]", - "permissions": { - "keys": "[parameters('keysPermissions')]", - "secrets": "[parameters('secretsPermissions')]" - } - }, - { - "tenantId": "[parameters('tenantId')]", - "objectId": "[variables('armResourceProvideServicePrincipalId')]", - "permissions": { - "keys": "[parameters('keysPermissions')]", - "secrets": "[parameters('secretsPermissions')]" - } - } - ], - "sku": { - "name": "[parameters('skuName')]", - "family": "A" - } - } - }, - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2015-06-15", - "location": "[resourceGroup().location]", - "properties": { - "accountType": "[parameters('storageAccountType')]" - } - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2015-04-01", - "name": "[variables('hostingPlanName')]", - "location": "[resourceGroup().location]", - "properties": { - "name": "[variables('hostingPlanName')]", - "computeMode": "Dynamic", - "sku": "Dynamic" - } - }, - { - "apiVersion": "2015-08-01", - "type": "Microsoft.Web/sites", - "name": "[variables('functionAppName')]", - "location": "[resourceGroup().location]", - "kind": "functionapp", - "properties": { - "name": "[variables('functionAppName')]", - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" - ], - "resources": [ - { - "apiVersion": "2016-03-01", - "name": "appsettings", - "type": "config", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" - ], - "properties": { - "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", - "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]", - "FUNCTIONS_EXTENSION_VERSION": "latest" - } - } - ] - } - ] -} \ No newline at end of file diff --git a/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.parameters.json b/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.parameters.json deleted file mode 100644 index 9fb9226..0000000 --- a/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.parameters.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "keyVaultName": { - "value": "letsencrypt-vault" - }, - "tenantId": { - "value": "" - }, - "objectId": { - "value": "" - }, - "keysPermissions": { - "value": [ - "all" - ] - }, - "secretsPermissions": { - "value": [ - "all" - ] - }, - "skuName": { - "value": "Standard" - }, - "enableVaultForDeployment": { - "value": false - }, - "enableVaultForDiskEncryption": { - "value": false - }, - "enabledForTemplateDeployment": { - "value": false - }, - "appName": { - "value": "letsencryptfunctionapp" - } - } -} \ No newline at end of file diff --git a/src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj b/src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj index 1e03c06..27f050f 100644 --- a/src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj +++ b/src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj @@ -2,15 +2,15 @@ Exe - netcoreapp2.1 + netcoreapp2.2 7.2 - - - - + + + + diff --git a/src/LetsEncrypt.Azure.Runner/Program.cs b/src/LetsEncrypt.Azure.Runner/Program.cs index f4289df..596a302 100644 --- a/src/LetsEncrypt.Azure.Runner/Program.cs +++ b/src/LetsEncrypt.Azure.Runner/Program.cs @@ -22,7 +22,7 @@ async static Task Main(string[] args) .Build(); var azureAppSettings = new AzureWebAppSettings[] { }; - + if (Configuration.GetSection("AzureAppService").Exists()) { azureAppSettings = new[] { Configuration.GetSection("AzureAppService").Get() }; @@ -43,16 +43,18 @@ async static Task Main(string[] args) c.AddConsole(); //c.AddDebug(); }) - .Configure(options => options.MinLevel = LogLevel.Information) + .Configure(options => options.MinLevel = LogLevel.Information) .AddAzureAppService(azureAppSettings); if (Configuration.GetSection("DnsSettings").Get().ShopperId != null) { serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get()); - } else if (Configuration.GetSection("DnsSettings").Get().AccountName != null) + } + else if (Configuration.GetSection("DnsSettings").Get().AccountName != null) { serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get()); - } else if (Configuration.GetSection("DnsSettings").Get().ResourceGroupName != null) + } + else if (Configuration.GetSection("DnsSettings").Get().ResourceGroupName != null) { serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get()); }