diff --git a/src/Agent/Agent.csproj b/src/Agent/Agent.csproj
index cd7c57d..8867ed6 100644
--- a/src/Agent/Agent.csproj
+++ b/src/Agent/Agent.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/ClientApp/ApiRoutes.tsx b/src/ClientApp/ApiRoutes.tsx
index 6995cdc..ee111db 100644
--- a/src/ClientApp/ApiRoutes.tsx
+++ b/src/ClientApp/ApiRoutes.tsx
@@ -20,14 +20,14 @@ enum ApiRoutes {
// 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 => {
let result: string = route
args.forEach((arg) => {
result = result.replace(/{.*?}/, arg)
})
- // TODO: need env var
- return `http://localhost:8080/${result}`
- //return `http://websrv0001.corp.maks-it.com:8080/${result}`
+ return `${apiBase.replace(/\/+$/, '')}/${result.replace(/^\/+/, '')}`
}
export { GetApiRoute, ApiRoutes }
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 3d31787..27d9738 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -14,7 +14,8 @@
-
+
+
diff --git a/src/Core/LockManager.cs b/src/Core/LockManager.cs
index a5f6119..44ed543 100644
--- a/src/Core/LockManager.cs
+++ b/src/Core/LockManager.cs
@@ -1,105 +1,93 @@
using System;
using System.Collections.Concurrent;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.RateLimiting;
public class LockManager : IDisposable {
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly ConcurrentDictionary _reentrantCounts = new ConcurrentDictionary();
+ 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 ExecuteWithLockAsync(Func> action) {
+ var lease = await _rateLimiter.AcquireAsync(1);
+ if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
+
var threadId = Thread.CurrentThread.ManagedThreadId;
-
- if (!_reentrantCounts.ContainsKey(threadId)) {
- _reentrantCounts[threadId] = 0;
- }
-
- if (_reentrantCounts[threadId] == 0) {
- await _semaphore.WaitAsync();
- }
-
+ if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
+ if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
_reentrantCounts[threadId]++;
try {
return await action();
}
finally {
_reentrantCounts[threadId]--;
- if (_reentrantCounts[threadId] == 0) {
- _semaphore.Release();
- }
+ if (_reentrantCounts[threadId] == 0) _semaphore.Release();
+ lease.Dispose();
}
}
public async Task ExecuteWithLockAsync(Func action) {
+ var lease = await _rateLimiter.AcquireAsync(1);
+ if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
+
var threadId = Thread.CurrentThread.ManagedThreadId;
-
- if (!_reentrantCounts.ContainsKey(threadId)) {
- _reentrantCounts[threadId] = 0;
- }
-
- if (_reentrantCounts[threadId] == 0) {
- await _semaphore.WaitAsync();
- }
-
+ if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
+ if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
_reentrantCounts[threadId]++;
try {
await action();
}
finally {
_reentrantCounts[threadId]--;
- if (_reentrantCounts[threadId] == 0) {
- _semaphore.Release();
- }
+ if (_reentrantCounts[threadId] == 0) _semaphore.Release();
+ lease.Dispose();
}
}
public async Task ExecuteWithLockAsync(Func action) {
+ var lease = await _rateLimiter.AcquireAsync(1);
+ if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
+
var threadId = Thread.CurrentThread.ManagedThreadId;
-
- if (!_reentrantCounts.ContainsKey(threadId)) {
- _reentrantCounts[threadId] = 0;
- }
-
- if (_reentrantCounts[threadId] == 0) {
- await _semaphore.WaitAsync();
- }
-
+ if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
+ if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
_reentrantCounts[threadId]++;
try {
return await Task.Run(action);
}
finally {
_reentrantCounts[threadId]--;
- if (_reentrantCounts[threadId] == 0) {
- _semaphore.Release();
- }
+ if (_reentrantCounts[threadId] == 0) _semaphore.Release();
+ lease.Dispose();
}
}
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;
-
- if (!_reentrantCounts.ContainsKey(threadId)) {
- _reentrantCounts[threadId] = 0;
- }
-
- if (_reentrantCounts[threadId] == 0) {
- await _semaphore.WaitAsync();
- }
-
+ if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
+ if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
_reentrantCounts[threadId]++;
try {
await Task.Run(action);
}
finally {
_reentrantCounts[threadId]--;
- if (_reentrantCounts[threadId] == 0) {
- _semaphore.Release();
- }
+ if (_reentrantCounts[threadId] == 0) _semaphore.Release();
+ lease.Dispose();
}
}
public void Dispose() {
_semaphore.Dispose();
+ _rateLimiter.Dispose();
}
}
diff --git a/src/Deploy-Helm.bat b/src/Deploy-Helm.bat
new file mode 100644
index 0000000..40d54a5
--- /dev/null
+++ b/src/Deploy-Helm.bat
@@ -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"
diff --git a/src/Deploy-Helm.ps1 b/src/Deploy-Helm.ps1
new file mode 100644
index 0000000..18f98b0
--- /dev/null
+++ b/src/Deploy-Helm.ps1
@@ -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
diff --git a/src/LetsEncrypt.sln b/src/LetsEncrypt.sln
index 8db719a..1194f3f 100644
--- a/src/LetsEncrypt.sln
+++ b/src/LetsEncrypt.sln
@@ -5,16 +5,10 @@ VisualStudioVersion = 17.6.33815.320
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
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}"
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}"
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}"
EndProject
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}.Release|Any CPU.ActiveCfg = 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.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.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.Build.0 = Debug|Any CPU
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -75,9 +57,6 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4} = {3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}
- EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
EndGlobalSection
diff --git a/src/LetsEncrypt/Entities/ContentType.cs b/src/LetsEncrypt/Entities/ContentType.cs
new file mode 100644
index 0000000..c107033
--- /dev/null
+++ b/src/LetsEncrypt/Entities/ContentType.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/LetsEncrypt/Entities/OrderStatus.cs b/src/LetsEncrypt/Entities/OrderStatus.cs
new file mode 100644
index 0000000..438164b
--- /dev/null
+++ b/src/LetsEncrypt/Entities/OrderStatus.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/LetsEncrypt/LetsEncrypt.csproj b/src/LetsEncrypt/LetsEncrypt.csproj
index c4a3748..194f00c 100644
--- a/src/LetsEncrypt/LetsEncrypt.csproj
+++ b/src/LetsEncrypt/LetsEncrypt.csproj
@@ -8,12 +8,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs
index ce32b36..1960e7a 100644
--- a/src/LetsEncrypt/Services/LetsEncryptService.cs
+++ b/src/LetsEncrypt/Services/LetsEncryptService.cs
@@ -22,7 +22,6 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
-using Microsoft.Extensions.Options;
namespace MaksIT.LetsEncrypt.Services;
@@ -39,6 +38,10 @@ public interface ILetsEncryptService {
}
public class LetsEncryptService : ILetsEncryptService {
+ private const string DnsType = "dns";
+ private const string DirectoryEndpoint = "directory";
+ private const string ReplayNonceHeader = "Replay-Nonce";
+
private readonly ILogger _logger;
private readonly LetsEncryptConfiguration _appSettings;
private readonly HttpClient _httpClient;
@@ -57,40 +60,61 @@ public class LetsEncryptService : ILetsEncryptService {
}
private State GetOrCreateState(Guid sessionId) {
- if (!_memoryCache.TryGetValue(sessionId, out State state)) {
+ if (!_memoryCache.TryGetValue(sessionId, out State? state) || state == null) {
state = new State();
_memoryCache.Set(sessionId, state, TimeSpan.FromHours(1));
}
return state;
}
+ // Helper: Send ACME request and process response
+ private async Task> SendAcmeRequest(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(response, responseText);
+ }
+
+ // Helper: Poll challenge status until valid or timeout
+ private async Task 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(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
public async Task ConfigureClient(Guid sessionId, bool isStaging) {
try {
var state = GetOrCreateState(sessionId);
-
state.IsStaging = isStaging;
- // TODO: need to propagate from Configuration
_httpClient.BaseAddress ??= new Uri(isStaging ? _appSettings.Staging : _appSettings.Production);
-
if (state.Directory == null) {
- var request = new HttpRequestMessage(HttpMethod.Get, new Uri("directory", UriKind.Relative));
- await HandleNonceAsync(sessionId, new Uri("directory", UriKind.Relative), state);
-
- var response = await _httpClient.SendAsync(request);
- UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Get);
-
- var responseText = await response.Content.ReadAsStringAsync();
- HandleProblemResponseAsync(response, responseText);
-
- var directory = ProcessResponseContent(response, responseText);
-
- state.Directory = directory.Result;
+ var request = new HttpRequestMessage(HttpMethod.Get, new Uri(DirectoryEndpoint, UriKind.Relative));
+ await HandleNonceAsync(sessionId, new Uri(DirectoryEndpoint, UriKind.Relative), state);
+ var directory = await SendAcmeRequest(request, state, HttpMethod.Get);
+ state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null");
}
-
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
const string message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
@@ -104,64 +128,42 @@ public class LetsEncryptService : ILetsEncryptService {
_logger.LogError("Invalid sessionId");
return IDomainResult.Failed();
}
-
if (contacts == null || contacts.Length == 0) {
_logger.LogError("Contacts are null or empty");
return IDomainResult.Failed();
}
-
var state = GetOrCreateState(sessionId);
-
if (state.Directory == null) {
_logger.LogError("State directory is null");
return IDomainResult.Failed();
}
-
_logger.LogInformation($"Executing {nameof(Init)}...");
-
try {
var accountKey = new RSACryptoServiceProvider(4096);
-
if (cache != null && cache.AccountKey != null) {
state.Cache = cache;
accountKey.ImportCspBlob(cache.AccountKey);
state.JwsService = new JwsService(accountKey);
- state.JwsService.SetKeyId(cache.Location.ToString());
- }
- else {
+ state.JwsService.SetKeyId(cache.Location?.ToString() ?? string.Empty);
+ } else {
state.JwsService = new JwsService(accountKey);
-
var letsEncryptOrder = new Account {
TermsOfServiceAgreed = true,
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
};
-
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewAccount);
-
-
await HandleNonceAsync(sessionId, state.Directory.NewAccount, state);
-
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
Url = state.Directory.NewAccount,
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 result = ProcessResponseContent(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}");
+ var result = await SendAcmeRequest(request, state, HttpMethod.Post);
+ state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty);
+ if (result.Result?.Status != "valid") {
+ _logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}");
return IDomainResult.Failed();
}
-
state.Cache = new RegistrationCache {
AccountId = accountId,
Description = description,
@@ -169,14 +171,12 @@ public class LetsEncryptService : ILetsEncryptService {
IsStaging = state.IsStaging,
Location = result.Result.Location,
AccountKey = accountKey.ExportCspBlob(true),
- Id = result.Result.Id,
+ Id = result.Result.Id ?? string.Empty,
Key = result.Result.Key
};
}
-
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
const string message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
@@ -186,10 +186,8 @@ public class LetsEncryptService : ILetsEncryptService {
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
var state = GetOrCreateState(sessionId);
-
if(state?.Cache == null)
return IDomainResult.Failed();
-
return IDomainResult.Success(state.Cache);
}
@@ -197,18 +195,13 @@ public class LetsEncryptService : ILetsEncryptService {
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
-
- if (state.Directory == null) {
+ if (state.Directory?.Meta?.TermsOfService == null) {
return IDomainResult.Failed();
}
-
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
@@ -219,107 +212,76 @@ public class LetsEncryptService : ILetsEncryptService {
public async Task<(Dictionary?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
-
state.Challenges.Clear();
-
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
- Identifiers = hostnames.Select(hostname => new OrderIdentifier {
- Type = "dns",
- Value = hostname
- }).ToArray()
+ Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
+ Type = DnsType,
+ Value = hostname ?? string.Empty
+ }).ToArray() ?? Array.Empty()
};
-
+ if (state.Directory == null || state.Directory.NewOrder == null)
+ return (null, IDomainResult.Failed());
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
-
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
Url = state.Directory.NewOrder,
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 order = ProcessResponseContent(response, responseText);
-
- if (order.Result.Status == "ready")
- return IDomainResult.Success(new Dictionary());
-
- 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?>();
+ var order = await SendAcmeRequest(request, state, HttpMethod.Post);
+ if (StatusEquals(order.Result?.Status, OrderStatus.Ready))
+ return (new Dictionary(), IDomainResult.Success());
+ if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) {
+ _logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}");
+ return (null, IDomainResult.Failed());
}
-
state.CurrentOrder = order.Result;
-
var results = new Dictionary();
- foreach (var item in state.CurrentOrder.Authorizations) {
-
+ foreach (var item in state.CurrentOrder?.Authorizations ?? Array.Empty()) {
+ if (item == null) continue;
request = new HttpRequestMessage(HttpMethod.Post, item);
await HandleNonceAsync(sessionId, item, state);
-
json = EncodeMessage(true, null, state, new JwsHeader {
Url = item,
Nonce = state.Nonce
});
PrepareRequestContent(request, json, HttpMethod.Post);
-
-
-
- response = await _httpClient.SendAsync(request);
- UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
-
- responseText = await response.Content.ReadAsStringAsync();
- HandleProblemResponseAsync(response, responseText);
-
- var challengeResponse = ProcessResponseContent(response, responseText);
-
-
- if (challengeResponse.Result.Status == "valid")
+ var challengeResponse = await SendAcmeRequest(request, state, HttpMethod.Post);
+ if (StatusEquals(challengeResponse.Result?.Status, OrderStatus.Valid))
continue;
-
- if (challengeResponse.Result.Status != "pending") {
- _logger.LogError($"Expected authorization status 'pending', but got: {state.CurrentOrder.Status} \r\n {challengeResponse.ResponseText}");
- return IDomainResult.Failed?>();
+ if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) {
+ _logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}");
+ return (null, IDomainResult.Failed());
+ }
+ 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.Cache.ChallengeType = challengeType;
-
- var keyToken = state.JwsService.GetKeyAuthorization(challenge.Token);
-
+ if (state.Cache != null) state.Cache.ChallengeType = challengeType;
+ var keyToken = state.JwsService != null ? state.JwsService.GetKeyAuthorization(challenge.Token) : string.Empty;
switch (challengeType) {
case "dns-01":
using (var sha256 = SHA256.Create()) {
- var dnsToken = state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
- results[challengeResponse.Result.Identifier.Value] = dnsToken;
+ var dnsToken = state.JwsService != null ? state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken ?? string.Empty))) : string.Empty;
+ results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = dnsToken;
}
break;
-
case "http-01":
- results[challengeResponse.Result.Identifier.Value] = keyToken;
+ results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = keyToken ?? string.Empty;
break;
-
default:
throw new NotImplementedException();
}
}
-
- return IDomainResult.Success(results);
- }
- catch (Exception ex) {
+ return (results, IDomainResult.Success());
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
- return IDomainResult.CriticalDependencyError?>(message);
+ return (null, IDomainResult.CriticalDependencyError(message));
}
}
#endregion
@@ -328,68 +290,31 @@ public class LetsEncryptService : ILetsEncryptService {
public async Task CompleteChallenges(Guid sessionId) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
-
if (state.CurrentOrder?.Identifiers == null) {
return IDomainResult.Failed("Current order identifiers are null");
}
-
for (var index = 0; index < state.Challenges.Count; index++) {
var challenge = state.Challenges[index];
- var start = DateTime.UtcNow;
-
- while (true) {
- 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(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");
+ if (challenge?.Url == null) {
+ _logger.LogError("Challenge URL is null");
+ return IDomainResult.Failed();
}
+ 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(request, state, HttpMethod.Post);
+ var result = await PollChallengeStatus(sessionId, challenge, state);
+ if (!result.IsSuccess)
+ return result;
}
-
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
@@ -400,40 +325,26 @@ public class LetsEncryptService : ILetsEncryptService {
public async Task GetOrder(Guid sessionId, string[] hostnames) {
try {
_logger.LogInformation($"Executing {nameof(GetOrder)}");
-
var state = GetOrCreateState(sessionId);
-
var letsEncryptOrder = new Order {
Expires = DateTime.UtcNow.AddDays(2),
- Identifiers = hostnames.Select(hostname => new OrderIdentifier {
+ Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
Type = "dns",
- Value = hostname
- }).ToArray()
+ Value = hostname!
+ }).ToArray() ?? Array.Empty()
};
-
- 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);
-
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
Url = state.Directory.NewOrder,
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 order = ProcessResponseContent(response, responseText);
+ var order = await SendAcmeRequest(request, state, HttpMethod.Post);
state.CurrentOrder = order.Result;
-
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
@@ -444,136 +355,89 @@ public class LetsEncryptService : ILetsEncryptService {
public async Task GetCertificate(Guid sessionId, string subject) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
-
- if (state.CurrentOrder == null) {
+ if (state.CurrentOrder?.Identifiers == null) {
return IDomainResult.Failed();
}
-
var key = new RSACryptoServiceProvider(4096);
var csr = new CertificateRequest("CN=" + subject,
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
-
var san = new SubjectAlternativeNameBuilder();
- foreach (var host in state.CurrentOrder.Identifiers)
- san.AddDnsName(host.Value);
-
+ foreach (var host in state.CurrentOrder.Identifiers) {
+ if (host?.Value != null)
+ san.AddDnsName(host.Value);
+ }
csr.CertificateExtensions.Add(san.Build());
-
var letsEncryptOrder = new FinalizeRequest {
- Csr = state.JwsService.Base64UrlEncoded(csr.CreateSigningRequest())
+ Csr = state.JwsService!.Base64UrlEncoded(csr.CreateSigningRequest())
};
-
Uri? certificateUrl = default;
-
var start = DateTime.UtcNow;
-
while (certificateUrl == null) {
- // https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
- await GetOrder(sessionId, state.CurrentOrder.Identifiers.Select(x => x.Value).ToArray());
-
- if (state.CurrentOrder.Status == "ready") {
-
-
- var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize);
- await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize, state);
-
+ var hostnames = state.CurrentOrder.Identifiers?.Select(x => x?.Value).Where(x => x != null).Cast().ToArray() ?? Array.Empty();
+ await GetOrder(sessionId, hostnames);
+ var status = state.CurrentOrder?.Status;
+ if (StatusEquals(status, OrderStatus.Ready)) {
+ var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize!);
+ await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize!, state);
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
Url = state.CurrentOrder.Finalize,
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 order = ProcessResponseContent(response, responseText);
-
-
-
- if (order.Result.Status == "processing") {
-
- request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location);
- await HandleNonceAsync(sessionId, state.CurrentOrder.Location, state);
-
+ var order = await SendAcmeRequest(request, state, HttpMethod.Post);
+ if (StatusEquals(order.Result?.Status, OrderStatus.Processing)) {
+ request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location!);
+ await HandleNonceAsync(sessionId, state.CurrentOrder.Location!, state);
json = EncodeMessage(true, null, state, new JwsHeader {
Url = state.CurrentOrder.Location,
Nonce = state.Nonce
});
PrepareRequestContent(request, json, HttpMethod.Post);
-
- response = await _httpClient.SendAsync(request);
- UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
-
- responseText = await response.Content.ReadAsStringAsync();
- HandleProblemResponseAsync(response, responseText);
-
- order = ProcessResponseContent(response, responseText);
+ order = await SendAcmeRequest(request, state, HttpMethod.Post);
}
-
- if (order.Result.Status == "valid") {
+ if (StatusEquals(order.Result?.Status, OrderStatus.Valid)) {
certificateUrl = order.Result.Certificate;
}
+ } else if (StatusEquals(status, OrderStatus.Valid)) {
+ certificateUrl = state.CurrentOrder.Certificate;
+ break;
}
-
if ((DateTime.UtcNow - start).Seconds > 120)
throw new TimeoutException();
-
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 {
Url = certificateUrl,
Nonce = state.Nonce
});
PrepareRequestContent(finalRequest, finalJson, 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(finalResponse, finalResponseText);
-
-
+ var pem = await SendAcmeRequest(finalRequest, state, HttpMethod.Post);
if (state.Cache == null) {
_logger.LogError($"{nameof(state.Cache)} is null");
return IDomainResult.Failed();
}
-
state.Cache.CachedCerts ??= new Dictionary();
state.Cache.CachedCerts[subject] = new CertificateCache {
- Cert = pem.Result,
+ Cert = pem.Result ?? string.Empty,
Private = key.ExportCspBlob(true),
PrivatePem = key.ExportRSAPrivateKeyPem()
};
-
- var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
-
-
-
+ var certPem = pem.Result ?? string.Empty;
+ if (!string.IsNullOrEmpty(certPem)) {
+ var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
+ }
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
}
#endregion
-
public Task KeyChange(Guid sessionId) {
throw new NotImplementedException();
}
@@ -581,58 +445,44 @@ public class LetsEncryptService : ILetsEncryptService {
public async Task RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
-
- if (state.Cache == null || state.Cache.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache)) {
+ if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) {
_logger.LogError("Certificate not found in cache");
return IDomainResult.Failed("Certificate not found");
}
-
- // Load the certificate from PEM format and convert it to DER format
- var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certificateCache.Cert));
+ var certPem = certificateCache.Cert ?? string.Empty;
+ if (string.IsNullOrEmpty(certPem)) {
+ _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 base64UrlEncodedCert = state.JwsService.Base64UrlEncoded(derEncodedCert);
-
-
+ var base64UrlEncodedCert = state.JwsService!.Base64UrlEncoded(derEncodedCert);
var revokeRequest = new RevokeRequest {
Certificate = base64UrlEncodedCert,
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);
-
var jwsHeader = new JwsHeader {
Url = state.Directory.RevokeCert,
Nonce = state.Nonce
};
-
var json = state.JwsService.Encode(revokeRequest, jwsHeader).ToJson();
-
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);
- UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
-
+ UpdateStateNonceIfNeeded(response, state, HttpMethod.Post);
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();
}
-
if (!response.IsSuccessStatusCode)
IDomainResult.CriticalDependencyError(responseText);
-
-
- // Remove the certificate from the cache after successful revocation
state.Cache.CachedCerts.Remove(subject);
-
_logger.LogInformation("Certificate revoked successfully");
-
return IDomainResult.Success();
- }
- catch (Exception ex) {
+ } catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
@@ -641,6 +491,7 @@ public class LetsEncryptService : ILetsEncryptService {
#region SendAsync
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
+ if (uri == null) throw new ArgumentNullException(nameof(uri));
if (uri.OriginalString != "directory") {
var (nonce, newNonceResult) = await NewNonce(sessionId);
if (!newNonceResult.IsSuccess || nonce == null) {
@@ -656,63 +507,63 @@ public class LetsEncryptService : ILetsEncryptService {
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
try {
var state = GetOrCreateState(sessionId);
-
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
-
- if (state.Directory == null)
- IDomainResult.Failed();
-
+ if (state.Directory?.NewNonce == null)
+ return (null, IDomainResult.Failed());
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) {
var message = "Let's Encrypt client unhandled exception";
-
_logger.LogError(ex, message);
- return IDomainResult.CriticalDependencyError(message);
+ return (null, IDomainResult.CriticalDependencyError(message));
}
}
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
return isPostAsGet
- ? state.JwsService.Encode(jwsHeader).ToJson()
- : state.JwsService.Encode(requestModel, jwsHeader).ToJson();
+ ? state.JwsService!.Encode(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) {
- request.Content = new StringContent(json);
- var contentType = method == HttpMethod.Post ? "application/jose+json" : "application/json";
+ request.Content = new StringContent(json ?? string.Empty);
+ var contentType = method == HttpMethod.Post ? GetContentType(ContentType.JoseJson) : GetContentType(ContentType.Json);
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) {
- if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
+ if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
throw new LetsEncrytException(responseText.ToObject(), response);
}
}
private SendResult ProcessResponseContent(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 {
Result = (TResult)(object)responseText
};
}
-
var responseContent = responseText.ToObject();
if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
ihl.Location = response.Headers.Location;
}
-
return new SendResult {
Result = responseContent,
ResponseText = responseText
};
}
#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();
}
diff --git a/src/LetsEncryptServer/LetsEncryptServer.csproj b/src/LetsEncryptServer/LetsEncryptServer.csproj
index 5b4d566..0605ead 100644
--- a/src/LetsEncryptServer/LetsEncryptServer.csproj
+++ b/src/LetsEncryptServer/LetsEncryptServer.csproj
@@ -9,10 +9,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/Release-DockerImage.bat b/src/Release-DockerImage.bat
new file mode 100644
index 0000000..93070d3
--- /dev/null
+++ b/src/Release-DockerImage.bat
@@ -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"
diff --git a/src/Release-DockerImage.ps1 b/src/Release-DockerImage.ps1
new file mode 100644
index 0000000..8415431
--- /dev/null
+++ b/src/Release-DockerImage.ps1
@@ -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."
diff --git a/src/ReleaseAndDeploy.bat b/src/ReleaseAndDeploy.bat
new file mode 100644
index 0000000..64fb452
--- /dev/null
+++ b/src/ReleaseAndDeploy.bat
@@ -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
\ No newline at end of file
diff --git a/src/ReverseProxy/ReverseProxy.csproj b/src/ReverseProxy/ReverseProxy.csproj
index e07bfdd..722f2de 100644
--- a/src/ReverseProxy/ReverseProxy.csproj
+++ b/src/ReverseProxy/ReverseProxy.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
diff --git a/src/ReverseProxy/appsettings.json b/src/ReverseProxy/appsettings.json
index 295531d..8ec7b6a 100644
--- a/src/ReverseProxy/appsettings.json
+++ b/src/ReverseProxy/appsettings.json
@@ -2,43 +2,31 @@
"ReverseProxy": {
"Routes": {
"well-known-acme-challenge-route": {
- "Match": {
- "Path": "/.well-known/acme-challenge/{**catch-all}"
- },
+ "Match": { "Path": "/.well-known/acme-challenge/{**catch-all}" },
"ClusterId": "letsencrypt-server"
},
"swagger-route": {
- "Match": {
- "Path": "/swagger/{**catch-all}"
- },
+ "Match": { "Path": "/swagger/{**catch-all}" },
"ClusterId": "letsencrypt-server"
},
"api-route": {
- "Match": {
- "Path": "/api/{**catch-all}"
- },
+ "Match": { "Path": "/api/{**catch-all}" },
"ClusterId": "letsencrypt-server"
},
"default-route": {
- "Match": {
- "Path": "{**catch-all}"
- },
+ "Match": { "Path": "{**catch-all}" },
"ClusterId": "letsencrypt-app"
}
},
"Clusters": {
"letsencrypt-server": {
"Destinations": {
- "destination1": {
- "Address": "http://letsencrypt-server:5000/"
- }
+ "d1": { "Address": "http://certs-ui-server:5000/" }
}
},
"letsencrypt-app": {
"Destinations": {
- "destination1": {
- "Address": "http://letsencrypt-app:3000/"
- }
+ "d1": { "Address": "http://certs-ui-client:3000/" }
}
}
}
diff --git a/src/SSHProvider/Configuration.cs b/src/SSHProvider/Configuration.cs
deleted file mode 100644
index 7f082ab..0000000
--- a/src/SSHProvider/Configuration.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-
-namespace MaksIT.SSHProvider;
-
-public class Configuration {
-}
diff --git a/src/SSHProvider/SSHProvider.csproj b/src/SSHProvider/SSHProvider.csproj
deleted file mode 100644
index 7ab36da..0000000
--- a/src/SSHProvider/SSHProvider.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- net8.0
- enable
- enable
-
-
-
-
-
-
-
-
-
-
diff --git a/src/SSHProvider/SSHService.cs b/src/SSHProvider/SSHService.cs
deleted file mode 100644
index b39e609..0000000
--- a/src/SSHProvider/SSHService.cs
+++ /dev/null
@@ -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();
- 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.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();
- }
- }
-}
diff --git a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
deleted file mode 100644
index 147f91e..0000000
--- a/src/Tests/SSHSerivceTests/Abstractions/ConfigurationBase.cs
+++ /dev/null
@@ -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>();
-
- // options.AuthMethod = EnumerationStringId.FromValue(vaultOptions["AuthMethod"]);
- // options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get();
- // options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get();
-
- // options.MountPath = vaultOptions["MountPath"];
- // options.SecretType = vaultOptions["SecretType"];
-
- // options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get>();
- //});
-
- return configurationBuilder.Build();
- }
-}
diff --git a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs b/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
deleted file mode 100644
index b608520..0000000
--- a/src/Tests/SSHSerivceTests/Abstractions/ServiceBase.cs
+++ /dev/null
@@ -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(appSettingsSection);
- var appSettings = appSettingsSection.Get();
-
- #region configurazione logging
- services.AddLogging(configure => {
- configure.AddSerilog(new LoggerConfiguration()
- //.ReadFrom.Configuration(_configuration)
- .CreateLogger());
- });
- #endregion
-
- }
-}
\ No newline at end of file
diff --git a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj b/src/Tests/SSHSerivceTests/SSHProviderTests.csproj
deleted file mode 100644
index dd0551b..0000000
--- a/src/Tests/SSHSerivceTests/SSHProviderTests.csproj
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
- net8.0
- enable
- enable
-
- false
- true
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
-
diff --git a/src/Tests/SSHSerivceTests/UnitTest1.cs b/src/Tests/SSHSerivceTests/UnitTest1.cs
deleted file mode 100644
index b4cfa22..0000000
--- a/src/Tests/SSHSerivceTests/UnitTest1.cs
+++ /dev/null
@@ -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>();
-
- 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);
- }
- }
- }
- }
-}
diff --git a/src/Tests/SSHSerivceTests/Usings.cs b/src/Tests/SSHSerivceTests/Usings.cs
deleted file mode 100644
index 8c927eb..0000000
--- a/src/Tests/SSHSerivceTests/Usings.cs
+++ /dev/null
@@ -1 +0,0 @@
-global using Xunit;
\ No newline at end of file
diff --git a/src/Tests/SSHSerivceTests/appsettings.json b/src/Tests/SSHSerivceTests/appsettings.json
deleted file mode 100644
index 0a219f9..0000000
--- a/src/Tests/SSHSerivceTests/appsettings.json
+++ /dev/null
@@ -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} {NewLine}{Exception}"
- }
- }
- ]
- },
- "Configuration": {
-
- }
-}
diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml
index d23c1d9..3832182 100644
--- a/src/docker-compose.override.yml
+++ b/src/docker-compose.override.yml
@@ -14,8 +14,6 @@ services:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- LETSENCRYPT_SERVER=http://localhost:8080
- # ports:
- # - "3000:3000"
networks:
- maks-it
@@ -31,10 +29,8 @@ services:
- MAKS-IT_AGENT_KEY=UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=
- MAKS-IT_AGENT_SERVICE=haproxy
volumes:
- - ./docker-compose/LetsEncryptServer/acme:/app/bin/Debug/net8.0/acme
- - ./docker-compose/LetsEncryptServer/cache:/app/bin/Debug/net8.0/cache
- ports:
- - "5000:5000"
+ - D:\Compose\MaksIT.CertsUI\acme:/app/bin/Debug/net8.0/acme
+ - D:\Compose\MaksIT.CertsUI\cache:/app/bin/Debug/net8.0/cache
networks:
- maks-it
diff --git a/src/helm/Chart.yaml b/src/helm/Chart.yaml
new file mode 100644
index 0000000..b37879e
--- /dev/null
+++ b/src/helm/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: certs-ui
+description: MaksIT CertsUI
+type: application
+version: 0.1.0
+appVersion: "latest"
\ No newline at end of file
diff --git a/src/helm/templates/_helpers.tpl b/src/helm/templates/_helpers.tpl
new file mode 100644
index 0000000..9a5449a
--- /dev/null
+++ b/src/helm/templates/_helpers.tpl
@@ -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 }}
diff --git a/src/helm/templates/deployments.yaml b/src/helm/templates/deployments.yaml
new file mode 100644
index 0000000..5a74f36
--- /dev/null
+++ b/src/helm/templates/deployments.yaml
@@ -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 }}
diff --git a/src/helm/templates/pvc.yaml b/src/helm/templates/pvc.yaml
new file mode 100644
index 0000000..f263783
--- /dev/null
+++ b/src/helm/templates/pvc.yaml
@@ -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 }}
diff --git a/src/helm/templates/secret-appsecrets.yaml b/src/helm/templates/secret-appsecrets.yaml
new file mode 100644
index 0000000..5712e50
--- /dev/null
+++ b/src/helm/templates/secret-appsecrets.yaml
@@ -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 }}
diff --git a/src/helm/templates/services.yaml b/src/helm/templates/services.yaml
new file mode 100644
index 0000000..1f005e9
--- /dev/null
+++ b/src/helm/templates/services.yaml
@@ -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 }}
diff --git a/src/helm/values.yaml b/src/helm/values.yaml
new file mode 100644
index 0000000..89a4251
--- /dev/null
+++ b/src/helm/values.yaml
@@ -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
\ No newline at end of file