(refactor): cert revokation and renew complete

This commit is contained in:
Maksym Sadovnychyy 2024-07-07 19:31:58 +02:00
parent 1e068877e0
commit 619348607e
19 changed files with 382 additions and 802 deletions

View File

@ -25,7 +25,7 @@ const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
args.forEach((arg) => {
result = result.replace(/{.*?}/, arg)
})
return `http://localhost:5000/${result}`
return `http://localhost:8080/${result}`
}
export { GetApiRoute, ApiRoutes }

View File

@ -21,7 +21,9 @@ Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-co
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agent", "Agent\Agent.csproj", "{871BDED3-C6AE-437D-9B45-3AA3F184D002}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Models", "Models\Models.csproj", "{6814169B-D4D0-40B2-9FA9-89997DD44C30}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csproj", "{6814169B-D4D0-40B2-9FA9-89997DD44C30}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseProxy", "ReverseProxy\ReverseProxy.csproj", "{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -65,6 +67,10 @@ Global
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Release|Any CPU.Build.0 = Release|Any CPU
{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -22,9 +22,6 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
using MaksIT.LetsEncrypt.Models.Requests;
using MaksIT.LetsEncrypt.Entities.Jws;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using System.Net.Mime;
using System;
using System.Security.Principal;
namespace MaksIT.LetsEncrypt.Services;
@ -141,8 +138,10 @@ public class LetsEncryptService : ILetsEncryptService {
await HandleNonceAsync(sessionId, state.Directory.NewAccount, state);
var jwsHeader = CreateJwsHeader(state.Directory.NewAccount, state.Nonce);
var json = EncodeMessage(false, letsEncryptOrder, state, jwsHeader);
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);
@ -233,8 +232,10 @@ public class LetsEncryptService : ILetsEncryptService {
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
var jwsHeader = CreateJwsHeader(state.Directory.NewOrder, state.Nonce);
var json = EncodeMessage(false, letsEncryptOrder, state, jwsHeader);
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);
@ -261,9 +262,10 @@ public class LetsEncryptService : ILetsEncryptService {
request = new HttpRequestMessage(HttpMethod.Post, item);
await HandleNonceAsync(sessionId, item, state);
jwsHeader = CreateJwsHeader(item, state.Nonce);
json = EncodeMessage(true, null, state, jwsHeader);
json = EncodeMessage(true, null, state, new JwsHeader {
Url = item,
Nonce = state.Nonce
});
PrepareRequestContent(request, json, HttpMethod.Post);
@ -349,8 +351,10 @@ public class LetsEncryptService : ILetsEncryptService {
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
await HandleNonceAsync(sessionId, challenge.Url, state);
var jwsHeader = CreateJwsHeader(challenge.Url, state.Nonce);
var json = EncodeMessage(false, "{}", state, jwsHeader);
var json = EncodeMessage(false, "{}", state, new JwsHeader {
Url = challenge.Url,
Nonce = state.Nonce
});
PrepareRequestContent(request, json, HttpMethod.Post);
var response = await _httpClient.SendAsync(request);
@ -407,8 +411,10 @@ public class LetsEncryptService : ILetsEncryptService {
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
var jwsHeader = CreateJwsHeader(state.Directory.NewOrder, state.Nonce);
var json = EncodeMessage(false, letsEncryptOrder, state, jwsHeader);
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);
@ -470,13 +476,12 @@ public class LetsEncryptService : ILetsEncryptService {
var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize);
await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize, state);
var jwsHeader = CreateJwsHeader(state.CurrentOrder.Finalize, state.Nonce);
var json = EncodeMessage(false, letsEncryptOrder, state, jwsHeader);
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);
@ -492,13 +497,12 @@ public class LetsEncryptService : ILetsEncryptService {
request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location);
await HandleNonceAsync(sessionId, state.CurrentOrder.Location, state);
jwsHeader = CreateJwsHeader(state.CurrentOrder.Location, state.Nonce);
json = EncodeMessage(true, null, state, jwsHeader);
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);
@ -524,13 +528,12 @@ public class LetsEncryptService : ILetsEncryptService {
var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl);
await HandleNonceAsync(sessionId, certificateUrl, state);
var finalJwsHeader = CreateJwsHeader(certificateUrl, state.Nonce);
var finalJson = EncodeMessage(true, null, state, finalJwsHeader);
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);
@ -583,42 +586,41 @@ public class LetsEncryptService : ILetsEncryptService {
return IDomainResult.Failed("Certificate not found");
}
string Base64UrlEncode(byte[] input) {
return Convert.ToBase64String(input)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
// Load the certificate from PEM format and convert it to DER format
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certificateCache.Cert));
var derEncodedCert = certificate.Export(X509ContentType.Cert);
var base64UrlEncodedCert = Base64UrlEncode(derEncodedCert);
var base64UrlEncodedCert = state.JwsService.Base64UrlEncoded(derEncodedCert);
// Convert the certificate to DER format and Base64 encode it
var base64Cert = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
var revokeRequest = new RevokeRequest {
Certificate = certificateCache.Cert,
Certificate = base64UrlEncodedCert,
Reason = (int)reason
};
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.RevokeCert);
await HandleNonceAsync(sessionId, state.Directory.RevokeCert, state);
var jwsHeader = CreateJwsHeader(state.Directory.RevokeCert, state.Nonce);
var json = EncodeMessage(false, revokeRequest, state, jwsHeader);
PrepareRequestContent(request, json, HttpMethod.Post);
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");
var response = await _httpClient.SendAsync(request);
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
var responseText = await response.Content.ReadAsStringAsync();
HandleProblemResponseAsync(response, responseText);
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
var erroObj = responseText.ToObject<Problem>();
}
if (!response.IsSuccessStatusCode)
IDomainResult.CriticalDependencyError(responseText);
var revokeResult = ProcessResponseContent<object>(response, responseText);
// Remove the certificate from the cache after successful revocation
state.Cache.CachedCerts.Remove(subject);
@ -630,7 +632,7 @@ public class LetsEncryptService : ILetsEncryptService {
catch (Exception ex) {
var message = "Let's Encrypt client unhandled exception";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
}
}
@ -668,13 +670,6 @@ public class LetsEncryptService : ILetsEncryptService {
}
}
private JwsHeader CreateJwsHeader(Uri uri, string? nonce) {
return new JwsHeader {
Url = uri,
Nonce = nonce
};
}
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
return isPostAsGet
? state.JwsService.Encode(jwsHeader).ToJson()

View File

@ -114,5 +114,17 @@ namespace MaksIT.LetsEncryptServer.Controllers {
var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData);
return result.ToActionResult();
}
/// <summary>
/// Revoke certificates
/// </summary>
/// <param name="sessionId"></param>
/// <param name="requestData"></param>
/// <returns></returns>
[HttpPost("{sessionId}/certificates/revoke")]
public async Task<IActionResult> RevokeCertificates(Guid sessionId, [FromBody] RevokeCertificatesRequest requestData) {
var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData);
return result.ToActionResult();
}
}
}

View File

@ -3,7 +3,7 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 5000
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release

View File

@ -23,7 +23,7 @@
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "5000"
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}

View File

@ -23,8 +23,8 @@ public interface ICertsInternalService : ICertsCommonService {
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType);
Task<IDomainResult> GetOrderAsync(Guid sessionId, string[] hostnames);
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, string[] hostnames);
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames);
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames);
Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames);
Task<IDomainResult> FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames);
@ -37,6 +37,7 @@ public interface ICertsRestService : ICertsCommonService {
Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData);
}
public interface ICertsRestChallengeService {
@ -166,33 +167,6 @@ public class CertsFlowService : ICertsFlowService {
return await _letsEncryptService.GetOrder(sessionId, hostnames);
}
public async Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
foreach (var hostname in hostnames) {
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
if (!result.IsSuccess)
return result;
}
// TODO: Move to separate method
// Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null)
return getCacheResult;
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
if (!saveResult.IsSuccess)
return saveResult;
return IDomainResult.Success();
}
public async Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames) {
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
@ -221,6 +195,26 @@ public class CertsFlowService : ICertsFlowService {
return IDomainResult.Success(results);
}
public async Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
foreach (var hostname in hostnames) {
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
if (!result.IsSuccess)
return result;
}
// TODO: Move to separate method
// Persist the cache
var (cache, getCacheResult) = _letsEncryptService.GetRegistrationCache(sessionId);
if (!getCacheResult.IsSuccess || cache == null)
return getCacheResult;
var saveResult = await _cacheService.SaveToCacheAsync(cache.AccountId, cache);
if (!saveResult.IsSuccess)
return saveResult;
return IDomainResult.Success();
}
public async Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[]hostnames) {
var (sessionId, configureClientResult) = await ConfigureClientAsync(isStaging);
if (!configureClientResult.IsSuccess || sessionId == null)
@ -230,13 +224,16 @@ public class CertsFlowService : ICertsFlowService {
if (!initResult.IsSuccess)
return (null, initResult);
var (_, newOrderResult) = await NewOrderAsync(sessionId.Value, hostnames, challengeType);
var (challenges, newOrderResult) = await NewOrderAsync(sessionId.Value, hostnames, challengeType);
if (!newOrderResult.IsSuccess)
return (null, newOrderResult);
if (challenges?.Count > 0) {
var challengeResult = await CompleteChallengesAsync(sessionId.Value);
if (!challengeResult.IsSuccess)
return (null, challengeResult);
}
var getOrderResult = await GetOrderAsync(sessionId.Value, hostnames);
if (!getOrderResult.IsSuccess)
@ -273,6 +270,7 @@ public class CertsFlowService : ICertsFlowService {
#endregion
#region REST methods
public Task<(Guid?, IDomainResult)> ConfigureClientAsync(ConfigureClientRequest requestData) =>
ConfigureClientAsync(requestData.IsStaging);
@ -291,6 +289,9 @@ public class CertsFlowService : ICertsFlowService {
public Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) =>
ApplyCertificatesAsync(sessionId, requestData.Hostnames);
public Task<IDomainResult> RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) =>
RevokeCertificatesAsync(sessionId, requestData.Hostnames);
#endregion
#region Acme Challenge REST methods

View File

@ -6,18 +6,11 @@ using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
public class PostAccountRequest : IValidatableObject {
public required string Description { get; set; }
public required string[] Contacts { get; set; }
public class RevokeCertificatesRequest : IValidatableObject {
public required string [] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Description == null || Description.Length == 0)
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
if (Contacts == null || Contacts.Length == 0)
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}

View File

@ -1,13 +1,13 @@
{
"info": {
"_postman_id": "95186b61-1197-4a6e-a90f-d97223528d90",
"name": "LetsEncrypt Staging",
"name": "LetsEncrypt Client",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "33635244"
},
"item": [
{
"name": "Cache",
"name": "account",
"item": [
{
"name": "get cache contacts",
@ -116,24 +116,17 @@
]
},
{
"name": "Certs Manual Flow",
"name": "certs",
"item": [
{
"name": "letsencrypt staging",
"name": "letsencrypt directory",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://acme-staging-v02.api.letsencrypt.org/directory",
"protocol": "https",
"raw": "{{letsEncryptDirectory}}",
"host": [
"acme-staging-v02",
"api",
"letsencrypt",
"org"
],
"path": [
"directory"
"{{letsEncryptDirectory}}"
]
},
"description": "[https://letsencrypt.status.io/](https://letsencrypt.status.io/)"
@ -141,7 +134,7 @@
"response": []
},
{
"name": "configure client",
"name": "configure-client",
"event": [
{
"listen": "test",
@ -185,8 +178,17 @@
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"isStaging\": {{isStaging}}\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/ConfigureClient",
"raw": "http://localhost:8080/api/certs/configure-client",
"protocol": "http",
"host": [
"localhost"
@ -194,20 +196,20 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"ConfigureClient"
"certs",
"configure-client"
]
}
},
"response": []
},
{
"name": "terms of service",
"name": "terms-of-service",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/api/CertsFlow/TermsOfService/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/terms-of-service",
"protocol": "http",
"host": [
"localhost"
@ -215,9 +217,9 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"TermsOfService",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"terms-of-service"
]
}
},
@ -300,7 +302,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/Init/{{sessionId}}/{{accountId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/init/{{accountId}}",
"protocol": "http",
"host": [
"localhost"
@ -308,9 +310,9 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"Init",
"certs",
"{{sessionId}}",
"init",
"{{accountId}}"
]
}
@ -318,7 +320,7 @@
"response": []
},
{
"name": "new order",
"name": "order",
"event": [
{
"listen": "test",
@ -378,7 +380,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/NewOrder/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/order",
"protocol": "http",
"host": [
"localhost"
@ -386,9 +388,9 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"NewOrder",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"order"
]
}
},
@ -438,7 +440,7 @@
"response": []
},
{
"name": "complete challenges",
"name": "complete-challenges",
"request": {
"method": "POST",
"header": [
@ -461,7 +463,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/CompleteChallenges/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/complete-challenges",
"protocol": "http",
"host": [
"localhost"
@ -469,16 +471,16 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"CompleteChallenges",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"complete-challenges"
]
}
},
"response": []
},
{
"name": "get order",
"name": "order-status",
"request": {
"method": "POST",
"header": [
@ -501,7 +503,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/GetOrder/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/order-status",
"protocol": "http",
"host": [
"localhost"
@ -509,16 +511,16 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"GetOrder",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"order-status"
]
}
},
"response": []
},
{
"name": "get certificates",
"name": "download",
"request": {
"method": "POST",
"header": [
@ -541,7 +543,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/GetCertificates/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}//certificates/download",
"protocol": "http",
"host": [
"localhost"
@ -549,16 +551,18 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"GetCertificates",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"",
"certificates",
"download"
]
}
},
"response": []
},
{
"name": "apply certificates",
"name": "apply",
"request": {
"method": "POST",
"header": [
@ -581,7 +585,7 @@
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/ApplyCertificates/{{sessionId}}",
"raw": "http://localhost:8080/api/certs/{{sessionId}}/certificates/apply",
"protocol": "http",
"host": [
"localhost"
@ -589,9 +593,53 @@
"port": "8080",
"path": [
"api",
"CertsFlow",
"ApplyCertificates",
"{{sessionId}}"
"certs",
"{{sessionId}}",
"certificates",
"apply"
]
}
},
"response": []
},
{
"name": "revoke",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
},
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"staging.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/certs/{{sessionId}}/certificates/revoke",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"certs",
"{{sessionId}}",
"certificates",
"revoke"
]
}
},

View File

@ -1,601 +0,0 @@
{
"info": {
"_postman_id": "728f64b6-893b-43fa-802e-ee836d1dc372",
"name": "LetsEncrypt Production",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "33635244"
},
"item": [
{
"name": "Cache",
"item": [
{
"name": "get cache contacts",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"url": {
"raw": "http://localhost:8080/api/Cache/GetContacts/{{accountId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"Cache",
"GetContacts",
"{{accountId}}"
]
}
},
"response": []
},
{
"name": "set cache contacts",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"contacts\": [\r\n \"maksym.sadovnychyy@gmail.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/Cache/SetContacts/{{accountId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"Cache",
"SetContacts",
"{{accountId}}"
]
}
},
"response": []
},
{
"name": "host with upcoming ssl expire",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"disabled": true
},
{
"key": "Accept",
"value": "application/json",
"disabled": true
}
],
"url": {
"raw": "http://localhost:8080/api/CertsFlow/HostsWithUpcomingSslExpiry/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"HostsWithUpcomingSslExpiry",
"{{sessionId}}"
]
}
},
"response": []
}
]
},
{
"name": "Certs Manual Flow",
"item": [
{
"name": "letsencrypt production",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://acme-v02.api.letsencrypt.org/directory",
"protocol": "https",
"host": [
"acme-v02",
"api",
"letsencrypt",
"org"
],
"path": [
"directory"
]
}
},
"response": []
},
{
"name": "configure client",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Get the plain text response\r",
" let responseBody = pm.response.text();\r",
" \r",
" // Remove the surrounding quotes if present\r",
" responseBody = responseBody.replace(/^\"|\"$/g, '');\r",
" \r",
" // Check if the response body is a valid GUID\r",
" if (/^[0-9a-fA-F-]{36}$/.test(responseBody)) {\r",
" // Set the environment variable sessionId with the response\r",
" pm.environment.set(\"sessionId\", responseBody);\r",
" console.log(`sessionId set to: ${responseBody}`);\r",
" } else {\r",
" console.log(\"Response body is not a valid GUID\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"url": {
"raw": "http://localhost:8080/api/CertsFlow/ConfigureClient",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"ConfigureClient"
]
}
},
"response": []
},
{
"name": "terms of service",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/api/CertsFlow/TermsOfService/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"TermsOfService",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "init",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Get the plain text response\r",
" let responseBody = pm.response.text();\r",
" \r",
" // Remove the surrounding quotes if present\r",
" responseBody = responseBody.replace(/^\"|\"$/g, '');\r",
" \r",
" // Check if the response body is a valid GUID\r",
" if (/^[0-9a-fA-F-]{36}$/.test(responseBody)) {\r",
" // Set the environment variable accountId with the response\r",
" pm.environment.set(\"accountId\", responseBody);\r",
" console.log(`accountId set to: ${responseBody}`);\r",
" } else {\r",
" console.log(\"Response body is not a valid GUID\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
},
{
"listen": "prerequest",
"script": {
"exec": [
"// Retrieve sessionId and accountId from environment variables or global variables\r",
"var sessionId = pm.environment.get(\"sessionId\") || pm.globals.get(\"sessionId\");\r",
"var accountId = pm.environment.get(\"accountId\") || pm.globals.get(\"accountId\");\r",
"\r",
"// Base URL without the optional accountId parameter\r",
"var baseUrl = `http://localhost:8080/CertsFlow/Init/${sessionId}`;\r",
"\r",
"// Append the accountId if it is provided\r",
"if (accountId) {\r",
" pm.request.url = `${baseUrl}/${accountId}`;\r",
"} else {\r",
" pm.request.url = baseUrl;\r",
"}"
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"contacts\": [\r\n \"maksym.sadovnychyy@gmail.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/Init/{{sessionId}}/{{accountId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"Init",
"{{sessionId}}",
"{{accountId}}"
]
}
},
"response": []
},
{
"name": "new order",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Parse the JSON response\r",
" let responseBody;\r",
" try {\r",
" responseBody = pm.response.json();\r",
" } catch (e) {\r",
" console.error(\"Failed to parse JSON response:\", e);\r",
" return;\r",
" }\r",
"\r",
" // Check if the response is an array and has at least one element\r",
" if (Array.isArray(responseBody) && responseBody.length > 0) {\r",
" // Get the first element of the array\r",
" const firstElement = responseBody[0];\r",
" \r",
" // Set the environment variable challenge with the first element\r",
" pm.environment.set(\"challenge\", firstElement);\r",
" console.log(`challenge set to: ${firstElement}`);\r",
" } else {\r",
" console.log(\"Response body is not an array or is empty\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ],\r\n \"challengeType\": \"http-01\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/NewOrder/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"NewOrder",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "acme-challenge local",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/.well-known/acme-challenge/{{challenge}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
".well-known",
"acme-challenge",
"{{challenge}}"
]
}
},
"response": []
},
{
"name": "acme-challenge",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://maks-it.com/.well-known/acme-challenge/{{challenge}}",
"protocol": "http",
"host": [
"maks-it",
"com"
],
"path": [
".well-known",
"acme-challenge",
"{{challenge}}"
]
}
},
"response": []
},
{
"name": "complete challenges",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/CompleteChallenges/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"CompleteChallenges",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "get order",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/GetOrder/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"GetOrder",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "get certificates",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/GetCertificates/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"GetCertificates",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "apply certificates",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/api/CertsFlow/ApplyCertificates/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"CertsFlow",
"ApplyCertificates",
"{{sessionId}}"
]
}
},
"response": []
}
]
}
]
}

View File

@ -0,0 +1,24 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ReverseProxy/ReverseProxy.csproj", "ReverseProxy/"]
RUN dotnet restore "./ReverseProxy/ReverseProxy.csproj"
COPY . .
WORKDIR "/src/ReverseProxy"
RUN dotnet build "./ReverseProxy.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ReverseProxy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ReverseProxy.dll"]

View File

@ -0,0 +1,22 @@
var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddDataProtection()
// .PersistKeysToFileSystem(new DirectoryInfo(@"/keys"))
// .SetApplicationName("YourAppName");
// Add YARP services
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseRouting();
// Use YARP reverse proxy
app.UseEndpoints(endpoints => {
endpoints.MapReverseProxy();
});
app.Run();

View File

@ -0,0 +1,40 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5276"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:40278",
"sslPort": 0
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="8.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,46 @@
{
"ReverseProxy": {
"Routes": {
"well-known-acme-challenge-route": {
"Match": {
"Path": "/.well-known/acme-challenge/{**catch-all}"
},
"ClusterId": "letsencryptserver"
},
"swagger-route": {
"Match": {
"Path": "/swagger/{**catch-all}"
},
"ClusterId": "letsencryptserver"
},
"api-route": {
"Match": {
"Path": "/api/{**catch-all}"
},
"ClusterId": "letsencryptserver"
},
"default-route": {
"Match": {
"Path": "{**catch-all}"
},
"ClusterId": "letsencryptapp"
}
},
"Clusters": {
"letsencryptserver": {
"Destinations": {
"destination1": {
"Address": "http://letsencryptserver:5000/"
}
}
},
"letsencryptapp": {
"Destinations": {
"destination1": {
"Address": "http://letsencryptapp:3000/"
}
}
}
}
}
}

View File

@ -1,14 +1,12 @@
version: '3.9'
services:
# haproxy:
# ports:
# - "8080:8080"
# volumes:
# - ./docker-compose/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
# depends_on:
# - letsencryptapp
# - letsencryptserver
reverseProxy:
ports:
- "8080:8080"
depends_on:
- letsencryptapp
- letsencryptserver
# letsencryptapp:
# ports:

View File

@ -1,15 +1,17 @@
version: '3.9'
services:
letsencryptapp:
image: ${DOCKER_REGISTRY-}letsencryptapp
build:
context: .
dockerfile: ClientApp/Dockerfile
# haproxy:
# image: haproxy:3.0.0-alpine
# letsencryptapp:
# image: ${DOCKER_REGISTRY-}letsencryptapp
# build:
# context: .
# dockerfile: ClientApp/Dockerfile
reverseProxy:
image: ${DOCKER_REGISTRY-}reverseproxy
build:
context: .
dockerfile: ReverseProxy/Dockerfile
letsencryptserver:
image: ${DOCKER_REGISTRY-}letsencryptserver

View File

@ -1,30 +0,0 @@
# docker_compose/haproxy/haproxy.cfg
global
log stdout format raw local0
maxconn 4096
tune.ssl.default-dh-param 2048
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http_front
bind *:8080
acl is_letsencryptserver path_beg /api /swagger /.well-known/acme-challenge
use_backend letsencryptserver_backend if is_letsencryptserver
default_backend letsencryptapp_backend
backend letsencryptapp_backend
server letsencryptapp letsencryptapp:3000 check
backend letsencryptserver_backend
server letsencryptserver letsencryptserver:5000 check