Skip to content

Commit

Permalink
#92
Browse files Browse the repository at this point in the history
  • Loading branch information
sjkp committed Aug 4, 2017
1 parent 894f4ac commit b3114ac
Show file tree
Hide file tree
Showing 21 changed files with 374 additions and 31 deletions.
47 changes: 47 additions & 0 deletions LetsEncrypt-SiteExtension/Controllers/Api/CertificateController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using LetsEncrypt.Azure.Core;
using LetsEncrypt.Azure.Core.Models;
using LetsEncrypt.SiteExtension.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;

namespace LetsEncrypt.SiteExtension.Controllers.Api
{
[ValidateApiVersion]
public class CertificateController : ApiController
{
[HttpPost]
[Route("api/certificates/renew")]
[ResponseType(typeof(List<CertificateInstallModel>))]
public async Task<IHttpActionResult> RenewExisting([FromUri(Name = "api-version")]string apiversion = null)
{
Trace.TraceInformation("Renew certificate");
var config = new AppSettingsAuthConfig();
var res = await new CertificateManager(new AppSettingsAuthConfig()).RenewCertificate(renewXNumberOfDaysBeforeExpiration: config.RenewXNumberOfDaysBeforeExpiration);
Trace.TraceInformation($"Completed renewal of '{res.Count()}' certificates");

return Ok(res);
}

[HttpPost]
[Route("api/certificates/provider/kudu/install")]
[ResponseType(typeof(CertificateInstallModel))]
public async Task<IHttpActionResult> GenerateAndInstall(GenerateAndInstallModel model, [FromUri(Name = "api-version")]string apiversion = null)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var mgr = new CertificateManager(model.AzureEnvironment, model.AcmeConfig, model.CertificateSettings, model.AuthorizationChallengeProviderConfig);

return Ok(await mgr.AddCertificate());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;

namespace LetsEncrypt.SiteExtension.Controllers.Api
{
public class ValidateApiVersionAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
object apiversion = string.Empty;
if (actionContext.ActionArguments.TryGetValue("apiversion", out apiversion))
{
var errorMessage = string.Empty;
if (!TryValidateApiVersion(apiversion as string, out errorMessage))
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessage);
}
}
}

private bool TryValidateApiVersion(string apiversion, out string error)
{
error = string.Empty;
if (apiversion == "2017-09-01")
{
return true;
}
error = "Missing query string api-version or unsupported api-version. Only supported api-version is 2017-09-01";
return false;

}
}
}
3 changes: 3 additions & 0 deletions LetsEncrypt-SiteExtension/LetsEncrypt.SiteExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@
<ItemGroup>
<Compile Include="App_Start\RouteConfig.cs" />
<Compile Include="App_Start\WebApiConfig.cs" />
<Compile Include="Controllers\Api\CertificateController.cs" />
<Compile Include="Controllers\Api\ValidateApiVersionAttribute.cs" />
<Compile Include="Models\GenerateAndInstallModel.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\HyakUtils.cs" />
<Compile Include="Controllers\Utils.cs" />
Expand Down
14 changes: 14 additions & 0 deletions LetsEncrypt-SiteExtension/Models/GenerateAndInstallModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using LetsEncrypt.Azure.Core.Models;

namespace LetsEncrypt.SiteExtension.Models
{
public class GenerateAndInstallModel
{
public AzureEnvironment AzureEnvironment { get; set; }
public AcmeConfig AcmeConfig { get; set; }

public CertificateServiceSettings CertificateSettings { get; set; }

public AuthorizationChallengeProviderConfig AuthorizationChallengeProviderConfig { get; set; }
}
}
20 changes: 10 additions & 10 deletions LetsEncrypt-SiteExtension/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,30 @@
</thead>
<tbody>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.tenantKey</td><td>The tenant name e.g. myazuretenant.onmicrosoft.com</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.tenantKey</td><td>The tenant name e.g. myazuretenant.onmicrosoft.com</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.subscriptionIdKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.subscriptionIdKey</td>
<td>(Optional) The subscription id, if left empty the enviroment variable WEBSITE_OWNER_NAME will be used</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.clientIdKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.clientIdKey</td>
<td>The value of the clientid of the service principal</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.clientSecretKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.clientSecretKey</td>
<td>The secret for the service principal</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.resourceGroupNameKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.resourceGroupNameKey</td>
<td>(Optional) The name of the resource group this web app belongs to, if left empty the enviroment variable WEBSITE_OWNER_NAME will be used</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.servicePlanResourceGroupNameKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.servicePlanResourceGroupNameKey</td>
<td>(Optional) The name of the resource group that the app service plan hosting the web app (only required if the app service plan is in a different resource group than the web app)</td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.useIPBasedSSL</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.useIPBasedSSL</td>
<td>Check this if you want the certificate to be bound to the WebApps' IP address instead of using SNI. With IP based SSL additional costs might be charged.</td>
</tr>
</tbody>
Expand All @@ -63,16 +63,16 @@
</thead>
<tbody>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.acmeBaseUriKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.acmeBaseUriKey</td>
<td>The url to Let's Encrypt servers e.g. https://acme-v01.api.letsencrypt.org/ or https://acme-staging.api.letsencrypt.org/ </td>
</tr>
<tr>
<td>@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.emailKey</td>
<td>@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.emailKey</td>
<td>The Email used for registering with Let's Encrypt</td>
</tr>
<tr>
<td>
@LetsEncrypt.SiteExtension.Models.AppSettingsAuthConfig.hostNamesKey
@LetsEncrypt.Azure.Core.Models.AppSettingsAuthConfig.hostNamesKey
</td>
<td>Comma separated list of custom hostnames (externally hosted setup with CNames), that should automatically be configured for the site.</td>
</tr>
Expand Down
9 changes: 9 additions & 0 deletions LetsEncrypt.SiteExtension.Core/AppSettingsAuthConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ public string PFXPassword
}
}

public bool UseProduction
{
get
{
return false; //Default behavior is not to use production environment.
}
}


#region overrideable settings to enable support for azure azure regions

[DataType(DataType.Url)]
Expand Down
26 changes: 13 additions & 13 deletions LetsEncrypt.SiteExtension.Core/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public CertificateManager(IAzureEnvironment settings, IAcmeConfig acmeConfig, IC
/// <summary>
/// Used for automatic installation of letsencrypt certificate
/// </summary>
public bool AddCertificate()
public async Task<CertificateInstallModel> AddCertificate()
{
Trace.TraceInformation("Staring add certificate");
using (var client = ArmHelper.GetWebSiteManagementClient(settings))
Expand All @@ -65,18 +65,18 @@ public bool AddCertificate()

if (acmeConfig.Hostnames.Any())
{
return RequestAndInstallInternal(this.acmeConfig) != null;
return await RequestAndInstallInternalAsync(this.acmeConfig);
}
else
{
Trace.TraceWarning("No hostnames found in configuration cannot add certificate automatically. Please run the manual configuration, or provide the all required app settings for automated deployment and delete firstrun.job in letsencrypt in the blob storage account to enable the job to be rerun.");
}

}
return false;
return null;
}

public async Task<List<AcmeConfig>> RenewCertificate(bool skipInstallCertificate = false, int renewXNumberOfDaysBeforeExpiration = 0)
public async Task<List<CertificateInstallModel>> RenewCertificate(bool skipInstallCertificate = false, int renewXNumberOfDaysBeforeExpiration = 0)
{
Trace.TraceInformation("Checking certificate");
var ss = SettingsStore.Instance.Load();
Expand All @@ -96,7 +96,7 @@ public async Task<List<AcmeConfig>> RenewCertificate(bool skipInstallCertificate
{
Trace.TraceInformation(string.Format("No certificates installed issued by Let's Encrypt that are about to expire within the next {0} days. Skipping.", renewXNumberOfDaysBeforeExpiration));
}
var res = new List<AcmeConfig>();
var res = new List<CertificateInstallModel>();
foreach (var toExpireCert in expiringCerts)
{
Trace.TraceInformation("Starting renew of certificate " + toExpireCert.Name + " expiration date " + toExpireCert.ExpirationDate);
Expand All @@ -120,35 +120,35 @@ public async Task<List<AcmeConfig>> RenewCertificate(bool skipInstallCertificate
};
if (!skipInstallCertificate)
{
await RequestAndInstallInternalAsync(target);
}
res.Add(target);
res.Add(await RequestAndInstallInternalAsync(target));
}
}
return res;
}
}


internal string RequestAndInstallInternal(IAcmeConfig config)
internal CertificateInstallModel RequestAndInstallInternal(IAcmeConfig config)
{
return RequestAndInstallInternalAsync(config).GetAwaiter().GetResult();
}

internal async Task<string> RequestAndInstallInternalAsync(IAcmeConfig config)
internal async Task<CertificateInstallModel> RequestAndInstallInternalAsync(IAcmeConfig config)
{
try
{
Trace.TraceInformation("RequestAndInstallInternal");
var service = new AcmeService(config, this.challengeProvider);

var cert = await service.RequestCertificate();
this.certificateService.Install(new CertificateInstallModel()
var model = new CertificateInstallModel()
{
CertificateInfo = cert,
AllDnsIdentifiers = config.Hostnames.ToList(),
Host = config.Host,
});
return cert.Certificate.Thumbprint;
};
this.certificateService.Install(model);
return model;
}
catch (Exception ex)
{
Expand Down
10 changes: 9 additions & 1 deletion LetsEncrypt.SiteExtension.Core/IAzureEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;

namespace LetsEncrypt.Azure.Core.Models
{
Expand Down Expand Up @@ -57,11 +58,13 @@ public string AzureWebSitesDefaultDomainName
}
}

[Required]
public Guid ClientId
{
get; private set;
}

[Required]
public string ClientSecret
{
get; private set;
Expand All @@ -75,26 +78,30 @@ public Uri ManagementEndpoint
}
}

[Required]
public string ResourceGroupName
{
get; private set;
}


public string ServicePlanResourceGroupName
{
get; private set;
}

public string SiteSlotName
{
get; private set;
}

[Required]
public Guid SubscriptionId
{
get; private set;
}

[Required]
public string Tenant
{
get; private set;
Expand All @@ -108,6 +115,7 @@ public Uri TokenAudience
}
}

[Required]
public string WebAppName
{
get; private set;
Expand Down
5 changes: 5 additions & 0 deletions LetsEncrypt.SiteExtension.Core/KuduRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ private string CreateToken()
return "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{this.publishingUserName}:{this.publishingPassword}"));
}

public HttpClient HttpClient
{
get { return client; }
}

public async Task<object> GetScmInfo()
{
var res = await client.GetStringAsync($"/api/scm/info");
Expand Down
14 changes: 14 additions & 0 deletions LetsEncrypt.SiteExtension.Core/Models/AcmeConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -8,13 +9,15 @@ namespace LetsEncrypt.Azure.Core.Models
{
public class AcmeConfig : IAcmeConfig
{
[Required]
public string RegistrationEmail { get; set; }

public string BaseUri { get; set; }

/// <summary>
/// The host name the certificate should be issued for.
/// </summary>
[Required]
public string Host { get; set; }

public IEnumerable<string> Hostnames
Expand All @@ -34,8 +37,19 @@ public IEnumerable<string> Hostnames

public List<string> AlternateNames { get; set; }

[Required]
[Range(1024,8096)]
public int RSAKeyLength { get; set; }

public string PFXPassword { get; set; }

/// <summary>
/// Should the Lets Encrypt production environment be used.
/// Only checked if <see cref="BaseUri"/> isn't set.
/// </summary>
public bool UseProduction
{
get; set;
}
}
}
Loading

0 comments on commit b3114ac

Please sign in to comment.