Implemented a basic https certificate issuing using lets encrypt
This commit is contained in:
parent
dda302ebf1
commit
732abe53e0
8 changed files with 264 additions and 6 deletions
|
@ -26,6 +26,8 @@ public class ConfigV1
|
|||
[Description("Specify the latency threshold which has to be reached in order to trigger the warning message")]
|
||||
public int LatencyCheckThreshold { get; set; } = 1000;
|
||||
|
||||
[JsonProperty("LetsEncrypt")] public LetsEncrypt LetsEncrypt { get; set; } = new();
|
||||
|
||||
[JsonProperty("Auth")] public AuthData Auth { get; set; } = new();
|
||||
|
||||
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
||||
|
@ -63,6 +65,33 @@ public class ConfigV1
|
|||
[JsonProperty("Tickets")] public TicketsData Tickets { get; set; } = new();
|
||||
}
|
||||
|
||||
public class LetsEncrypt
|
||||
{
|
||||
[JsonProperty("Enable")]
|
||||
[Description("Enable automatic lets encrypt certificate issuing")]
|
||||
public bool Enable { get; set; } = false;
|
||||
|
||||
[JsonProperty("ExpireEmail")]
|
||||
[Description("Lets encrypt will send you an email upon certificate expiration to this address")]
|
||||
public string ExpireEmail { get; set; } = "your@email.test";
|
||||
|
||||
[JsonProperty("CountryCode")]
|
||||
[Description("Country code to use for generating the certificate")]
|
||||
public string CountryCode { get; set; } = "DE";
|
||||
|
||||
[JsonProperty("State")]
|
||||
[Description("State to use for generating the certificate")]
|
||||
public string State { get; set; } = "Germany";
|
||||
|
||||
[JsonProperty("Locality")]
|
||||
[Description("Locality to use for generating the certificate")]
|
||||
public string Locality { get; set; } = "Bavaria";
|
||||
|
||||
[JsonProperty("Organization")]
|
||||
[Description("Organization to use for generating the certificate")]
|
||||
public string Organization { get; set; } = "Moonlight Panel";
|
||||
}
|
||||
|
||||
public class TicketsData
|
||||
{
|
||||
[JsonProperty("WelcomeMessage")]
|
||||
|
|
|
@ -113,13 +113,18 @@ public class EventSystem
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T> WaitForEvent<T>(string id, object handle, Func<T, bool> filter)
|
||||
public Task<T> WaitForEvent<T>(string id, object handle, Func<T, bool>? filter = null)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
|
||||
Func<T, Task> action = async data =>
|
||||
{
|
||||
if (filter.Invoke(data))
|
||||
if (filter == null)
|
||||
{
|
||||
taskCompletionSource.SetResult(data);
|
||||
await Off(id, handle);
|
||||
}
|
||||
else if(filter.Invoke(data))
|
||||
{
|
||||
taskCompletionSource.SetResult(data);
|
||||
await Off(id, handle);
|
||||
|
|
33
Moonlight/App/Http/Controllers/WellKnown/AcmeController.cs
Normal file
33
Moonlight/App/Http/Controllers/WellKnown/AcmeController.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Services;
|
||||
|
||||
namespace Moonlight.App.Http.Controllers.WellKnown;
|
||||
|
||||
[ApiController]
|
||||
[Route(".well-known/acme-challenge")]
|
||||
public class AcmeController : Controller
|
||||
{
|
||||
private readonly LetsEncryptService LetsEncryptService;
|
||||
private readonly EventSystem Event;
|
||||
|
||||
public AcmeController(LetsEncryptService letsEncryptService, EventSystem eventSystem)
|
||||
{
|
||||
LetsEncryptService = letsEncryptService;
|
||||
Event = eventSystem;
|
||||
}
|
||||
|
||||
[HttpGet("{token}")]
|
||||
public async Task<ActionResult> Get([FromRoute] string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(LetsEncryptService.HttpChallenge) || string.IsNullOrEmpty(LetsEncryptService.HttpChallengeToken))
|
||||
return Problem();
|
||||
|
||||
if (string.IsNullOrEmpty(token) || LetsEncryptService.HttpChallengeToken != token)
|
||||
return Problem();
|
||||
|
||||
await Event.Emit("letsEncrypt.challengeFetched");
|
||||
|
||||
return Ok(LetsEncryptService.HttpChallenge);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ public class ConfigService
|
|||
public ConfigService(StorageService storageService)
|
||||
{
|
||||
StorageService = storageService;
|
||||
StorageService.EnsureCreated();
|
||||
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ML_CONFIG_PATH")))
|
||||
Path = Environment.GetEnvironmentVariable("ML_CONFIG_PATH")!;
|
||||
|
|
|
@ -13,6 +13,7 @@ public class StorageService
|
|||
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
|
||||
Directory.CreateDirectory(PathBuilder.Dir("storage", "certs"));
|
||||
|
||||
await UpdateResources();
|
||||
|
||||
|
|
173
Moonlight/App/Services/LetsEncryptService.cs
Normal file
173
Moonlight/App/Services/LetsEncryptService.cs
Normal file
|
@ -0,0 +1,173 @@
|
|||
using System.Security.Cryptography.X509Certificates;
|
||||
using Certes;
|
||||
using Certes.Acme;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Helpers;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class LetsEncryptService
|
||||
{
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly string LetsEncryptCertPath;
|
||||
private readonly EventSystem Event;
|
||||
private X509Certificate2 Certificate;
|
||||
|
||||
public string HttpChallenge { get; private set; } = "";
|
||||
public string HttpChallengeToken { get; private set; } = "";
|
||||
|
||||
public LetsEncryptService(ConfigService configService, EventSystem eventSystem)
|
||||
{
|
||||
ConfigService = configService;
|
||||
Event = eventSystem;
|
||||
LetsEncryptCertPath = PathBuilder.File("storage", "certs", "letsencrypt.pfx");
|
||||
}
|
||||
|
||||
public async Task AutoProcess()
|
||||
{
|
||||
if (!ConfigService.Get().Moonlight.LetsEncrypt.Enable)
|
||||
return;
|
||||
|
||||
if (await CheckNeedsRenewal())
|
||||
{
|
||||
try
|
||||
{
|
||||
await Renew();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("Unable to issue lets encrypt certificate");
|
||||
Logger.Error(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
Logger.Info("Skipping lets encrypt renewal");
|
||||
|
||||
await LoadCertificate();
|
||||
}
|
||||
|
||||
private Task LoadCertificate()
|
||||
{
|
||||
try
|
||||
{
|
||||
Certificate = new X509Certificate2(
|
||||
LetsEncryptCertPath,
|
||||
ConfigService.Get().Moonlight.Security.Token
|
||||
);
|
||||
|
||||
Logger.Info($"Loaded ssl certificate. '{Certificate.FriendlyName}' issued by '{Certificate.IssuerName.Name}'");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn("Unable to load ssl certificates");
|
||||
Logger.Warn(e);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Renew()
|
||||
{
|
||||
Logger.Info("Renewing lets encrypt certificate");
|
||||
|
||||
var uri = new Uri(ConfigService.Get().Moonlight.AppUrl);
|
||||
var config = ConfigService.Get().Moonlight.LetsEncrypt;
|
||||
|
||||
if (uri.HostNameType == UriHostNameType.IPv4 || uri.HostNameType == UriHostNameType.IPv6)
|
||||
{
|
||||
Logger.Warn("You cannot use an ip to issue a lets encrypt certificate");
|
||||
return;
|
||||
}
|
||||
|
||||
var acmeContext = new AcmeContext(WellKnownServers.LetsEncryptV2);
|
||||
|
||||
Logger.Info($"Starting lets encrypt certificate issuing. Using acme server '{acmeContext.DirectoryUri}'");
|
||||
|
||||
var account = await acmeContext.NewAccount(config.ExpireEmail, true);
|
||||
|
||||
Logger.Info("Creating order");
|
||||
var order = await acmeContext.NewOrder(new[] { uri.Host });
|
||||
var authZ = (await order.Authorizations()).First();
|
||||
|
||||
var challenge = await authZ.Http();
|
||||
|
||||
HttpChallengeToken = challenge.Token;
|
||||
HttpChallenge = challenge.KeyAuthz;
|
||||
|
||||
Logger.Info("Waiting for http challenge to complete");
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||
|
||||
try
|
||||
{
|
||||
await challenge.Validate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error("Unable to validate challenge");
|
||||
Logger.Error(e);
|
||||
}
|
||||
});
|
||||
|
||||
await Event.WaitForEvent<Object>("letsEncrypt.challengeFetched", this);
|
||||
|
||||
Logger.Info("Generating certificate");
|
||||
|
||||
var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
|
||||
|
||||
var certificate = await order.Generate(new CsrInfo
|
||||
{
|
||||
CountryName = config.CountryCode,
|
||||
State = config.State,
|
||||
Locality = config.Locality,
|
||||
Organization = config.Organization,
|
||||
OrganizationUnit = "Dev",
|
||||
CommonName = uri.Host
|
||||
}, privateKey);
|
||||
|
||||
var builder = certificate.ToPfx(privateKey);
|
||||
|
||||
var certBytes = builder.Build(
|
||||
uri.Host,
|
||||
ConfigService.Get().Moonlight.Security.Token
|
||||
);
|
||||
|
||||
Logger.Info($"Saved lets encrypt certificate to '{LetsEncryptCertPath}'");
|
||||
await File.WriteAllBytesAsync(LetsEncryptCertPath, certBytes);
|
||||
}
|
||||
|
||||
private Task<bool> CheckNeedsRenewal()
|
||||
{
|
||||
if (!File.Exists(LetsEncryptCertPath))
|
||||
{
|
||||
Logger.Info("No lets encrypt certificate found");
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
var existingCert = new X509Certificate2(LetsEncryptCertPath, ConfigService.Get().Moonlight.Security.Token);
|
||||
var expirationDate = existingCert.NotAfter;
|
||||
|
||||
if (DateTime.Now < expirationDate)
|
||||
{
|
||||
Logger.Info($"Lets encrypt certificate valid until {Formatter.FormatDate(expirationDate)}");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
Logger.Info("Lets encrypt certificate expired");
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public X509Certificate2? SelectCertificate(ConnectionContext? context, string? domain)
|
||||
{
|
||||
if (context == null)
|
||||
return null;
|
||||
|
||||
Logger.Info(domain);
|
||||
|
||||
return Certificate;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
<PackageReference Include="BlazorDownloadFile" Version="2.4.0.2" />
|
||||
<PackageReference Include="BlazorMonaco" Version="2.1.0" />
|
||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||
<PackageReference Include="Certes" Version="3.0.4" />
|
||||
<PackageReference Include="CloudFlare.Client" Version="6.1.4" />
|
||||
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.5.0" />
|
||||
<PackageReference Include="Discord.Net" Version="3.10.0" />
|
||||
|
|
|
@ -114,6 +114,17 @@ namespace Moonlight
|
|||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var eventSystem = new EventSystem();
|
||||
var letsEncryptService = new LetsEncryptService(configService, eventSystem);
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ConfigureHttpsDefaults(httpsOptions =>
|
||||
{
|
||||
httpsOptions.ServerCertificateSelector = letsEncryptService.SelectCertificate;
|
||||
});
|
||||
});
|
||||
|
||||
var pluginService = new PluginService();
|
||||
await pluginService.BuildServices(builder.Services);
|
||||
|
||||
|
@ -176,7 +187,7 @@ namespace Moonlight
|
|||
builder.Services.AddScoped(typeof(Repository<>));
|
||||
|
||||
// Services
|
||||
builder.Services.AddSingleton<ConfigService>();
|
||||
builder.Services.AddSingleton(configService);
|
||||
builder.Services.AddSingleton<StorageService>();
|
||||
builder.Services.AddScoped<CookieService>();
|
||||
builder.Services.AddScoped<IdentityService>();
|
||||
|
@ -200,7 +211,7 @@ namespace Moonlight
|
|||
builder.Services.AddScoped<WebSpaceService>();
|
||||
builder.Services.AddScoped<StatisticsViewService>();
|
||||
builder.Services.AddSingleton<DateTimeService>();
|
||||
builder.Services.AddSingleton<EventSystem>();
|
||||
builder.Services.AddSingleton(eventSystem);
|
||||
builder.Services.AddScoped<FileDownloadService>();
|
||||
builder.Services.AddScoped<ForgeService>();
|
||||
builder.Services.AddScoped<FabricService>();
|
||||
|
@ -254,6 +265,7 @@ namespace Moonlight
|
|||
builder.Services.AddSingleton<TempMailService>();
|
||||
builder.Services.AddSingleton<DdosProtectionService>();
|
||||
builder.Services.AddSingleton(pluginService);
|
||||
builder.Services.AddSingleton(letsEncryptService);
|
||||
|
||||
// Other
|
||||
builder.Services.AddSingleton<MoonlightService>();
|
||||
|
@ -310,7 +322,12 @@ namespace Moonlight
|
|||
// Discord bot service
|
||||
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();
|
||||
|
||||
await app.RunAsync();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await letsEncryptService.AutoProcess();
|
||||
});
|
||||
|
||||
await app.RunAsync(configService.Get().Moonlight.AppUrl);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue