mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): migrate to helm and k8s, cleanup
This commit is contained in:
parent
35eec5d864
commit
30f5ededa3
@ -8,8 +8,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -20,14 +20,14 @@ enum ApiRoutes {
|
|||||||
// CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
// CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''
|
||||||
|
|
||||||
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
||||||
let result: string = route
|
let result: string = route
|
||||||
args.forEach((arg) => {
|
args.forEach((arg) => {
|
||||||
result = result.replace(/{.*?}/, arg)
|
result = result.replace(/{.*?}/, arg)
|
||||||
})
|
})
|
||||||
// TODO: need env var
|
return `${apiBase.replace(/\/+$/, '')}/${result.replace(/^\/+/, '')}`
|
||||||
return `http://localhost:8080/${result}`
|
|
||||||
//return `http://websrv0001.corp.maks-it.com:8080/${result}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GetApiRoute, ApiRoutes }
|
export { GetApiRoute, ApiRoutes }
|
||||||
|
|||||||
@ -14,7 +14,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||||
|
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,105 +1,93 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading.RateLimiting;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public class LockManager : IDisposable {
|
public class LockManager : IDisposable {
|
||||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||||
private readonly ConcurrentDictionary<int, int> _reentrantCounts = new ConcurrentDictionary<int, int>();
|
private readonly ConcurrentDictionary<int, int> _reentrantCounts = new ConcurrentDictionary<int, int>();
|
||||||
|
private readonly TokenBucketRateLimiter _rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions {
|
||||||
|
TokenLimit = 5, // max 5 requests per second (adjust as needed)
|
||||||
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||||
|
QueueLimit = 100,
|
||||||
|
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
|
||||||
|
TokensPerPeriod = 5,
|
||||||
|
AutoReplenishment = true
|
||||||
|
});
|
||||||
|
|
||||||
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
||||||
|
var lease = await _rateLimiter.AcquireAsync(1);
|
||||||
|
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||||
|
|
||||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||||
_reentrantCounts[threadId] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
|
||||||
await _semaphore.WaitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_reentrantCounts[threadId]++;
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_reentrantCounts[threadId]--;
|
_reentrantCounts[threadId]--;
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||||
_semaphore.Release();
|
lease.Dispose();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
||||||
|
var lease = await _rateLimiter.AcquireAsync(1);
|
||||||
|
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||||
|
|
||||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||||
_reentrantCounts[threadId] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
|
||||||
await _semaphore.WaitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_reentrantCounts[threadId]++;
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_reentrantCounts[threadId]--;
|
_reentrantCounts[threadId]--;
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||||
_semaphore.Release();
|
lease.Dispose();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
||||||
|
var lease = await _rateLimiter.AcquireAsync(1);
|
||||||
|
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||||
|
|
||||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||||
_reentrantCounts[threadId] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
|
||||||
await _semaphore.WaitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_reentrantCounts[threadId]++;
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
return await Task.Run(action);
|
return await Task.Run(action);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_reentrantCounts[threadId]--;
|
_reentrantCounts[threadId]--;
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||||
_semaphore.Release();
|
lease.Dispose();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteWithLockAsync(Action action) {
|
public async Task ExecuteWithLockAsync(Action action) {
|
||||||
|
var lease = await _rateLimiter.AcquireAsync(1);
|
||||||
|
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||||
|
|
||||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||||
_reentrantCounts[threadId] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
|
||||||
await _semaphore.WaitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_reentrantCounts[threadId]++;
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
await Task.Run(action);
|
await Task.Run(action);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_reentrantCounts[threadId]--;
|
_reentrantCounts[threadId]--;
|
||||||
if (_reentrantCounts[threadId] == 0) {
|
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||||
_semaphore.Release();
|
lease.Dispose();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_semaphore.Dispose();
|
_semaphore.Dispose();
|
||||||
|
_rateLimiter.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Deploy-Helm.bat
Normal file
7
src/Deploy-Helm.bat
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
REM Change directory to the location of the script
|
||||||
|
cd /d %~dp0
|
||||||
|
|
||||||
|
REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory
|
||||||
|
powershell -ExecutionPolicy Bypass -File "%~dp0Deploy-Helm.ps1"
|
||||||
65
src/Deploy-Helm.ps1
Normal file
65
src/Deploy-Helm.ps1
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Set variables
|
||||||
|
$projectName = "certs-ui"
|
||||||
|
$namespace = "certs-ui"
|
||||||
|
$chartPath = "./helm"
|
||||||
|
$harborUrl = "cr.maks-it.com"
|
||||||
|
$loadBalancerIP = "172.16.0.5"
|
||||||
|
|
||||||
|
# Retrieve and decode username:password from environment variable (Base64)
|
||||||
|
try {
|
||||||
|
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Env:CR_MAKS_IT))
|
||||||
|
} catch {
|
||||||
|
Write-Error "Failed to decode CR_MAKS_IT as Base64. Expected base64('username:password')."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split decoded credentials
|
||||||
|
$creds = $decoded -split ':', 2
|
||||||
|
$harborUsername = $creds[0]
|
||||||
|
$harborPassword = $creds[1]
|
||||||
|
|
||||||
|
# Verify environment variable
|
||||||
|
if (-not $harborUsername -or -not $harborPassword) {
|
||||||
|
Write-Error "Decoded CR_MAKS_IT must be in the format 'username:password'."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure namespace exists
|
||||||
|
if (-not (kubectl get ns $namespace -o name 2>$null)) {
|
||||||
|
Write-Output "Creating namespace '$namespace'..."
|
||||||
|
kubectl create namespace $namespace | Out-Null
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Output "Namespace '$namespace' already exists."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create or update Docker registry pull secret
|
||||||
|
Write-Output "Creating or updating image pull secret..."
|
||||||
|
kubectl -n $namespace create secret docker-registry cr-maksit-pull `
|
||||||
|
--docker-server=$harborUrl `
|
||||||
|
--docker-username=$harborUsername `
|
||||||
|
--docker-password=$harborPassword `
|
||||||
|
--docker-email="devnull@maks-it.com" `
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f - | Out-Null
|
||||||
|
|
||||||
|
# Lint Helm chart
|
||||||
|
Write-Output "Linting Helm chart..."
|
||||||
|
helm lint $chartPath
|
||||||
|
|
||||||
|
# Render Helm chart to verify output (optional)
|
||||||
|
Write-Output "Rendering Helm chart for validation..."
|
||||||
|
helm template $projectName $chartPath -n $namespace | Out-Null
|
||||||
|
|
||||||
|
# Deploy Helm release
|
||||||
|
Write-Output "Deploying Helm release '$projectName'..."
|
||||||
|
helm upgrade --install $projectName $chartPath -n $namespace `
|
||||||
|
--set imagePullSecret.create=false `
|
||||||
|
--set imagePullSecrets[0].name=cr-maksit-pull `
|
||||||
|
|
||||||
|
# Check deployment status
|
||||||
|
Write-Output "Waiting for deployment rollout..."
|
||||||
|
kubectl -n $namespace rollout status deployment/$projectName-reverseproxy
|
||||||
|
|
||||||
|
# Display service details
|
||||||
|
Write-Output "Service information:"
|
||||||
|
kubectl -n $namespace get svc $projectName-reverseproxy
|
||||||
@ -5,16 +5,10 @@ VisualStudioVersion = 17.6.33815.320
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptConsole", "LetsEncryptConsole\LetsEncryptConsole.csproj", "{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProvider", "SSHProvider\SSHProvider.csproj", "{B6556305-D728-4368-A22C-93079C236808}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
|
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
|
||||||
@ -35,22 +29,10 @@ Global
|
|||||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@ -75,9 +57,6 @@ Global
|
|||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4} = {3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
|
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|||||||
27
src/LetsEncrypt/Entities/ContentType.cs
Normal file
27
src/LetsEncrypt/Entities/ContentType.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MaksIT.LetsEncrypt.Entities
|
||||||
|
{
|
||||||
|
public enum ContentType
|
||||||
|
{
|
||||||
|
[Display(Name = "application/jose+json")]
|
||||||
|
JoseJson,
|
||||||
|
[Display(Name = "application/problem+json")]
|
||||||
|
ProblemJson,
|
||||||
|
[Display(Name = "application/pem-certificate-chain")]
|
||||||
|
PemCertificateChain,
|
||||||
|
[Display(Name = "application/json")]
|
||||||
|
Json
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ContentTypeExtensions
|
||||||
|
{
|
||||||
|
public static string GetDisplayName(this ContentType contentType)
|
||||||
|
{
|
||||||
|
var type = typeof(ContentType);
|
||||||
|
var memInfo = type.GetMember(contentType.ToString());
|
||||||
|
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
|
||||||
|
return attributes.Length > 0 ? ((DisplayAttribute)attributes[0]).Name : contentType.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/LetsEncrypt/Entities/OrderStatus.cs
Normal file
27
src/LetsEncrypt/Entities/OrderStatus.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MaksIT.LetsEncrypt.Entities
|
||||||
|
{
|
||||||
|
public enum OrderStatus
|
||||||
|
{
|
||||||
|
[Display(Name = "pending")]
|
||||||
|
Pending,
|
||||||
|
[Display(Name = "valid")]
|
||||||
|
Valid,
|
||||||
|
[Display(Name = "ready")]
|
||||||
|
Ready,
|
||||||
|
[Display(Name = "processing")]
|
||||||
|
Processing
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OrderStatusExtensions
|
||||||
|
{
|
||||||
|
public static string GetDisplayName(this OrderStatus status)
|
||||||
|
{
|
||||||
|
var type = typeof(OrderStatus);
|
||||||
|
var memInfo = type.GetMember(status.ToString());
|
||||||
|
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
|
||||||
|
return attributes.Length > 0 ? ((DisplayAttribute)attributes[0]).Name : status.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,12 +8,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
|
<PackageReference Include="DomainResult.Common" Version="3.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -22,7 +22,6 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
|
|||||||
using MaksIT.LetsEncrypt.Models.Requests;
|
using MaksIT.LetsEncrypt.Models.Requests;
|
||||||
using MaksIT.LetsEncrypt.Entities.Jws;
|
using MaksIT.LetsEncrypt.Entities.Jws;
|
||||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncrypt.Services;
|
namespace MaksIT.LetsEncrypt.Services;
|
||||||
|
|
||||||
@ -39,6 +38,10 @@ public interface ILetsEncryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class LetsEncryptService : ILetsEncryptService {
|
public class LetsEncryptService : ILetsEncryptService {
|
||||||
|
private const string DnsType = "dns";
|
||||||
|
private const string DirectoryEndpoint = "directory";
|
||||||
|
private const string ReplayNonceHeader = "Replay-Nonce";
|
||||||
|
|
||||||
private readonly ILogger<LetsEncryptService> _logger;
|
private readonly ILogger<LetsEncryptService> _logger;
|
||||||
private readonly LetsEncryptConfiguration _appSettings;
|
private readonly LetsEncryptConfiguration _appSettings;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
@ -57,40 +60,61 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private State GetOrCreateState(Guid sessionId) {
|
private State GetOrCreateState(Guid sessionId) {
|
||||||
if (!_memoryCache.TryGetValue(sessionId, out State state)) {
|
if (!_memoryCache.TryGetValue(sessionId, out State? state) || state == null) {
|
||||||
state = new State();
|
state = new State();
|
||||||
_memoryCache.Set(sessionId, state, TimeSpan.FromHours(1));
|
_memoryCache.Set(sessionId, state, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Send ACME request and process response
|
||||||
|
private async Task<SendResult<T>> SendAcmeRequest<T>(HttpRequestMessage request, State state, HttpMethod method) {
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
UpdateStateNonceIfNeeded(response, state, method);
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
HandleProblemResponseAsync(response, responseText);
|
||||||
|
return ProcessResponseContent<T>(response, responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Poll challenge status until valid or timeout
|
||||||
|
private async Task<IDomainResult> PollChallengeStatus(Guid sessionId, AuthorizationChallengeChallenge challenge, State state) {
|
||||||
|
if (challenge?.Url == null) return IDomainResult.Failed("Challenge URL is null");
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
while (true) {
|
||||||
|
var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||||
|
await HandleNonceAsync(sessionId, challenge.Url, state);
|
||||||
|
var pollJson = EncodeMessage(true, null, state, new JwsHeader {
|
||||||
|
Url = challenge.Url,
|
||||||
|
Nonce = state.Nonce
|
||||||
|
});
|
||||||
|
PrepareRequestContent(pollRequest, pollJson, HttpMethod.Post);
|
||||||
|
var pollResponse = await _httpClient.SendAsync(pollRequest);
|
||||||
|
UpdateStateNonceIfNeeded(pollResponse, state, HttpMethod.Post);
|
||||||
|
var pollResponseText = await pollResponse.Content.ReadAsStringAsync();
|
||||||
|
HandleProblemResponseAsync(pollResponse, pollResponseText);
|
||||||
|
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(pollResponse, pollResponseText);
|
||||||
|
if (authChallenge.Result?.Status != "pending")
|
||||||
|
return authChallenge.Result?.Status == "valid" ? IDomainResult.Success() : IDomainResult.Failed();
|
||||||
|
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
|
return IDomainResult.Failed("Timeout");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region ConfigureClient
|
#region ConfigureClient
|
||||||
public async Task<IDomainResult> ConfigureClient(Guid sessionId, bool isStaging) {
|
public async Task<IDomainResult> ConfigureClient(Guid sessionId, bool isStaging) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
state.IsStaging = isStaging;
|
state.IsStaging = isStaging;
|
||||||
// TODO: need to propagate from Configuration
|
|
||||||
_httpClient.BaseAddress ??= new Uri(isStaging ? _appSettings.Staging : _appSettings.Production);
|
_httpClient.BaseAddress ??= new Uri(isStaging ? _appSettings.Staging : _appSettings.Production);
|
||||||
|
|
||||||
if (state.Directory == null) {
|
if (state.Directory == null) {
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("directory", UriKind.Relative));
|
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(DirectoryEndpoint, UriKind.Relative));
|
||||||
await HandleNonceAsync(sessionId, new Uri("directory", UriKind.Relative), state);
|
await HandleNonceAsync(sessionId, new Uri(DirectoryEndpoint, UriKind.Relative), state);
|
||||||
|
var directory = await SendAcmeRequest<AcmeDirectory>(request, state, HttpMethod.Get);
|
||||||
var response = await _httpClient.SendAsync(request);
|
state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null");
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Get);
|
|
||||||
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var directory = ProcessResponseContent<AcmeDirectory>(response, responseText);
|
|
||||||
|
|
||||||
state.Directory = directory.Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
const string message = "Let's Encrypt client unhandled exception";
|
const string message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
@ -104,64 +128,42 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
_logger.LogError("Invalid sessionId");
|
_logger.LogError("Invalid sessionId");
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contacts == null || contacts.Length == 0) {
|
if (contacts == null || contacts.Length == 0) {
|
||||||
_logger.LogError("Contacts are null or empty");
|
_logger.LogError("Contacts are null or empty");
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
if (state.Directory == null) {
|
if (state.Directory == null) {
|
||||||
_logger.LogError("State directory is null");
|
_logger.LogError("State directory is null");
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(Init)}...");
|
_logger.LogInformation($"Executing {nameof(Init)}...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var accountKey = new RSACryptoServiceProvider(4096);
|
var accountKey = new RSACryptoServiceProvider(4096);
|
||||||
|
|
||||||
if (cache != null && cache.AccountKey != null) {
|
if (cache != null && cache.AccountKey != null) {
|
||||||
state.Cache = cache;
|
state.Cache = cache;
|
||||||
accountKey.ImportCspBlob(cache.AccountKey);
|
accountKey.ImportCspBlob(cache.AccountKey);
|
||||||
state.JwsService = new JwsService(accountKey);
|
state.JwsService = new JwsService(accountKey);
|
||||||
state.JwsService.SetKeyId(cache.Location.ToString());
|
state.JwsService.SetKeyId(cache.Location?.ToString() ?? string.Empty);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
state.JwsService = new JwsService(accountKey);
|
state.JwsService = new JwsService(accountKey);
|
||||||
|
|
||||||
var letsEncryptOrder = new Account {
|
var letsEncryptOrder = new Account {
|
||||||
TermsOfServiceAgreed = true,
|
TermsOfServiceAgreed = true,
|
||||||
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewAccount);
|
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewAccount);
|
||||||
|
|
||||||
|
|
||||||
await HandleNonceAsync(sessionId, state.Directory.NewAccount, state);
|
await HandleNonceAsync(sessionId, state.Directory.NewAccount, state);
|
||||||
|
|
||||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||||
Url = state.Directory.NewAccount,
|
Url = state.Directory.NewAccount,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var result = await SendAcmeRequest<Account>(request, state, HttpMethod.Post);
|
||||||
var response = await _httpClient.SendAsync(request);
|
state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty);
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
if (result.Result?.Status != "valid") {
|
||||||
|
_logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}");
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var result = ProcessResponseContent<Account>(response, responseText);
|
|
||||||
|
|
||||||
state.JwsService.SetKeyId(result.Result.Location.ToString());
|
|
||||||
|
|
||||||
if (result.Result.Status != "valid") {
|
|
||||||
_logger.LogError($"Account status is not valid, was: {result.Result.Status} \r\n {result.ResponseText}");
|
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Cache = new RegistrationCache {
|
state.Cache = new RegistrationCache {
|
||||||
AccountId = accountId,
|
AccountId = accountId,
|
||||||
Description = description,
|
Description = description,
|
||||||
@ -169,14 +171,12 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
IsStaging = state.IsStaging,
|
IsStaging = state.IsStaging,
|
||||||
Location = result.Result.Location,
|
Location = result.Result.Location,
|
||||||
AccountKey = accountKey.ExportCspBlob(true),
|
AccountKey = accountKey.ExportCspBlob(true),
|
||||||
Id = result.Result.Id,
|
Id = result.Result.Id ?? string.Empty,
|
||||||
Key = result.Result.Key
|
Key = result.Result.Key
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
const string message = "Let's Encrypt client unhandled exception";
|
const string message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
@ -186,10 +186,8 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
|
|
||||||
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
|
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
if(state?.Cache == null)
|
if(state?.Cache == null)
|
||||||
return IDomainResult.Failed<RegistrationCache?>();
|
return IDomainResult.Failed<RegistrationCache?>();
|
||||||
|
|
||||||
return IDomainResult.Success(state.Cache);
|
return IDomainResult.Success(state.Cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,18 +195,13 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
|
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
||||||
|
if (state.Directory?.Meta?.TermsOfService == null) {
|
||||||
if (state.Directory == null) {
|
|
||||||
return IDomainResult.Failed<string?>();
|
return IDomainResult.Failed<string?>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
|
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||||
}
|
}
|
||||||
@ -219,107 +212,76 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||||
|
|
||||||
state.Challenges.Clear();
|
state.Challenges.Clear();
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
var letsEncryptOrder = new Order {
|
||||||
Expires = DateTime.UtcNow.AddDays(2),
|
Expires = DateTime.UtcNow.AddDays(2),
|
||||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
|
||||||
Type = "dns",
|
Type = DnsType,
|
||||||
Value = hostname
|
Value = hostname ?? string.Empty
|
||||||
}).ToArray()
|
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
||||||
};
|
};
|
||||||
|
if (state.Directory == null || state.Directory.NewOrder == null)
|
||||||
|
return (null, IDomainResult.Failed());
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
||||||
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
||||||
|
|
||||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||||
Url = state.Directory.NewOrder,
|
Url = state.Directory.NewOrder,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
var response = await _httpClient.SendAsync(request);
|
if (StatusEquals(order.Result?.Status, OrderStatus.Ready))
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
return (new Dictionary<string, string>(), IDomainResult.Success());
|
||||||
|
if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) {
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
_logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}");
|
||||||
HandleProblemResponseAsync(response, responseText);
|
return (null, IDomainResult.Failed());
|
||||||
|
|
||||||
var order = ProcessResponseContent<Order>(response, responseText);
|
|
||||||
|
|
||||||
if (order.Result.Status == "ready")
|
|
||||||
return IDomainResult.Success(new Dictionary<string, string>());
|
|
||||||
|
|
||||||
if (order.Result.Status != "pending") {
|
|
||||||
_logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
|
|
||||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.CurrentOrder = order.Result;
|
state.CurrentOrder = order.Result;
|
||||||
|
|
||||||
var results = new Dictionary<string, string>();
|
var results = new Dictionary<string, string>();
|
||||||
foreach (var item in state.CurrentOrder.Authorizations) {
|
foreach (var item in state.CurrentOrder?.Authorizations ?? Array.Empty<Uri>()) {
|
||||||
|
if (item == null) continue;
|
||||||
request = new HttpRequestMessage(HttpMethod.Post, item);
|
request = new HttpRequestMessage(HttpMethod.Post, item);
|
||||||
await HandleNonceAsync(sessionId, item, state);
|
await HandleNonceAsync(sessionId, item, state);
|
||||||
|
|
||||||
json = EncodeMessage(true, null, state, new JwsHeader {
|
json = EncodeMessage(true, null, state, new JwsHeader {
|
||||||
Url = item,
|
Url = item,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var challengeResponse = await SendAcmeRequest<AuthorizationChallengeResponse>(request, state, HttpMethod.Post);
|
||||||
|
if (StatusEquals(challengeResponse.Result?.Status, OrderStatus.Valid))
|
||||||
|
|
||||||
response = await _httpClient.SendAsync(request);
|
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
|
||||||
|
|
||||||
responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var challengeResponse = ProcessResponseContent<AuthorizationChallengeResponse>(response, responseText);
|
|
||||||
|
|
||||||
|
|
||||||
if (challengeResponse.Result.Status == "valid")
|
|
||||||
continue;
|
continue;
|
||||||
|
if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) {
|
||||||
if (challengeResponse.Result.Status != "pending") {
|
_logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}");
|
||||||
_logger.LogError($"Expected authorization status 'pending', but got: {state.CurrentOrder.Status} \r\n {challengeResponse.ResponseText}");
|
return (null, IDomainResult.Failed());
|
||||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
}
|
||||||
|
var challenge = challengeResponse.Result?.Challenges?.FirstOrDefault(x => x?.Type == challengeType);
|
||||||
|
if (challenge == null || challenge.Token == null) {
|
||||||
|
_logger.LogError("Challenge or token is null");
|
||||||
|
return (null, IDomainResult.Failed());
|
||||||
}
|
}
|
||||||
|
|
||||||
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
|
|
||||||
state.Challenges.Add(challenge);
|
state.Challenges.Add(challenge);
|
||||||
state.Cache.ChallengeType = challengeType;
|
if (state.Cache != null) state.Cache.ChallengeType = challengeType;
|
||||||
|
var keyToken = state.JwsService != null ? state.JwsService.GetKeyAuthorization(challenge.Token) : string.Empty;
|
||||||
var keyToken = state.JwsService.GetKeyAuthorization(challenge.Token);
|
|
||||||
|
|
||||||
switch (challengeType) {
|
switch (challengeType) {
|
||||||
case "dns-01":
|
case "dns-01":
|
||||||
using (var sha256 = SHA256.Create()) {
|
using (var sha256 = SHA256.Create()) {
|
||||||
var dnsToken = state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
var dnsToken = state.JwsService != null ? state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken ?? string.Empty))) : string.Empty;
|
||||||
results[challengeResponse.Result.Identifier.Value] = dnsToken;
|
results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = dnsToken;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "http-01":
|
case "http-01":
|
||||||
results[challengeResponse.Result.Identifier.Value] = keyToken;
|
results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = keyToken ?? string.Empty;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return (results, IDomainResult.Success());
|
||||||
return IDomainResult.Success(results);
|
} catch (Exception ex) {
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
|
return (null, IDomainResult.CriticalDependencyError(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@ -328,68 +290,31 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public async Task<IDomainResult> CompleteChallenges(Guid sessionId) {
|
public async Task<IDomainResult> CompleteChallenges(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||||
|
|
||||||
if (state.CurrentOrder?.Identifiers == null) {
|
if (state.CurrentOrder?.Identifiers == null) {
|
||||||
return IDomainResult.Failed("Current order identifiers are null");
|
return IDomainResult.Failed("Current order identifiers are null");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var index = 0; index < state.Challenges.Count; index++) {
|
for (var index = 0; index < state.Challenges.Count; index++) {
|
||||||
var challenge = state.Challenges[index];
|
var challenge = state.Challenges[index];
|
||||||
var start = DateTime.UtcNow;
|
if (challenge?.Url == null) {
|
||||||
|
_logger.LogError("Challenge URL is null");
|
||||||
while (true) {
|
return IDomainResult.Failed();
|
||||||
var authorizeChallenge = new AuthorizeChallenge();
|
|
||||||
|
|
||||||
switch (challenge.Type) {
|
|
||||||
case "dns-01":
|
|
||||||
authorizeChallenge.KeyAuthorization = state.JwsService.GetKeyAuthorization(challenge.Token);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "http-01":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
|
||||||
await HandleNonceAsync(sessionId, challenge.Url, state);
|
|
||||||
|
|
||||||
var json = EncodeMessage(false, "{}", state, new JwsHeader {
|
|
||||||
Url = challenge.Url,
|
|
||||||
Nonce = state.Nonce
|
|
||||||
});
|
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
|
||||||
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(response, responseText);
|
|
||||||
//return IDomainResult.Success(result);
|
|
||||||
|
|
||||||
|
|
||||||
if (authChallenge.Result.Status == "valid")
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (authChallenge.Result.Status != "pending") {
|
|
||||||
_logger.LogError($"Challenge failed with status {authChallenge.Result.Status} \r\n {authChallenge.ResponseText}");
|
|
||||||
return IDomainResult.Failed();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
|
||||||
return IDomainResult.Failed("Timeout");
|
|
||||||
}
|
}
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||||
|
await HandleNonceAsync(sessionId, challenge.Url, state);
|
||||||
|
var json = EncodeMessage(false, "{}", state, new JwsHeader {
|
||||||
|
Url = challenge.Url,
|
||||||
|
Nonce = state.Nonce
|
||||||
|
});
|
||||||
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var authChallenge = await SendAcmeRequest<AuthorizationChallengeResponse>(request, state, HttpMethod.Post);
|
||||||
|
var result = await PollChallengeStatus(sessionId, challenge, state);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
}
|
}
|
||||||
@ -400,40 +325,26 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public async Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames) {
|
public async Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames) {
|
||||||
try {
|
try {
|
||||||
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||||
|
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
var letsEncryptOrder = new Order {
|
var letsEncryptOrder = new Order {
|
||||||
Expires = DateTime.UtcNow.AddDays(2),
|
Expires = DateTime.UtcNow.AddDays(2),
|
||||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
|
||||||
Type = "dns",
|
Type = "dns",
|
||||||
Value = hostname
|
Value = hostname!
|
||||||
}).ToArray()
|
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
||||||
};
|
};
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.NewOrder);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
|
||||||
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
||||||
|
|
||||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||||
Url = state.Directory.NewOrder,
|
Url = state.Directory.NewOrder,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
var response = await _httpClient.SendAsync(request);
|
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
|
||||||
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var order = ProcessResponseContent<Order>(response, responseText);
|
|
||||||
state.CurrentOrder = order.Result;
|
state.CurrentOrder = order.Result;
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
}
|
}
|
||||||
@ -444,136 +355,89 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public async Task<IDomainResult> GetCertificate(Guid sessionId, string subject) {
|
public async Task<IDomainResult> GetCertificate(Guid sessionId, string subject) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
||||||
|
if (state.CurrentOrder?.Identifiers == null) {
|
||||||
if (state.CurrentOrder == null) {
|
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = new RSACryptoServiceProvider(4096);
|
var key = new RSACryptoServiceProvider(4096);
|
||||||
var csr = new CertificateRequest("CN=" + subject,
|
var csr = new CertificateRequest("CN=" + subject,
|
||||||
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
var san = new SubjectAlternativeNameBuilder();
|
var san = new SubjectAlternativeNameBuilder();
|
||||||
foreach (var host in state.CurrentOrder.Identifiers)
|
foreach (var host in state.CurrentOrder.Identifiers) {
|
||||||
san.AddDnsName(host.Value);
|
if (host?.Value != null)
|
||||||
|
san.AddDnsName(host.Value);
|
||||||
|
}
|
||||||
csr.CertificateExtensions.Add(san.Build());
|
csr.CertificateExtensions.Add(san.Build());
|
||||||
|
|
||||||
var letsEncryptOrder = new FinalizeRequest {
|
var letsEncryptOrder = new FinalizeRequest {
|
||||||
Csr = state.JwsService.Base64UrlEncoded(csr.CreateSigningRequest())
|
Csr = state.JwsService!.Base64UrlEncoded(csr.CreateSigningRequest())
|
||||||
};
|
};
|
||||||
|
|
||||||
Uri? certificateUrl = default;
|
Uri? certificateUrl = default;
|
||||||
|
|
||||||
var start = DateTime.UtcNow;
|
var start = DateTime.UtcNow;
|
||||||
|
|
||||||
while (certificateUrl == null) {
|
while (certificateUrl == null) {
|
||||||
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
|
var hostnames = state.CurrentOrder.Identifiers?.Select(x => x?.Value).Where(x => x != null).Cast<string>().ToArray() ?? Array.Empty<string>();
|
||||||
await GetOrder(sessionId, state.CurrentOrder.Identifiers.Select(x => x.Value).ToArray());
|
await GetOrder(sessionId, hostnames);
|
||||||
|
var status = state.CurrentOrder?.Status;
|
||||||
if (state.CurrentOrder.Status == "ready") {
|
if (StatusEquals(status, OrderStatus.Ready)) {
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize!);
|
||||||
|
await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize!, state);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize);
|
|
||||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize, state);
|
|
||||||
|
|
||||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||||
Url = state.CurrentOrder.Finalize,
|
Url = state.CurrentOrder.Finalize,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
var response = await _httpClient.SendAsync(request);
|
if (StatusEquals(order.Result?.Status, OrderStatus.Processing)) {
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location!);
|
||||||
|
await HandleNonceAsync(sessionId, state.CurrentOrder.Location!, state);
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
var order = ProcessResponseContent<Order>(response, responseText);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (order.Result.Status == "processing") {
|
|
||||||
|
|
||||||
request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location);
|
|
||||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Location, state);
|
|
||||||
|
|
||||||
json = EncodeMessage(true, null, state, new JwsHeader {
|
json = EncodeMessage(true, null, state, new JwsHeader {
|
||||||
Url = state.CurrentOrder.Location,
|
Url = state.CurrentOrder.Location,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||||
|
order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||||
response = await _httpClient.SendAsync(request);
|
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
|
||||||
|
|
||||||
responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(response, responseText);
|
|
||||||
|
|
||||||
order = ProcessResponseContent<Order>(response, responseText);
|
|
||||||
}
|
}
|
||||||
|
if (StatusEquals(order.Result?.Status, OrderStatus.Valid)) {
|
||||||
if (order.Result.Status == "valid") {
|
|
||||||
certificateUrl = order.Result.Certificate;
|
certificateUrl = order.Result.Certificate;
|
||||||
}
|
}
|
||||||
|
} else if (StatusEquals(status, OrderStatus.Valid)) {
|
||||||
|
certificateUrl = state.CurrentOrder.Certificate;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||||
throw new TimeoutException();
|
throw new TimeoutException();
|
||||||
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
}
|
}
|
||||||
|
var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl!);
|
||||||
|
await HandleNonceAsync(sessionId, certificateUrl!, state);
|
||||||
|
|
||||||
var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl);
|
|
||||||
await HandleNonceAsync(sessionId, certificateUrl, state);
|
|
||||||
|
|
||||||
var finalJson = EncodeMessage(true, null, state, new JwsHeader {
|
var finalJson = EncodeMessage(true, null, state, new JwsHeader {
|
||||||
Url = certificateUrl,
|
Url = certificateUrl,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
});
|
});
|
||||||
PrepareRequestContent(finalRequest, finalJson, HttpMethod.Post);
|
PrepareRequestContent(finalRequest, finalJson, HttpMethod.Post);
|
||||||
|
var pem = await SendAcmeRequest<string>(finalRequest, state, HttpMethod.Post);
|
||||||
var finalResponse = await _httpClient.SendAsync(finalRequest);
|
|
||||||
UpdateStateNonceIfNeededAsync(finalResponse, state, HttpMethod.Post);
|
|
||||||
|
|
||||||
var finalResponseText = await finalResponse.Content.ReadAsStringAsync();
|
|
||||||
HandleProblemResponseAsync(finalResponse, finalResponseText);
|
|
||||||
|
|
||||||
var pem = ProcessResponseContent<string>(finalResponse, finalResponseText);
|
|
||||||
|
|
||||||
|
|
||||||
if (state.Cache == null) {
|
if (state.Cache == null) {
|
||||||
_logger.LogError($"{nameof(state.Cache)} is null");
|
_logger.LogError($"{nameof(state.Cache)} is null");
|
||||||
return IDomainResult.Failed();
|
return IDomainResult.Failed();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||||
state.Cache.CachedCerts[subject] = new CertificateCache {
|
state.Cache.CachedCerts[subject] = new CertificateCache {
|
||||||
Cert = pem.Result,
|
Cert = pem.Result ?? string.Empty,
|
||||||
Private = key.ExportCspBlob(true),
|
Private = key.ExportCspBlob(true),
|
||||||
PrivatePem = key.ExportRSAPrivateKeyPem()
|
PrivatePem = key.ExportRSAPrivateKeyPem()
|
||||||
};
|
};
|
||||||
|
var certPem = pem.Result ?? string.Empty;
|
||||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
if (!string.IsNullOrEmpty(certPem)) {
|
||||||
|
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||||
|
}
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError(message);
|
return IDomainResult.CriticalDependencyError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
public Task<IDomainResult> KeyChange(Guid sessionId) {
|
public Task<IDomainResult> KeyChange(Guid sessionId) {
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@ -581,58 +445,44 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
|
public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
|
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
|
||||||
|
if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) {
|
||||||
if (state.Cache == null || state.Cache.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache)) {
|
|
||||||
_logger.LogError("Certificate not found in cache");
|
_logger.LogError("Certificate not found in cache");
|
||||||
return IDomainResult.Failed("Certificate not found");
|
return IDomainResult.Failed("Certificate not found");
|
||||||
}
|
}
|
||||||
|
var certPem = certificateCache.Cert ?? string.Empty;
|
||||||
// Load the certificate from PEM format and convert it to DER format
|
if (string.IsNullOrEmpty(certPem)) {
|
||||||
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certificateCache.Cert));
|
_logger.LogError("Certificate PEM is null or empty");
|
||||||
|
return IDomainResult.Failed("Certificate PEM is null or empty");
|
||||||
|
}
|
||||||
|
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||||
var derEncodedCert = certificate.Export(X509ContentType.Cert);
|
var derEncodedCert = certificate.Export(X509ContentType.Cert);
|
||||||
var base64UrlEncodedCert = state.JwsService.Base64UrlEncoded(derEncodedCert);
|
var base64UrlEncodedCert = state.JwsService!.Base64UrlEncoded(derEncodedCert);
|
||||||
|
|
||||||
|
|
||||||
var revokeRequest = new RevokeRequest {
|
var revokeRequest = new RevokeRequest {
|
||||||
Certificate = base64UrlEncodedCert,
|
Certificate = base64UrlEncodedCert,
|
||||||
Reason = (int)reason
|
Reason = (int)reason
|
||||||
};
|
};
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.RevokeCert);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.RevokeCert);
|
|
||||||
await HandleNonceAsync(sessionId, state.Directory.RevokeCert, state);
|
await HandleNonceAsync(sessionId, state.Directory.RevokeCert, state);
|
||||||
|
|
||||||
var jwsHeader = new JwsHeader {
|
var jwsHeader = new JwsHeader {
|
||||||
Url = state.Directory.RevokeCert,
|
Url = state.Directory.RevokeCert,
|
||||||
Nonce = state.Nonce
|
Nonce = state.Nonce
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = state.JwsService.Encode(revokeRequest, jwsHeader).ToJson();
|
var json = state.JwsService.Encode(revokeRequest, jwsHeader).ToJson();
|
||||||
|
|
||||||
request.Content = new StringContent(json);
|
request.Content = new StringContent(json);
|
||||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/jose+json");
|
request.Content.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(ContentType.JoseJson));
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
UpdateStateNonceIfNeeded(response, state, HttpMethod.Post);
|
||||||
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
|
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
|
||||||
var erroObj = responseText.ToObject<Problem>();
|
var erroObj = responseText.ToObject<Problem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
IDomainResult.CriticalDependencyError(responseText);
|
IDomainResult.CriticalDependencyError(responseText);
|
||||||
|
|
||||||
|
|
||||||
// Remove the certificate from the cache after successful revocation
|
|
||||||
state.Cache.CachedCerts.Remove(subject);
|
state.Cache.CachedCerts.Remove(subject);
|
||||||
|
|
||||||
_logger.LogInformation("Certificate revoked successfully");
|
_logger.LogInformation("Certificate revoked successfully");
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
|
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
|
||||||
@ -641,6 +491,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
|
|
||||||
#region SendAsync
|
#region SendAsync
|
||||||
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
||||||
|
if (uri == null) throw new ArgumentNullException(nameof(uri));
|
||||||
if (uri.OriginalString != "directory") {
|
if (uri.OriginalString != "directory") {
|
||||||
var (nonce, newNonceResult) = await NewNonce(sessionId);
|
var (nonce, newNonceResult) = await NewNonce(sessionId);
|
||||||
if (!newNonceResult.IsSuccess || nonce == null) {
|
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||||
@ -656,63 +507,63 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
|
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
var state = GetOrCreateState(sessionId);
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
||||||
|
if (state.Directory?.NewNonce == null)
|
||||||
if (state.Directory == null)
|
return (null, IDomainResult.Failed());
|
||||||
IDomainResult.Failed();
|
|
||||||
|
|
||||||
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
||||||
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
|
var nonce = result.Headers.GetValues("Replay-Nonce").FirstOrDefault();
|
||||||
|
return (nonce, IDomainResult.Success());
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Let's Encrypt client unhandled exception";
|
var message = "Let's Encrypt client unhandled exception";
|
||||||
|
|
||||||
_logger.LogError(ex, message);
|
_logger.LogError(ex, message);
|
||||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
return (null, IDomainResult.CriticalDependencyError(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
|
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
|
||||||
return isPostAsGet
|
return isPostAsGet
|
||||||
? state.JwsService.Encode(jwsHeader).ToJson()
|
? state.JwsService!.Encode(jwsHeader).ToJson()
|
||||||
: state.JwsService.Encode(requestModel, jwsHeader).ToJson();
|
: state.JwsService!.Encode(requestModel, jwsHeader).ToJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetContentType(ContentType type) => type.GetDisplayName();
|
||||||
|
|
||||||
private void PrepareRequestContent(HttpRequestMessage request, string json, HttpMethod method) {
|
private void PrepareRequestContent(HttpRequestMessage request, string json, HttpMethod method) {
|
||||||
request.Content = new StringContent(json);
|
request.Content = new StringContent(json ?? string.Empty);
|
||||||
var contentType = method == HttpMethod.Post ? "application/jose+json" : "application/json";
|
var contentType = method == HttpMethod.Post ? GetContentType(ContentType.JoseJson) : GetContentType(ContentType.Json);
|
||||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateStateNonceIfNeededAsync(HttpResponseMessage response, State state, HttpMethod method) {
|
|
||||||
if (method == HttpMethod.Post && response.Headers.Contains("Replay-Nonce")) {
|
|
||||||
state.Nonce = response.Headers.GetValues("Replay-Nonce").First();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleProblemResponseAsync(HttpResponseMessage response, string responseText) {
|
private void HandleProblemResponseAsync(HttpResponseMessage response, string responseText) {
|
||||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
|
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
|
||||||
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendResult<TResult> ProcessResponseContent<TResult>(HttpResponseMessage response, string responseText) {
|
private SendResult<TResult> ProcessResponseContent<TResult>(HttpResponseMessage response, string responseText) {
|
||||||
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
|
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.PemCertificateChain) && typeof(TResult) == typeof(string)) {
|
||||||
return new SendResult<TResult> {
|
return new SendResult<TResult> {
|
||||||
Result = (TResult)(object)responseText
|
Result = (TResult)(object)responseText
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseContent = responseText.ToObject<TResult>();
|
var responseContent = responseText.ToObject<TResult>();
|
||||||
if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
|
if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
|
||||||
ihl.Location = response.Headers.Location;
|
ihl.Location = response.Headers.Location;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SendResult<TResult> {
|
return new SendResult<TResult> {
|
||||||
Result = responseContent,
|
Result = responseContent,
|
||||||
ResponseText = responseText
|
ResponseText = responseText
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private void UpdateStateNonceIfNeeded(HttpResponseMessage response, State state, HttpMethod method) {
|
||||||
|
if (method == HttpMethod.Post && response.Headers.Contains(ReplayNonceHeader)) {
|
||||||
|
state.Nonce = response.Headers.GetValues(ReplayNonceHeader).FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for status comparison
|
||||||
|
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DomainResult" Version="3.2.0" />
|
<PackageReference Include="DomainResult" Version="3.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
7
src/Release-DockerImage.bat
Normal file
7
src/Release-DockerImage.bat
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
REM Change directory to the location of the script
|
||||||
|
cd /d %~dp0
|
||||||
|
|
||||||
|
REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory
|
||||||
|
powershell -ExecutionPolicy Bypass -File "%~dp0Release-DockerImage.ps1"
|
||||||
59
src/Release-DockerImage.ps1
Normal file
59
src/Release-DockerImage.ps1
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Set variables
|
||||||
|
$projectName = "certs-ui"
|
||||||
|
$harborUrl = "cr.maks-it.com" # e.g., "harbor.yourdomain.com"
|
||||||
|
$tag = "latest" # Customize the tag as needed
|
||||||
|
|
||||||
|
# Retrieve and decode username:password from environment variable (Base64 encoded)
|
||||||
|
try {
|
||||||
|
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Env:CR_MAKS_IT))
|
||||||
|
} catch {
|
||||||
|
throw "Failed to decode CR_MAKS_IT as Base64. Ensure it's base64('username:password'). Error: $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split decoded credentials
|
||||||
|
$creds = $decoded -split ':', 2
|
||||||
|
if ($creds.Count -ne 2) {
|
||||||
|
throw "Invalid decoded CR_MAKS_IT format. Expected 'username:password'."
|
||||||
|
}
|
||||||
|
|
||||||
|
$harborUsername = $creds[0]
|
||||||
|
$harborPassword = $creds[1]
|
||||||
|
|
||||||
|
# Authenticate with Harbor
|
||||||
|
Write-Output "Logging into $harborUrl as $harborUsername..."
|
||||||
|
$loginResult = $harborPassword | docker login $harborUrl -u $harborUsername --password-stdin 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0 -or ($loginResult -notmatch "Login Succeeded")) {
|
||||||
|
throw "Docker login failed for $harborUrl.`n$loginResult"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List of services to build and push with the current context
|
||||||
|
$services = @{
|
||||||
|
"reverseproxy" = "ReverseProxy/Dockerfile"
|
||||||
|
"server" = "LetsEncryptServer/Dockerfile"
|
||||||
|
"client" = "ClientApp/Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
$contextPath = "."
|
||||||
|
|
||||||
|
foreach ($service in $services.Keys) {
|
||||||
|
$dockerfilePath = $services[$service]
|
||||||
|
$imageName = "$harborUrl/$projectName/${service}:${tag}"
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
Write-Output "Building image $imageName from $dockerfilePath..."
|
||||||
|
docker build -t $imageName -f $dockerfilePath $contextPath
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker build failed for $imageName"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push the Docker image
|
||||||
|
Write-Output "Pushing image $imageName..."
|
||||||
|
docker push $imageName
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Docker push failed for $imageName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logout after pushing images
|
||||||
|
docker logout $harborUrl | Out-Null
|
||||||
|
Write-Output "Completed successfully."
|
||||||
14
src/ReleaseAndDeploy.bat
Normal file
14
src/ReleaseAndDeploy.bat
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Get the directory of the current script
|
||||||
|
set "SCRIPT_DIR=%~dp0"
|
||||||
|
|
||||||
|
REM Run the first batch file
|
||||||
|
call "%SCRIPT_DIR%Release-DockerImage.bat"
|
||||||
|
|
||||||
|
REM Run the second batch file
|
||||||
|
call "%SCRIPT_DIR%Deploy-Helm.bat"
|
||||||
|
|
||||||
|
echo All scripts completed.
|
||||||
|
pause
|
||||||
@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="8.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -2,43 +2,31 @@
|
|||||||
"ReverseProxy": {
|
"ReverseProxy": {
|
||||||
"Routes": {
|
"Routes": {
|
||||||
"well-known-acme-challenge-route": {
|
"well-known-acme-challenge-route": {
|
||||||
"Match": {
|
"Match": { "Path": "/.well-known/acme-challenge/{**catch-all}" },
|
||||||
"Path": "/.well-known/acme-challenge/{**catch-all}"
|
|
||||||
},
|
|
||||||
"ClusterId": "letsencrypt-server"
|
"ClusterId": "letsencrypt-server"
|
||||||
},
|
},
|
||||||
"swagger-route": {
|
"swagger-route": {
|
||||||
"Match": {
|
"Match": { "Path": "/swagger/{**catch-all}" },
|
||||||
"Path": "/swagger/{**catch-all}"
|
|
||||||
},
|
|
||||||
"ClusterId": "letsencrypt-server"
|
"ClusterId": "letsencrypt-server"
|
||||||
},
|
},
|
||||||
"api-route": {
|
"api-route": {
|
||||||
"Match": {
|
"Match": { "Path": "/api/{**catch-all}" },
|
||||||
"Path": "/api/{**catch-all}"
|
|
||||||
},
|
|
||||||
"ClusterId": "letsencrypt-server"
|
"ClusterId": "letsencrypt-server"
|
||||||
},
|
},
|
||||||
"default-route": {
|
"default-route": {
|
||||||
"Match": {
|
"Match": { "Path": "{**catch-all}" },
|
||||||
"Path": "{**catch-all}"
|
|
||||||
},
|
|
||||||
"ClusterId": "letsencrypt-app"
|
"ClusterId": "letsencrypt-app"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Clusters": {
|
"Clusters": {
|
||||||
"letsencrypt-server": {
|
"letsencrypt-server": {
|
||||||
"Destinations": {
|
"Destinations": {
|
||||||
"destination1": {
|
"d1": { "Address": "http://certs-ui-server:5000/" }
|
||||||
"Address": "http://letsencrypt-server:5000/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"letsencrypt-app": {
|
"letsencrypt-app": {
|
||||||
"Destinations": {
|
"Destinations": {
|
||||||
"destination1": {
|
"d1": { "Address": "http://certs-ui-client:3000/" }
|
||||||
"Address": "http://letsencrypt-app:3000/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
namespace MaksIT.SSHProvider;
|
|
||||||
|
|
||||||
public class Configuration {
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
|
||||||
<PackageReference Include="SSH.NET" Version="2024.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using DomainResults.Common;
|
|
||||||
|
|
||||||
using Renci.SshNet;
|
|
||||||
using Renci.SshNet.Common;
|
|
||||||
|
|
||||||
namespace MaksIT.SSHProvider {
|
|
||||||
|
|
||||||
public interface ISSHService : IDisposable {
|
|
||||||
IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes);
|
|
||||||
|
|
||||||
IDomainResult ListDir(string workingdirectory);
|
|
||||||
|
|
||||||
IDomainResult Download();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SSHService : ISSHService {
|
|
||||||
|
|
||||||
public readonly ILogger _logger;
|
|
||||||
|
|
||||||
public readonly SshClient _sshClient;
|
|
||||||
public readonly SftpClient _sftpClient;
|
|
||||||
|
|
||||||
public SSHService(
|
|
||||||
ILogger logger,
|
|
||||||
string host,
|
|
||||||
int port,
|
|
||||||
string username,
|
|
||||||
string password
|
|
||||||
) {
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space");
|
|
||||||
|
|
||||||
_logger = logger;
|
|
||||||
_sshClient = new SshClient(host, port, username, password);
|
|
||||||
_sftpClient = new SftpClient(host, port, username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public SSHService(
|
|
||||||
ILogger logger,
|
|
||||||
string host,
|
|
||||||
int port,
|
|
||||||
string username,
|
|
||||||
string [] privateKeys
|
|
||||||
) {
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x)))
|
|
||||||
throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space");
|
|
||||||
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
var privateKeyFiles = new List<PrivateKeyFile>();
|
|
||||||
foreach (var privateKey in privateKeys) {
|
|
||||||
using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) {
|
|
||||||
privateKeyFiles.Add(new PrivateKeyFile(ms));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray());
|
|
||||||
_sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDomainResult Connect() {
|
|
||||||
try {
|
|
||||||
_sshClient.Connect();
|
|
||||||
_sftpClient.Connect();
|
|
||||||
|
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
|
||||||
catch (Exception ex){
|
|
||||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
|
||||||
return IDomainResult.CriticalDependencyError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
_sftpClient.ChangeDirectory(workingdirectory);
|
|
||||||
_logger.LogInformation($"Changed directory to {workingdirectory}");
|
|
||||||
|
|
||||||
using var memoryStream = new MemoryStream(bytes);
|
|
||||||
|
|
||||||
_logger.LogInformation($"Uploading {fileName} ({memoryStream.Length:N0} bytes)");
|
|
||||||
|
|
||||||
_sftpClient.BufferSize = 4 * 1024; // bypass Payload error large files
|
|
||||||
_sftpClient.UploadFile(memoryStream, fileName);
|
|
||||||
|
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
|
||||||
return IDomainResult.CriticalDependencyError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDomainResult ListDir(string workingdirectory) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
var listDirectory = _sftpClient.ListDirectory(workingdirectory);
|
|
||||||
|
|
||||||
_logger.LogInformation($"Listing directory:");
|
|
||||||
|
|
||||||
foreach (var file in listDirectory) {
|
|
||||||
_logger.LogInformation($" - " + file.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
|
||||||
return IDomainResult.CriticalDependencyError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDomainResult Download() {
|
|
||||||
return IDomainResult.Failed();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDomainResult RunSudoCommand(string password, string command) {
|
|
||||||
try {
|
|
||||||
command = $"sudo {command}";
|
|
||||||
|
|
||||||
using (var shellStream = _sshClient.CreateShellStream("xterm", 80, 24, 800, 600, 1024, new Dictionary<TerminalModes, uint> {
|
|
||||||
{ TerminalModes.ECHO, 53 }
|
|
||||||
})) {
|
|
||||||
// Get logged in
|
|
||||||
string rep = shellStream.Expect(new Regex(@"[$>]"), TimeSpan.FromSeconds(10)); // expect user prompt with timeout
|
|
||||||
_logger.LogInformation("Initial prompt: {Prompt}", rep);
|
|
||||||
|
|
||||||
// Send command
|
|
||||||
shellStream.WriteLine(command);
|
|
||||||
rep = shellStream.Expect(new Regex(@"([$#>:])"), TimeSpan.FromSeconds(10)); // expect password or user prompt with timeout
|
|
||||||
_logger.LogInformation("After command prompt: {Prompt}", rep);
|
|
||||||
|
|
||||||
// Check to send password
|
|
||||||
if (rep.Contains(":")) {
|
|
||||||
// Send password
|
|
||||||
shellStream.WriteLine(password);
|
|
||||||
rep = shellStream.Expect(new Regex(@"[$#>]"), TimeSpan.FromSeconds(10)); // expect user or root prompt with timeout
|
|
||||||
_logger.LogInformation("After password prompt: {Prompt}", rep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IDomainResult.Success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
_logger.LogError(ex, "SSH Service unhandled exception");
|
|
||||||
return IDomainResult.CriticalDependencyError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
_sshClient.Disconnect();
|
|
||||||
_sshClient.Dispose();
|
|
||||||
|
|
||||||
_sftpClient.Disconnect();
|
|
||||||
_sftpClient.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
//using PecMgr.VaultProvider.Extensions;
|
|
||||||
//using PecMgr.VaultProvider;
|
|
||||||
//using PecMgr.Core.Abstractions;
|
|
||||||
|
|
||||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
|
|
||||||
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
|
||||||
public abstract class ConfigurationBase {
|
|
||||||
|
|
||||||
protected IConfiguration Configuration;
|
|
||||||
|
|
||||||
protected ServiceCollection ServiceCollection = new ServiceCollection();
|
|
||||||
|
|
||||||
protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); }
|
|
||||||
|
|
||||||
public ConfigurationBase() {
|
|
||||||
Configuration = InitConfig();
|
|
||||||
ConfigureServices(ServiceCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void ConfigureServices(IServiceCollection services);
|
|
||||||
|
|
||||||
private IConfiguration InitConfig() {
|
|
||||||
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
|
||||||
var currentDirectory = Directory.GetCurrentDirectory();
|
|
||||||
|
|
||||||
var configurationBuilder = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddEnvironmentVariables();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
|
|
||||||
configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
|
|
||||||
else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
|
|
||||||
configurationBuilder.AddJsonFile("appsettings.json", true, true);
|
|
||||||
else
|
|
||||||
throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
|
|
||||||
|
|
||||||
//var builtConfig = configurationBuilder.Build();
|
|
||||||
//var vaultOptions = builtConfig.GetSection("Vault");
|
|
||||||
|
|
||||||
//configurationBuilder.AddVault(options => {
|
|
||||||
// options.Address = vaultOptions["Address"];
|
|
||||||
|
|
||||||
// options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get<List<string>>();
|
|
||||||
|
|
||||||
// options.AuthMethod = EnumerationStringId.FromValue<AuthenticationMethod>(vaultOptions["AuthMethod"]);
|
|
||||||
// options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get<AppRoleAuthMethod>();
|
|
||||||
// options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get<TokenAuthMethod>();
|
|
||||||
|
|
||||||
// options.MountPath = vaultOptions["MountPath"];
|
|
||||||
// options.SecretType = vaultOptions["SecretType"];
|
|
||||||
|
|
||||||
// options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get<Dictionary<string, string>>();
|
|
||||||
//});
|
|
||||||
|
|
||||||
return configurationBuilder.Build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using MaksIT.SSHProvider;
|
|
||||||
|
|
||||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
|
|
||||||
public abstract class ServicesBase : ConfigurationBase {
|
|
||||||
|
|
||||||
public ServicesBase() : base() { }
|
|
||||||
|
|
||||||
protected override void ConfigureServices(IServiceCollection services) {
|
|
||||||
// configure strongly typed settings objects
|
|
||||||
var appSettingsSection = Configuration.GetSection("Configuration");
|
|
||||||
services.Configure<Configuration>(appSettingsSection);
|
|
||||||
var appSettings = appSettingsSection.Get<Configuration>();
|
|
||||||
|
|
||||||
#region configurazione logging
|
|
||||||
services.AddLogging(configure => {
|
|
||||||
configure.AddSerilog(new LoggerConfiguration()
|
|
||||||
//.ReadFrom.Configuration(_configuration)
|
|
||||||
.CreateLogger());
|
|
||||||
});
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.8.1" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
|
||||||
|
|
||||||
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
|
|
||||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\SSHProvider\SSHProvider.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="appsettings.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using MaksIT.SSHProvider;
|
|
||||||
|
|
||||||
using MaksIT.Tests.SSHProviderTests.Abstractions;
|
|
||||||
|
|
||||||
namespace MaksIT.SSHSerivceTests;
|
|
||||||
public class UnitTest1 : ServicesBase {
|
|
||||||
|
|
||||||
public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UploadFile() {
|
|
||||||
|
|
||||||
var username = "";
|
|
||||||
var password = "";
|
|
||||||
var filePath = Path.Combine(_appPath, "randomfile.txt");
|
|
||||||
CreateRandomFile(filePath, 1);
|
|
||||||
|
|
||||||
var logger = ServiceProvider.GetService<ILogger<SSHService>>();
|
|
||||||
|
|
||||||
using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
|
|
||||||
sshService.Connect();
|
|
||||||
|
|
||||||
var bytes = File.ReadAllBytes(filePath);
|
|
||||||
|
|
||||||
logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)");
|
|
||||||
|
|
||||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
|
||||||
sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R");
|
|
||||||
|
|
||||||
sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes);
|
|
||||||
|
|
||||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
|
||||||
sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateRandomFile(string filePath, int sizeInMb) {
|
|
||||||
// Note: block size must be a factor of 1MB to avoid rounding errors
|
|
||||||
const int blockSize = 1024 * 8;
|
|
||||||
const int blocksPerMb = (1024 * 1024) / blockSize;
|
|
||||||
|
|
||||||
byte[] data = new byte[blockSize];
|
|
||||||
|
|
||||||
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
|
|
||||||
using (FileStream stream = File.OpenWrite(filePath)) {
|
|
||||||
for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
|
|
||||||
crypto.GetBytes(data);
|
|
||||||
stream.Write(data, 0, data.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
global using Xunit;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"Serilog": {
|
|
||||||
"Using": [ "Serilog.Settings.Configuration", "Serilog.Expressions", "Serilog.Sinks.Console" ],
|
|
||||||
"MinimumLevel": "Verbose",
|
|
||||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
|
|
||||||
"WriteTo": [
|
|
||||||
{
|
|
||||||
"Name": "Console",
|
|
||||||
"Args": {
|
|
||||||
"restrictedToMinimumLevel": "Verbose",
|
|
||||||
//"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
|
|
||||||
|
|
||||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
|
|
||||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Configuration": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,8 +14,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
- LETSENCRYPT_SERVER=http://localhost:8080
|
- LETSENCRYPT_SERVER=http://localhost:8080
|
||||||
# ports:
|
|
||||||
# - "3000:3000"
|
|
||||||
networks:
|
networks:
|
||||||
- maks-it
|
- maks-it
|
||||||
|
|
||||||
@ -31,10 +29,8 @@ services:
|
|||||||
- MAKS-IT_AGENT_KEY=UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=
|
- MAKS-IT_AGENT_KEY=UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=
|
||||||
- MAKS-IT_AGENT_SERVICE=haproxy
|
- MAKS-IT_AGENT_SERVICE=haproxy
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker-compose/LetsEncryptServer/acme:/app/bin/Debug/net8.0/acme
|
- D:\Compose\MaksIT.CertsUI\acme:/app/bin/Debug/net8.0/acme
|
||||||
- ./docker-compose/LetsEncryptServer/cache:/app/bin/Debug/net8.0/cache
|
- D:\Compose\MaksIT.CertsUI\cache:/app/bin/Debug/net8.0/cache
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
networks:
|
networks:
|
||||||
- maks-it
|
- maks-it
|
||||||
|
|
||||||
|
|||||||
6
src/helm/Chart.yaml
Normal file
6
src/helm/Chart.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: certs-ui
|
||||||
|
description: MaksIT CertsUI
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "latest"
|
||||||
35
src/helm/templates/_helpers.tpl
Normal file
35
src/helm/templates/_helpers.tpl
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{{- define "certs-ui.name" -}}
|
||||||
|
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "certs-ui.fullname" -}}
|
||||||
|
{{- $name := .Chart.Name -}}
|
||||||
|
{{- $rel := .Release.Name -}}
|
||||||
|
{{- if or (hasPrefix (printf "%s-" $name) $rel) (eq $rel $name) -}}
|
||||||
|
{{- $rel | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- printf "%s-%s" $rel $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "certs-ui.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "certs-ui.labels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "certs-ui.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
helm.sh/chart: {{ include "certs-ui.chart" . }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- /* Image pull secrets (global) -> list of names) */ -}}
|
||||||
|
{{- define "certs-ui.imagePullSecrets" -}}
|
||||||
|
{{- $ips := default (list) .Values.global.imagePullSecrets -}}
|
||||||
|
{{- if $ips }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- range $ips }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
83
src/helm/templates/deployments.yaml
Normal file
83
src/helm/templates/deployments.yaml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{{- $root := . -}}
|
||||||
|
{{- range $compName, $comp := .Values.components }}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
|
||||||
|
labels:
|
||||||
|
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ default 1 $comp.replicas }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||||
|
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "certs-ui.labels" $root | nindent 8 }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
{{- if and $comp.secretsFile $comp.secretsFile.forceUpdate }}
|
||||||
|
annotations:
|
||||||
|
"checksum/secrets-file": {{ (default "" $comp.secretsFile.content) | toString | sha256sum | quote }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- include "certs-ui.imagePullSecrets" $root | nindent 6 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ $compName }}
|
||||||
|
image: "{{ $comp.image.registry }}/{{ $comp.image.repository }}:{{ $comp.image.tag }}"
|
||||||
|
imagePullPolicy: {{ default "IfNotPresent" $comp.image.pullPolicy }}
|
||||||
|
{{ $svc := default dict $comp.service }}
|
||||||
|
{{ $tgt := default 8080 $svc.targetPort }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ $tgt }}
|
||||||
|
{{- if $comp.env }}
|
||||||
|
env:
|
||||||
|
{{- range $comp.env }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $p := default dict $comp.persistence -}}
|
||||||
|
{{- $vols := default (list) $p.volumes -}}
|
||||||
|
{{- $hasVols := gt (len $vols) 0 -}}
|
||||||
|
{{- $hasSecret := (hasKey $comp "secretsFile") -}}
|
||||||
|
{{- if or $hasVols $hasSecret }}
|
||||||
|
volumeMounts:
|
||||||
|
{{- range $vol := $vols }}
|
||||||
|
- name: {{ $compName }}-{{ $vol.name }}
|
||||||
|
mountPath: {{ $vol.mountPath }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $comp.secretsFile }}
|
||||||
|
- name: {{ $compName }}-secrets
|
||||||
|
mountPath: {{ $comp.secretsFile.mountPath }}
|
||||||
|
subPath: {{ base $comp.secretsFile.mountPath }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or $hasVols $hasSecret }}
|
||||||
|
volumes:
|
||||||
|
{{- range $vol := $vols }}
|
||||||
|
- name: {{ $compName }}-{{ $vol.name }}
|
||||||
|
{{- if eq $vol.type "pvc" }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-{{ $vol.name }}
|
||||||
|
{{- else if eq $vol.type "emptyDir" }}
|
||||||
|
emptyDir: {{ toYaml (default dict $vol.emptyDir) | nindent 12 }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $comp.secretsFile }}
|
||||||
|
- name: {{ $compName }}-secrets
|
||||||
|
secret:
|
||||||
|
secretName: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-secrets
|
||||||
|
items:
|
||||||
|
- key: {{ $comp.secretsFile.key }}
|
||||||
|
path: {{ base $comp.secretsFile.mountPath }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
23
src/helm/templates/pvc.yaml
Normal file
23
src/helm/templates/pvc.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{{- $root := . -}}
|
||||||
|
{{- range $compName, $comp := .Values.components }}
|
||||||
|
{{- $p := default dict $comp.persistence }}
|
||||||
|
{{- $vols := default (list) $p.volumes }}
|
||||||
|
{{- range $vol := $vols }}
|
||||||
|
{{- if and (eq $vol.type "pvc") $vol.pvc.create }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-{{ $vol.name }}
|
||||||
|
labels:
|
||||||
|
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
spec:
|
||||||
|
accessModes: {{ toYaml (default (list "ReadWriteOnce") $vol.pvc.accessModes) | nindent 2 }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ default "1Gi" $vol.pvc.size }}
|
||||||
|
storageClassName: {{ default "local-path" $vol.pvc.storageClass }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
28
src/helm/templates/secret-appsecrets.yaml
Normal file
28
src/helm/templates/secret-appsecrets.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{{- $root := . -}}
|
||||||
|
{{- range $compName, $comp := .Values.components }}
|
||||||
|
{{- if $comp.secretsFile }}
|
||||||
|
{{- $sf := $comp.secretsFile -}}
|
||||||
|
{{- $secretName := printf "%s-%s-secrets" (include "certs-ui.fullname" $root) $compName -}}
|
||||||
|
{{- $existing := lookup "v1" "Secret" $root.Release.Namespace $secretName -}}
|
||||||
|
{{- if and $sf.keep $existing }}
|
||||||
|
{{/* keep=true and Secret exists -> render nothing */}}
|
||||||
|
{{- else }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ $secretName }}
|
||||||
|
labels:
|
||||||
|
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
{{- if $sf.keep }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/resource-policy": keep
|
||||||
|
{{- end }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
{{ $sf.key }}: |
|
||||||
|
{{ $sf.content | indent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
73
src/helm/templates/services.yaml
Normal file
73
src/helm/templates/services.yaml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{{- $root := . -}}
|
||||||
|
{{- range $compName, $comp := .Values.components }}
|
||||||
|
{{- $svc := default dict $comp.service }}
|
||||||
|
{{- if and $svc $svc.enabled }}
|
||||||
|
{{- $stype := default "ClusterIP" $svc.type }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
|
||||||
|
labels:
|
||||||
|
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
{{- if $svc.labels }}
|
||||||
|
{{ toYaml $svc.labels | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $svc.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{ toYaml $svc.annotations | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
type: {{ $stype }}
|
||||||
|
{{- if $svc.clusterIP }}
|
||||||
|
clusterIP: {{ $svc.clusterIP }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if $svc.loadBalancerClass }}
|
||||||
|
loadBalancerClass: {{ $svc.loadBalancerClass }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (or (eq $stype "LoadBalancer") (eq $stype "NodePort")) ($svc.allocateLoadBalancerNodePorts | default nil) }}
|
||||||
|
allocateLoadBalancerNodePorts: {{ $svc.allocateLoadBalancerNodePorts }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerSourceRanges }}
|
||||||
|
loadBalancerSourceRanges:
|
||||||
|
{{ toYaml $svc.loadBalancerSourceRanges | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilies }}
|
||||||
|
ipFamilies:
|
||||||
|
{{ toYaml $svc.ipFamilies | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilyPolicy }}
|
||||||
|
ipFamilyPolicy: {{ $svc.ipFamilyPolicy }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerIP }}
|
||||||
|
loadBalancerIP: {{ $svc.loadBalancerIP }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ default 80 $svc.port }}
|
||||||
|
targetPort: {{ default 80 $svc.targetPort }}
|
||||||
|
{{- if eq $stype "NodePort" }}
|
||||||
|
{{- if $svc.nodePort }}
|
||||||
|
nodePort: {{ $svc.nodePort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||||
|
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
|
||||||
|
app.kubernetes.io/component: {{ $compName }}
|
||||||
|
{{- if and (ne $stype "ClusterIP") $svc.externalTrafficPolicy }}
|
||||||
|
externalTrafficPolicy: {{ $svc.externalTrafficPolicy }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (eq $stype "LoadBalancer") $svc.healthCheckNodePort }}
|
||||||
|
healthCheckNodePort: {{ $svc.healthCheckNodePort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and (typeIs "string" $svc.sessionAffinity) $svc.sessionAffinity }}
|
||||||
|
sessionAffinity: {{ $svc.sessionAffinity }}
|
||||||
|
{{- if and (eq $svc.sessionAffinity "ClientIP") (typeIs "map" $svc.sessionAffinityConfig) }}
|
||||||
|
sessionAffinityConfig:
|
||||||
|
{{ toYaml $svc.sessionAffinityConfig | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
96
src/helm/values.yaml
Normal file
96
src/helm/values.yaml
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
global:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: cr-maksit-pull
|
||||||
|
|
||||||
|
components:
|
||||||
|
server:
|
||||||
|
replicas: 1
|
||||||
|
image:
|
||||||
|
registry: cr.maks-it.com
|
||||||
|
repository: certs-ui/server
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: ASPNETCORE_ENVIRONMENT
|
||||||
|
value: Development
|
||||||
|
- name: ASPNETCORE_HTTP_PORTS
|
||||||
|
value: "5000"
|
||||||
|
service:
|
||||||
|
enabled: true
|
||||||
|
type: ClusterIP
|
||||||
|
port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
persistence:
|
||||||
|
volumes:
|
||||||
|
- name: acme
|
||||||
|
mountPath: /acme
|
||||||
|
type: pvc
|
||||||
|
pvc:
|
||||||
|
create: true
|
||||||
|
storageClass: local-path
|
||||||
|
size: 50Mi
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
|
- name: cache
|
||||||
|
mountPath: /cache
|
||||||
|
type: pvc
|
||||||
|
pvc:
|
||||||
|
create: true
|
||||||
|
storageClass: local-path
|
||||||
|
size: 50Mi
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
|
secretsFile:
|
||||||
|
key: appsecrets.json
|
||||||
|
mountPath: /secrets/appsecrets.json
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"Agent": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keep: true
|
||||||
|
forceUpdate: false
|
||||||
|
|
||||||
|
client:
|
||||||
|
replicas: 1
|
||||||
|
image:
|
||||||
|
registry: cr.maks-it.com
|
||||||
|
repository: certs-ui/client
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: ASPNETCORE_ENVIRONMENT
|
||||||
|
value: Development
|
||||||
|
- name: NEXT_PUBLIC_API_BASE_URL
|
||||||
|
value: http://certs-ui-server:5000
|
||||||
|
service:
|
||||||
|
enabled: true
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
|
||||||
|
reverseproxy:
|
||||||
|
replicas: 1
|
||||||
|
image:
|
||||||
|
registry: cr.maks-it.com
|
||||||
|
repository: certs-ui/reverseproxy
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: ASPNETCORE_ENVIRONMENT
|
||||||
|
value: Development
|
||||||
|
- name: ASPNETCORE_HTTP_PORTS
|
||||||
|
value: "8080"
|
||||||
|
service:
|
||||||
|
enabled: true
|
||||||
|
type: LoadBalancer
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
loadBalancerIP: "172.16.0.5"
|
||||||
|
annotations:
|
||||||
|
lbipam.cilium.io/ips: "172.16.0.5"
|
||||||
|
labels:
|
||||||
|
export: "bgp"
|
||||||
|
externalTrafficPolicy: Local
|
||||||
|
sessionAffinity: ClientIP
|
||||||
|
sessionAffinityConfig:
|
||||||
|
clientIP:
|
||||||
|
timeoutSeconds: 10800
|
||||||
Loading…
Reference in New Issue
Block a user