diff --git a/src/ClientApp/ApiRoutes.tsx b/src/ClientApp/ApiRoutes.tsx index 841bc7c..15af4ec 100644 --- a/src/ClientApp/ApiRoutes.tsx +++ b/src/ClientApp/ApiRoutes.tsx @@ -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 } diff --git a/src/LetsEncrypt.sln b/src/LetsEncrypt.sln index 42180a2..8db719a 100644 --- a/src/LetsEncrypt.sln +++ b/src/LetsEncrypt.sln @@ -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 diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index 8549caf..9ddffa5 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -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); @@ -232,9 +231,11 @@ 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); @@ -406,9 +410,11 @@ 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,43 +586,42 @@ 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(); + } - var revokeResult = ProcessResponseContent(response, responseText); + if (!response.IsSuccessStatusCode) + IDomainResult.CriticalDependencyError(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() diff --git a/src/LetsEncryptServer/Controllers/CertsFlowController.cs b/src/LetsEncryptServer/Controllers/CertsFlowController.cs index 6ae66be..56ef963 100644 --- a/src/LetsEncryptServer/Controllers/CertsFlowController.cs +++ b/src/LetsEncryptServer/Controllers/CertsFlowController.cs @@ -114,5 +114,17 @@ namespace MaksIT.LetsEncryptServer.Controllers { var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData); return result.ToActionResult(); } + + /// + /// Revoke certificates + /// + /// + /// + /// + [HttpPost("{sessionId}/certificates/revoke")] + public async Task RevokeCertificates(Guid sessionId, [FromBody] RevokeCertificatesRequest requestData) { + var result = await _certsFlowService.RevokeCertificatesAsync(sessionId, requestData); + return result.ToActionResult(); + } } } diff --git a/src/LetsEncryptServer/Dockerfile b/src/LetsEncryptServer/Dockerfile index 901b826..7599bb7 100644 --- a/src/LetsEncryptServer/Dockerfile +++ b/src/LetsEncryptServer/Dockerfile @@ -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 diff --git a/src/LetsEncryptServer/Properties/launchSettings.json b/src/LetsEncryptServer/Properties/launchSettings.json index b702226..0ff7fbc 100644 --- a/src/LetsEncryptServer/Properties/launchSettings.json +++ b/src/LetsEncryptServer/Properties/launchSettings.json @@ -23,7 +23,7 @@ "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "environmentVariables": { - "ASPNETCORE_HTTP_PORTS": "5000" + "ASPNETCORE_HTTP_PORTS": "8080" }, "publishAllPorts": true } diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs index a3114d8..41ca61f 100644 --- a/src/LetsEncryptServer/Services/CertsFlowService.cs +++ b/src/LetsEncryptServer/Services/CertsFlowService.cs @@ -23,8 +23,8 @@ public interface ICertsInternalService : ICertsCommonService { Task<(List?, IDomainResult)> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType); Task GetOrderAsync(Guid sessionId, string[] hostnames); Task GetCertificatesAsync(Guid sessionId, string[] hostnames); - Task RevokeCertificatesAsync(Guid sessionId, string[] hostnames); Task<(Dictionary?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, string[] hostnames); + Task RevokeCertificatesAsync(Guid sessionId, string[] hostnames); Task<(Guid?, IDomainResult)> FullFlow(bool isStaging, Guid? accountId, string description, string[] contacts, string challengeType, string[] hostnames); Task FullRevocationFlow(bool isStaging, Guid accountId, string description, string[] contacts, string[] hostnames); @@ -37,6 +37,7 @@ public interface ICertsRestService : ICertsCommonService { Task GetOrderAsync(Guid sessionId, GetOrderRequest requestData); Task GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); Task<(Dictionary?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); + Task 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 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?, 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 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,14 +224,17 @@ 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); - - var challengeResult = await CompleteChallengesAsync(sessionId.Value); - if (!challengeResult.IsSuccess) - return (null, challengeResult); - + + 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) return (null, getOrderResult); @@ -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?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) => ApplyCertificatesAsync(sessionId, requestData.Hostnames); + public Task RevokeCertificatesAsync(Guid sessionId, RevokeCertificatesRequest requestData) => + RevokeCertificatesAsync(sessionId, requestData.Hostnames); + #endregion #region Acme Challenge REST methods diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/PostAccountRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs similarity index 53% rename from src/Models/LetsEncryptServer/CertsFlow/Requests/PostAccountRequest.cs rename to src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs index 5dd6619..27c66b2 100644 --- a/src/Models/LetsEncryptServer/CertsFlow/Requests/PostAccountRequest.cs +++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs @@ -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 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) }); } diff --git a/src/Postman/LetsEncrypt Staging.postman_collection.json b/src/Postman/LetsEncrypt Client.postman_collection.json similarity index 83% rename from src/Postman/LetsEncrypt Staging.postman_collection.json rename to src/Postman/LetsEncrypt Client.postman_collection.json index e3469b5..9fe5b7c 100644 --- a/src/Postman/LetsEncrypt Staging.postman_collection.json +++ b/src/Postman/LetsEncrypt Client.postman_collection.json @@ -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" ] } }, diff --git a/src/Postman/LetsEncrypt Production.postman_collection.json b/src/Postman/LetsEncrypt Production.postman_collection.json deleted file mode 100644 index d5c844a..0000000 --- a/src/Postman/LetsEncrypt Production.postman_collection.json +++ /dev/null @@ -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": [] - } - ] - } - ] -} \ No newline at end of file diff --git a/src/ReverseProxy/Dockerfile b/src/ReverseProxy/Dockerfile new file mode 100644 index 0000000..1aa13ca --- /dev/null +++ b/src/ReverseProxy/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/ReverseProxy/Program.cs b/src/ReverseProxy/Program.cs new file mode 100644 index 0000000..5a0f792 --- /dev/null +++ b/src/ReverseProxy/Program.cs @@ -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(); diff --git a/src/ReverseProxy/Properties/launchSettings.json b/src/ReverseProxy/Properties/launchSettings.json new file mode 100644 index 0000000..b4c5ea7 --- /dev/null +++ b/src/ReverseProxy/Properties/launchSettings.json @@ -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 + } + } +} \ No newline at end of file diff --git a/src/ReverseProxy/ReverseProxy.csproj b/src/ReverseProxy/ReverseProxy.csproj new file mode 100644 index 0000000..e07bfdd --- /dev/null +++ b/src/ReverseProxy/ReverseProxy.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + diff --git a/src/ReverseProxy/appsettings.Development.json b/src/ReverseProxy/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/ReverseProxy/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/ReverseProxy/appsettings.json b/src/ReverseProxy/appsettings.json new file mode 100644 index 0000000..f1f04e1 --- /dev/null +++ b/src/ReverseProxy/appsettings.json @@ -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/" + } + } + } + } + } +} diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml index a84838e..5012558 100644 --- a/src/docker-compose.override.yml +++ b/src/docker-compose.override.yml @@ -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: diff --git a/src/docker-compose.yml b/src/docker-compose.yml index e7fab65..372244a 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -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 diff --git a/src/docker-compose/haproxy/haproxy.cfg b/src/docker-compose/haproxy/haproxy.cfg deleted file mode 100644 index 39944f1..0000000 --- a/src/docker-compose/haproxy/haproxy.cfg +++ /dev/null @@ -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