mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): webapi implementation and containerization
This commit is contained in:
parent
150b8e76fc
commit
d75265c621
501
LetsEncrypt.postman_collection.json
Normal file
501
LetsEncrypt.postman_collection.json
Normal file
@ -0,0 +1,501 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "728f64b6-893b-43fa-802e-ee836d1dc372",
|
||||
"name": "LetsEncrypt",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "33635244"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "letsencrypt staging",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"acme-staging-v02",
|
||||
"api",
|
||||
"letsencrypt",
|
||||
"org"
|
||||
],
|
||||
"path": [
|
||||
"directory"
|
||||
]
|
||||
},
|
||||
"description": "[https://letsencrypt.status.io/](https://letsencrypt.status.io/)"
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"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": "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": "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": "terms of service",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/CertsFlow/TermsOfService/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"CertsFlow",
|
||||
"TermsOfService",
|
||||
"{{sessionId}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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/CertsFlow/ConfigureClient",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"CertsFlow",
|
||||
"ConfigureClient"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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/CertsFlow/Init/{{sessionId}}/{{accountId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"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 \"www.maks-it.com\"\r\n ],\r\n \"challengeType\": \"http-01\"\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/CertsFlow/NewOrder/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"CertsFlow",
|
||||
"NewOrder",
|
||||
"{{sessionId}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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/CertsFlow/CompleteChallenges/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"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 \"www.maks-it.com\"\r\n ]\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/CertsFlow/GetOrder/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"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 \"www.maks-it.com\"\r\n ]\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/CertsFlow/GetCertificates/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"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 \"www.maks-it.com\"\r\n ]\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/CertsFlow/ApplyCertificates/{{sessionId}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"CertsFlow",
|
||||
"ApplyCertificates",
|
||||
"{{sessionId}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
102
README.md
102
README.md
@ -6,3 +6,105 @@ Simple client to obtain Let's Encrypt HTTPS certificates developed with .net cor
|
||||
|
||||
* 29 Jun, 2019 - V1.0
|
||||
* 01 Nov, 2019 - V2.0 (Dependency Injection pattern impelemtation)
|
||||
* 31 May, 2024 - V3.0 (Webapi and containerization)
|
||||
|
||||
## Haproxy configuration
|
||||
|
||||
```bash
|
||||
#---------------------------------------------------------------------
|
||||
# Example configuration for a possible web application. See the
|
||||
# full configuration options online.
|
||||
#
|
||||
# https://www.haproxy.org/download/1.8/doc/configuration.txt
|
||||
#
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Global settings
|
||||
#---------------------------------------------------------------------
|
||||
global
|
||||
# to have these messages end up in /var/log/haproxy.log you will
|
||||
# need to:
|
||||
#
|
||||
# 1) configure syslog to accept network log events. This is done
|
||||
# by adding the '-r' option to the SYSLOGD_OPTIONS in
|
||||
# /etc/sysconfig/syslog
|
||||
#
|
||||
# 2) configure local2 events to go to the /var/log/haproxy.log
|
||||
# file. A line like the following can be added to
|
||||
# /etc/sysconfig/syslog
|
||||
#
|
||||
# local2.* /var/log/haproxy.log
|
||||
#
|
||||
log 127.0.0.1 local2
|
||||
|
||||
chroot /var/lib/haproxy
|
||||
pidfile /var/run/haproxy.pid
|
||||
maxconn 4000
|
||||
user haproxy
|
||||
group haproxy
|
||||
daemon
|
||||
|
||||
# Adjust the maxconn value based on your server\'s capacity
|
||||
maxconn 2048
|
||||
|
||||
# SSL certificates directory
|
||||
# ca-base /etc/ssl/certs
|
||||
#crt-base /etc/ssl/private
|
||||
|
||||
# Default SSL certificate (used if no SNI match)
|
||||
#ssl-default-bind-crt /etc/haproxy/certs/default.pem
|
||||
|
||||
# turn on stats unix socket
|
||||
# stats socket /var/lib/haproxy/stats level admin mode 660
|
||||
#stats socket /var/run/haproxy/admin.sock level admin mode 660 user haproxy group haproxy
|
||||
|
||||
setenv ACCOUNT_THUMBPRINT \'\'
|
||||
|
||||
# utilize system-wide crypto-policies
|
||||
ssl-default-bind-ciphers PROFILE=SYSTEM
|
||||
ssl-default-server-ciphers PROFILE=SYSTEM
|
||||
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# common defaults that all the \'listen\' and \'backend\' sections will
|
||||
# use if not designated in their block
|
||||
#---------------------------------------------------------------------
|
||||
defaults
|
||||
mode http
|
||||
log global
|
||||
option httplog
|
||||
option dontlognull
|
||||
option http-server-close
|
||||
option forwardfor except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 3
|
||||
timeout http-request 10s
|
||||
timeout queue 1m
|
||||
timeout connect 10s
|
||||
timeout client 1m
|
||||
timeout server 1m
|
||||
timeout http-keep-alive 10s
|
||||
timeout check 10s
|
||||
maxconn 3000
|
||||
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Frontend configuration for handling multiple domains with SNI
|
||||
#---------------------------------------------------------------------
|
||||
frontend web
|
||||
bind :80
|
||||
bind :443 ssl crt /etc/haproxy/certs/ strict-sni
|
||||
|
||||
# Handling for ACME challenge paths
|
||||
acl acme_challenge path_beg /.well-known/acme-challenge/
|
||||
use_backend acme_challenge_backend if acme_challenge
|
||||
|
||||
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Backend configuration for ACME challenge
|
||||
#---------------------------------------------------------------------
|
||||
backend acme_challenge_backend
|
||||
server acme_challenge 127.0.0.1:8080
|
||||
```
|
||||
@ -13,4 +13,8 @@
|
||||
<None Remove="Abstractions\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
24
src/Core/Logger/ConsoleLogger.cs
Normal file
24
src/Core/Logger/ConsoleLogger.cs
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MaksIT.Core.Logger;
|
||||
|
||||
public class MyCustomLogger : ILogger {
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
19
src/Core/Logger/ConsoleLoggerProvider.cs
Normal file
19
src/Core/Logger/ConsoleLoggerProvider.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MaksIT.Core.Logger;
|
||||
|
||||
public class MyCustomLoggerProvider : ILoggerProvider {
|
||||
public ILogger CreateLogger(string categoryName) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
8
src/Core/Logger/ConsoleLoggerServiceExtension.cs
Normal file
8
src/Core/Logger/ConsoleLoggerServiceExtension.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
namespace MaksIT.Core.Logger;
|
||||
|
||||
public static class MyCustomLoggerExtensions {
|
||||
|
||||
}
|
||||
@ -4,5 +4,8 @@ namespace MaksIT.LetsEncrypt.Entities;
|
||||
|
||||
public class CachedCertificateResult {
|
||||
public RSACryptoServiceProvider? PrivateKey { get; set; }
|
||||
|
||||
public string PrivateKeyPem => PrivateKey == null ? "" : PrivateKey.ExportRSAPrivateKeyPem();
|
||||
|
||||
public string? Certificate { get; set; }
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
|
||||
@ -1,25 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Models.Responses;
|
||||
namespace MaksIT.LetsEncrypt.Models.Responses;
|
||||
|
||||
public class AcmeDirectory {
|
||||
public Uri NewNonce { get; set; }
|
||||
|
||||
public Uri NewAccount { get; set; }
|
||||
|
||||
public Uri NewOrder { get; set; }
|
||||
|
||||
// New authorization If the ACME server does not implement pre-authorization
|
||||
// (Section 7.4.1) it MUST omit the "newAuthz" field of the directory.
|
||||
// [JsonProperty("newAuthz")]
|
||||
// public Uri NewAuthz { get; set; }
|
||||
public Uri RevokeCertificate { get; set; }
|
||||
|
||||
public Uri KeyChange { get; set; }
|
||||
|
||||
public AcmeDirectoryMeta Meta { get; set; }
|
||||
public Uri NewAccount { get; set; }
|
||||
public Uri NewNonce { get; set; }
|
||||
public Uri NewOrder { get; set; }
|
||||
public Uri RenewalInfo { get; set; }
|
||||
public Uri RevokeCertificate { get; set; }
|
||||
}
|
||||
|
||||
public class AcmeDirectoryMeta {
|
||||
public string[] CaaIdentities { get; set; }
|
||||
public string TermsOfService { get; set; }
|
||||
public string Website { get; set; }
|
||||
}
|
||||
@ -1,153 +1,178 @@
|
||||
/**
|
||||
* https://community.letsencrypt.org/t/trying-to-do-post-as-get-but-getting-post-jws-not-signed/108371
|
||||
* https://tools.ietf.org/html/rfc8555#section-6.2
|
||||
*
|
||||
*/
|
||||
using System.Text;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Exceptions;
|
||||
using MaksIT.Core.Extensions;
|
||||
|
||||
using MaksIT.LetsEncrypt.Models.Responses;
|
||||
using MaksIT.LetsEncrypt.Models.Interfaces;
|
||||
using MaksIT.LetsEncrypt.Models.Requests;
|
||||
using MaksIT.LetsEncrypt.Entities.Jws;
|
||||
using DomainResults.Common;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Services;
|
||||
|
||||
public interface ILetsEncryptService {
|
||||
Task<(AcmeDirectory?, IDomainResult)> ConfigureClient(string url);
|
||||
Task<(RegistrationCache?, IDomainResult)> Init(Uri newAccount, Uri newNonce, string[] contacts);
|
||||
Task<((Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?), IDomainResult)> NewOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames, string challengeType);
|
||||
Task<IDomainResult> CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List<AuthorizationChallenge> _challenges);
|
||||
Task<(Order?, IDomainResult)> GetOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames);
|
||||
Task<(Dictionary<string, CertificateCache>?, IDomainResult)> GetCertificate(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, Order currentOrder, string location, string [] subjects);
|
||||
Task<IDomainResult> ConfigureClient(Guid sessionId, string url);
|
||||
Task<IDomainResult> Init(Guid sessionId, string[] contacts, RegistrationCache? registrationCache);
|
||||
RegistrationCache? GetRegistrationCache(Guid sessionId);
|
||||
(string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId);
|
||||
Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
|
||||
Task<IDomainResult> CompleteChallenges(Guid sessionId);
|
||||
Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames);
|
||||
Task<IDomainResult> GetCertificate(Guid sessionId, string subject);
|
||||
(CachedCertificateResult?, IDomainResult) TryGetCachedCertificate(Guid sessionId, string subject);
|
||||
}
|
||||
|
||||
public class LetsEncryptService : ILetsEncryptService {
|
||||
|
||||
private readonly ILogger<LetsEncryptService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public LetsEncryptService(
|
||||
ILogger<LetsEncryptService> logger,
|
||||
HttpClient httpClient
|
||||
) {
|
||||
HttpClient httpClient,
|
||||
IMemoryCache cache) {
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
private State GetOrCreateState(Guid sessionId) {
|
||||
if (!_cache.TryGetValue(sessionId, out State state)) {
|
||||
state = new State();
|
||||
_cache.Set(sessionId, state, TimeSpan.FromHours(1));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="contacts"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(AcmeDirectory?, IDomainResult)> ConfigureClient(string url) {
|
||||
#region ConfigureClient
|
||||
public async Task<IDomainResult> ConfigureClient(Guid sessionId, string url) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_httpClient.BaseAddress ??= new Uri(url);
|
||||
|
||||
var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null, null, null, null);
|
||||
if (!getAcmeDirectoryResult.IsSuccess)
|
||||
return (null, getAcmeDirectoryResult);
|
||||
if (state.Directory == null) {
|
||||
var (directory, getAcmeDirectoryResult) = await SendAsync<AcmeDirectory>(sessionId, HttpMethod.Get, new Uri("directory", UriKind.Relative), false, null);
|
||||
if (!getAcmeDirectoryResult.IsSuccess || directory == null)
|
||||
return getAcmeDirectoryResult;
|
||||
|
||||
var result = directory?.Result;
|
||||
state.Directory = directory.Result;
|
||||
}
|
||||
|
||||
return IDomainResult.Success(result);
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "Let's Encrypt client unhandled exception");
|
||||
return IDomainResult.CriticalDependencyError<AcmeDirectory>();
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Account creation or Initialization from cache
|
||||
/// </summary>
|
||||
/// <param name="contacts"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(RegistrationCache?, IDomainResult)> Init(Uri newAccount, Uri newNonce, string[] contacts) {
|
||||
#region Init
|
||||
public async Task<IDomainResult> Init(Guid sessionId, string[] contacts, RegistrationCache? cache) {
|
||||
if (sessionId == Guid.Empty) {
|
||||
_logger.LogError("Invalid sessionId");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
try {
|
||||
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);
|
||||
var jwsService = new JwsService(accountKey);
|
||||
|
||||
if (cache != null && cache.AccountKey != null) {
|
||||
state.Cache = cache;
|
||||
accountKey.ImportCspBlob(cache.AccountKey);
|
||||
}
|
||||
else {
|
||||
// New Account request
|
||||
state.JwsService = new JwsService(accountKey);
|
||||
|
||||
var letsEncryptOrder = new Account {
|
||||
TermsOfServiceAgreed = true,
|
||||
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
||||
};
|
||||
|
||||
var (account, postAccuntResult) = await SendAsync<Account>(HttpMethod.Post, newAccount, false, letsEncryptOrder, accountKey, null, newNonce);
|
||||
if (!postAccuntResult.IsSuccess || account == null)
|
||||
return (null, postAccuntResult);
|
||||
|
||||
// Probably non necessary here
|
||||
// jwsService.SetKeyId(account.Result.Location.ToString());
|
||||
var (account, postAccountResult) = await SendAsync<Account>(sessionId, HttpMethod.Post, state.Directory.NewAccount, false, letsEncryptOrder);
|
||||
state.JwsService.SetKeyId(account.Result.Location.ToString());
|
||||
|
||||
if (account.Result.Status != "valid") {
|
||||
_logger.LogError($"Account status is not valid, was: {account.Result.Status} \r\n {account.ResponseText}");
|
||||
return IDomainResult.Failed<RegistrationCache>();
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
var cache = new RegistrationCache {
|
||||
state.Cache = new RegistrationCache {
|
||||
Location = account.Result.Location,
|
||||
AccountKey = accountKey.ExportCspBlob(true),
|
||||
Id = account.Result.Id,
|
||||
Key = account.Result.Key
|
||||
};
|
||||
}
|
||||
|
||||
return IDomainResult.Success(cache);
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public RegistrationCache? GetRegistrationCache(Guid sessionId) {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
return state.Cache;
|
||||
}
|
||||
|
||||
#region GetTermsOfService
|
||||
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
||||
|
||||
if (state.Directory == null) {
|
||||
return IDomainResult.Failed<string?>();
|
||||
}
|
||||
|
||||
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<RegistrationCache>(message);
|
||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Create new Certificate Order. In case you want the wildcard-certificate you must select dns-01 challange.
|
||||
/// <para>
|
||||
/// Available challange types:
|
||||
/// <list type="number">
|
||||
/// <item>dns-01</item>
|
||||
/// <item>http-01</item>
|
||||
/// <item>tls-alpn-01</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="hostnames"></param>
|
||||
/// <param name="challengeType"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<((Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?), IDomainResult)> NewOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames, string challengeType) {
|
||||
#region NewOrder
|
||||
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
||||
try {
|
||||
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
accountKey.ImportCspBlob(accountKeyBytes);
|
||||
|
||||
var jwsService = new JwsService(accountKey);
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||
|
||||
var currentOrder = default(Order);
|
||||
var results = new Dictionary<string, string>();
|
||||
var challenges = new List<AuthorizationChallenge>();
|
||||
state.Challenges.Clear();
|
||||
|
||||
var letsEncryptOrder = new Order {
|
||||
Expires = DateTime.UtcNow.AddDays(2),
|
||||
@ -157,132 +182,97 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
var (order, postNewOrderResult) = await SendAsync<Order>(HttpMethod.Post, newOrder, false, letsEncryptOrder, accountKey, location, newNonce);
|
||||
var (order, postNewOrderResult) = await SendAsync<Order>(sessionId, HttpMethod.Post, state.Directory.NewOrder, false, letsEncryptOrder);
|
||||
if (!postNewOrderResult.IsSuccess) {
|
||||
return ((null, null, null), postNewOrderResult);
|
||||
return (null, postNewOrderResult);
|
||||
}
|
||||
|
||||
if (order.Result.Status == "ready")
|
||||
return IDomainResult.Success((currentOrder, results, challenges));
|
||||
return IDomainResult.Success(new Dictionary<string, string>());
|
||||
|
||||
if (order.Result.Status != "pending") {
|
||||
_logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
|
||||
return IDomainResult.Failed<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>();
|
||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||
}
|
||||
|
||||
currentOrder = order.Result;
|
||||
state.CurrentOrder = order.Result;
|
||||
|
||||
|
||||
foreach (var item in currentOrder.Authorizations) {
|
||||
|
||||
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, item, true, null, accountKey, location, newNonce);
|
||||
var results = new Dictionary<string, string>();
|
||||
foreach (var item in order.Result.Authorizations) {
|
||||
var (challengeResponse, postAuthorisationChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(sessionId, HttpMethod.Post, item, true, null);
|
||||
if (!postAuthorisationChallengeResult.IsSuccess) {
|
||||
return ((null, null, null), postAuthorisationChallengeResult);
|
||||
return (null, postAuthorisationChallengeResult);
|
||||
}
|
||||
|
||||
if (challengeResponse.Result.Status == "valid")
|
||||
continue;
|
||||
|
||||
if (challengeResponse.Result.Status != "pending") {
|
||||
_logger.LogError($"Expected autorization status 'pending', but got: {currentOrder.Status} \r\n {challengeResponse.ResponseText}");
|
||||
return IDomainResult.Failed<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>();
|
||||
_logger.LogError($"Expected authorization status 'pending', but got: {order.Result.Status} \r\n {challengeResponse.ResponseText}");
|
||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||
}
|
||||
|
||||
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
|
||||
challenges.Add(challenge);
|
||||
state.Challenges.Add(challenge);
|
||||
|
||||
var keyToken = jwsService.GetKeyAuthorization(challenge.Token);
|
||||
var keyToken = state.JwsService.GetKeyAuthorization(challenge.Token);
|
||||
|
||||
switch (challengeType) {
|
||||
|
||||
// A client fulfills this challenge by constructing a key authorization
|
||||
// from the "token" value provided in the challenge and the client's
|
||||
// account key. The client then computes the SHA-256 digest [FIPS180-4]
|
||||
// of the key authorization.
|
||||
//
|
||||
// The record provisioned to the DNS contains the base64url encoding of
|
||||
// this digest.
|
||||
|
||||
case "dns-01": {
|
||||
case "dns-01":
|
||||
using (var sha256 = SHA256.Create()) {
|
||||
var dnsToken = jwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
||||
var dnsToken = state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
||||
results[challengeResponse.Result.Identifier.Value] = dnsToken;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// A client fulfills this challenge by constructing a key authorization
|
||||
// from the "token" value provided in the challenge and the client's
|
||||
// account key. The client then provisions the key authorization as a
|
||||
// resource on the HTTP server for the domain in question.
|
||||
//
|
||||
// The path at which the resource is provisioned is comprised of the
|
||||
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
|
||||
// value in the challenge. The value of the resource MUST be the ASCII
|
||||
// representation of the key authorization.
|
||||
|
||||
case "http-01": {
|
||||
case "http-01":
|
||||
results[challengeResponse.Result.Identifier.Value] = keyToken;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: reurn challenges
|
||||
return IDomainResult.Success((currentOrder, results, challenges));
|
||||
return IDomainResult.Success(results);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<(Order?, Dictionary<string, string>?, List<AuthorizationChallenge>?)>(message);
|
||||
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public async Task<IDomainResult> CompleteChallenges(Uri newNonce, byte[] accountKeyBytes, string location, Order currentOrder, List<AuthorizationChallenge> challenges) {
|
||||
#region CompleteChallenges
|
||||
public async Task<IDomainResult> CompleteChallenges(Guid sessionId) {
|
||||
try {
|
||||
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
accountKey.ImportCspBlob(accountKeyBytes);
|
||||
var jwsService = new JwsService(accountKey);
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||
|
||||
if (currentOrder?.Identifiers == null) {
|
||||
if (state.CurrentOrder?.Identifiers == null) {
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
for (var index = 0; index < challenges.Count; index++) {
|
||||
|
||||
var challenge = challenges[index];
|
||||
|
||||
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 = jwsService.GetKeyAuthorization(challenge.Token);
|
||||
//var (result, responseText) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, authorizeChallenge, token);
|
||||
case "dns-01":
|
||||
authorizeChallenge.KeyAuthorization = state.JwsService.GetKeyAuthorization(challenge.Token);
|
||||
break;
|
||||
|
||||
case "http-01":
|
||||
break;
|
||||
}
|
||||
|
||||
case "http-01": {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(HttpMethod.Post, challenge.Url, false, "{}", accountKey, location, newNonce);
|
||||
var (authChallenge, postAuthChallengeResult) = await SendAsync<AuthorizationChallengeResponse>(sessionId, HttpMethod.Post, challenge.Url, false, "{}");
|
||||
if (!postAuthChallengeResult.IsSuccess) {
|
||||
return postAuthChallengeResult;
|
||||
}
|
||||
@ -291,7 +281,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
break;
|
||||
|
||||
if (authChallenge.Result.Status != "pending") {
|
||||
_logger.LogError($"Failed autorization of {currentOrder.Identifiers[index].Value} \r\n {authChallenge.ResponseText}");
|
||||
_logger.LogError($"Challenge failed with status {authChallenge.Result.Status} \r\n {authChallenge.ResponseText}");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
@ -311,20 +301,15 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="hostnames"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(Order?, IDomainResult)> GetOrder(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, string location, string[] hostnames) {
|
||||
|
||||
#region GetOrder
|
||||
public async Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames) {
|
||||
try {
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
accountKey.ImportCspBlob(accountKeyBytes);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
var letsEncryptOrder = new Order {
|
||||
Expires = DateTime.UtcNow.AddDays(2),
|
||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
||||
@ -333,48 +318,32 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
}).ToArray()
|
||||
};
|
||||
|
||||
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, newOrder, false, letsEncryptOrder, accountKey, location, newNonce);
|
||||
var (order, postOrderResult) = await SendAsync<Order>(sessionId, HttpMethod.Post, state.Directory.NewOrder, false, letsEncryptOrder);
|
||||
if (!postOrderResult.IsSuccess)
|
||||
return (null, postOrderResult);
|
||||
return postOrderResult;
|
||||
|
||||
var currentOrder = order.Result;
|
||||
state.CurrentOrder = order.Result;
|
||||
|
||||
return IDomainResult.Success(currentOrder);
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<Order?>(message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="subject"></param>
|
||||
/// <returns>Cert and Private key</returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public async Task<(Dictionary<string, CertificateCache>?, IDomainResult)> GetCertificate(Uri newOrder, Uri newNonce, byte[] accountKeyBytes, Order currentOrder, string location, string [] subjects) {
|
||||
|
||||
#region GetCertificates
|
||||
public async Task<IDomainResult> GetCertificate(Guid sessionId, string subject) {
|
||||
try {
|
||||
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
accountKey.ImportCspBlob(accountKeyBytes);
|
||||
|
||||
var jwsService = new JwsService(accountKey);
|
||||
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
||||
|
||||
var cachedCerts = new Dictionary<string, CertificateCache>();
|
||||
|
||||
|
||||
foreach (var subject in subjects) {
|
||||
|
||||
|
||||
if (currentOrder == null) {
|
||||
return IDomainResult.Failed<Dictionary<string, CertificateCache>>();
|
||||
if (state.CurrentOrder == null) {
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
var key = new RSACryptoServiceProvider(4096);
|
||||
@ -382,34 +351,32 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
var san = new SubjectAlternativeNameBuilder();
|
||||
foreach (var host in currentOrder.Identifiers)
|
||||
foreach (var host in state.CurrentOrder.Identifiers)
|
||||
san.AddDnsName(host.Value);
|
||||
|
||||
csr.CertificateExtensions.Add(san.Build());
|
||||
|
||||
var letsEncryptOrder = new FinalizeRequest {
|
||||
Csr = 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(newOrder, newNonce, accountKeyBytes, location, currentOrder.Identifiers.Select(x => x.Value).ToArray());
|
||||
await GetOrder(sessionId, state.CurrentOrder.Identifiers.Select(x => x.Value).ToArray());
|
||||
|
||||
if (currentOrder.Status == "ready") {
|
||||
var (order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, currentOrder.Finalize, false, letsEncryptOrder, accountKey, location, newNonce);
|
||||
if (state.CurrentOrder.Status == "ready") {
|
||||
var (order, postOrderResult) = await SendAsync<Order>(sessionId, HttpMethod.Post, state.CurrentOrder.Finalize, false, letsEncryptOrder);
|
||||
if (!postOrderResult.IsSuccess || order?.Result == null)
|
||||
return (null, postOrderResult);
|
||||
|
||||
return postOrderResult;
|
||||
|
||||
if (order.Result.Status == "processing") {
|
||||
(order, postOrderResult) = await SendAsync<Order>(HttpMethod.Post, currentOrder.Location, true, null, accountKey, location, newNonce);
|
||||
(order, postOrderResult) = await SendAsync<Order>(sessionId, HttpMethod.Post, state.CurrentOrder.Location, true, null);
|
||||
if (!postOrderResult.IsSuccess || order?.Result == null)
|
||||
return (null, postOrderResult);
|
||||
return postOrderResult;
|
||||
}
|
||||
|
||||
if (order.Result.Status == "valid") {
|
||||
@ -423,64 +390,209 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
var (pem, postPemResult) = await SendAsync<string>(HttpMethod.Post, certificateUrl, true, null, accountKey, location, newNonce);
|
||||
var (pem, postPemResult) = await SendAsync<string>(sessionId, HttpMethod.Post, certificateUrl, true, null);
|
||||
if (!postPemResult.IsSuccess || pem?.Result == null)
|
||||
return (null, postPemResult);
|
||||
|
||||
|
||||
|
||||
cachedCerts.Add(subject, new CertificateCache {
|
||||
Cert = pem.Result,
|
||||
Private = key.ExportCspBlob(true)
|
||||
});
|
||||
|
||||
//var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
||||
return postPemResult;
|
||||
|
||||
if (state.Cache == null) {
|
||||
_logger.LogError($"{nameof(state.Cache)} is null");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
return IDomainResult.Success(cachedCerts);
|
||||
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||
state.Cache.CachedCerts[subject] = new CertificateCache {
|
||||
Cert = pem.Result,
|
||||
Private = key.ExportCspBlob(true)
|
||||
};
|
||||
|
||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<Dictionary<string, CertificateCache>?>(message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region TryGetCachedCertificate
|
||||
public (CachedCertificateResult?, IDomainResult) TryGetCachedCertificate(Guid sessionId, string subject) {
|
||||
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
var certRes = new CachedCertificateResult();
|
||||
if (state.Cache != null && state.Cache.TryGetCachedCertificate(subject, out certRes)) {
|
||||
return IDomainResult.Success(certRes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task<IDomainResult> KeyChange() {
|
||||
return IDomainResult.Failed<CachedCertificateResult?>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
public Task<IDomainResult> KeyChange(Guid sessionId) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task<IDomainResult> RevokeCertificate() {
|
||||
public Task<IDomainResult> RevokeCertificate(Guid sessionId) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
#region SendAsync
|
||||
/// <summary>
|
||||
/// Request New Nonce to be able to start POST requests
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="isPostAsGet"></param>
|
||||
/// <param name="requestModel"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(string?, IDomainResult)> NewNonce(Uri newNonce) {
|
||||
//private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(Guid sessionId, HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel) {
|
||||
// try {
|
||||
// var state = GetOrCreateState(sessionId);
|
||||
|
||||
// _logger.LogInformation($"Executing {nameof(SendAsync)}...");
|
||||
|
||||
// var request = new HttpRequestMessage(method, uri);
|
||||
|
||||
// if (uri.OriginalString != "directory") {
|
||||
// var (nonce, newNonceResult) = await NewNonce(sessionId);
|
||||
// if (!newNonceResult.IsSuccess || nonce == null) {
|
||||
// return (null, newNonceResult);
|
||||
// }
|
||||
|
||||
// state.Nonce = nonce;
|
||||
// }
|
||||
// else {
|
||||
// state.Nonce = default;
|
||||
// }
|
||||
|
||||
// if (requestModel != null || isPostAsGet) {
|
||||
// var jwsHeader = new JwsHeader {
|
||||
// Url = uri,
|
||||
// };
|
||||
|
||||
// if (state.Nonce != null)
|
||||
// jwsHeader.Nonce = state.Nonce;
|
||||
|
||||
// var encodedMessage = isPostAsGet
|
||||
// ? state.JwsService.Encode(jwsHeader)
|
||||
// : state.JwsService.Encode(requestModel, jwsHeader);
|
||||
|
||||
// var json = encodedMessage.ToJson();
|
||||
|
||||
// request.Content = new StringContent(json);
|
||||
|
||||
// var requestType = "application/json";
|
||||
// if (method == HttpMethod.Post)
|
||||
// requestType = "application/jose+json";
|
||||
|
||||
// request.Content.Headers.Remove("Content-Type");
|
||||
// request.Content.Headers.Add("Content-Type", requestType);
|
||||
// }
|
||||
|
||||
// var response = await _httpClient.SendAsync(request);
|
||||
|
||||
// if (method == HttpMethod.Post)
|
||||
// state.Nonce = response.Headers.GetValues("Replay-Nonce").First();
|
||||
|
||||
// var responseText = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
|
||||
// throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
||||
|
||||
// if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
|
||||
// return IDomainResult.Success(new SendResult<TResult> {
|
||||
// Result = (TResult)(object)responseText
|
||||
// });
|
||||
// }
|
||||
|
||||
// var responseContent = responseText.ToObject<TResult>();
|
||||
|
||||
// if (responseContent is IHasLocation ihl) {
|
||||
// if (response.Headers.Location != null)
|
||||
// ihl.Location = response.Headers.Location;
|
||||
// }
|
||||
|
||||
// return IDomainResult.Success(new SendResult<TResult> {
|
||||
// Result = responseContent,
|
||||
// ResponseText = responseText
|
||||
// });
|
||||
|
||||
// }
|
||||
// catch (Exception ex) {
|
||||
// var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
// _logger.LogError(ex, message);
|
||||
// return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
|
||||
// }
|
||||
//}
|
||||
|
||||
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(
|
||||
Guid sessionId,
|
||||
HttpMethod method,
|
||||
Uri uri,
|
||||
bool isPostAsGet,
|
||||
object? requestModel
|
||||
) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
|
||||
|
||||
var request = new HttpRequestMessage(method, uri);
|
||||
await HandleNonceAsync(sessionId, uri, state);
|
||||
|
||||
if (requestModel != null || isPostAsGet) {
|
||||
var jwsHeader = CreateJwsHeader(uri, state.Nonce);
|
||||
var json = EncodeMessage(isPostAsGet, requestModel, state, jwsHeader);
|
||||
PrepareRequestContent(request, json, method);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
await UpdateStateNonceIfNeededAsync(response, state, method);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
await HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var result = ProcessResponseContent<TResult>(response, responseText);
|
||||
return IDomainResult.Success(result);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
const string message = "Let's Encrypt client unhandled exception";
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
||||
if (uri.OriginalString != "directory") {
|
||||
var (nonce, newNonceResult) = await NewNonce(sessionId);
|
||||
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||
throw new InvalidOperationException("Failed to retrieve nonce.");
|
||||
}
|
||||
state.Nonce = nonce;
|
||||
}
|
||||
else {
|
||||
state.Nonce = default;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
||||
|
||||
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, newNonce));
|
||||
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
|
||||
if (state.Directory == null)
|
||||
IDomainResult.Failed();
|
||||
|
||||
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
||||
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
@ -490,98 +602,62 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main method used to send data to LetsEncrypt
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="requestModel"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<(SendResult<TResult>?, IDomainResult)> SendAsync<TResult>(HttpMethod method, Uri uri, bool isPostAsGet, object? requestModel, RSACryptoServiceProvider? accountKey, string? location, Uri? newNonce) {
|
||||
try {
|
||||
var _nonce = default(string?);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(SendAsync)}...");
|
||||
|
||||
var request = new HttpRequestMessage(method, uri);
|
||||
|
||||
if (uri.OriginalString != "directory") {
|
||||
var (nonce, newNonceResult) = await NewNonce(newNonce);
|
||||
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||
return (null, newNonceResult);
|
||||
}
|
||||
|
||||
_nonce = nonce;
|
||||
}
|
||||
|
||||
if (requestModel != null || isPostAsGet) {
|
||||
|
||||
if (accountKey == null)
|
||||
return IDomainResult.Failed<SendResult<TResult>?>();
|
||||
|
||||
var jwsService = new JwsService(accountKey);
|
||||
if(location != null)
|
||||
jwsService.SetKeyId(location);
|
||||
|
||||
var jwsHeader = new JwsHeader {
|
||||
private JwsHeader CreateJwsHeader(Uri uri, string? nonce) {
|
||||
return new JwsHeader {
|
||||
Url = uri,
|
||||
Nonce = nonce
|
||||
};
|
||||
|
||||
if (_nonce != null)
|
||||
jwsHeader.Nonce = _nonce;
|
||||
|
||||
var encodedMessage = isPostAsGet
|
||||
? jwsService.Encode(jwsHeader)
|
||||
: jwsService.Encode(requestModel, jwsHeader);
|
||||
|
||||
var json = encodedMessage.ToJson();
|
||||
|
||||
request.Content = new StringContent(json);
|
||||
|
||||
var requestType = "application/json";
|
||||
if (method == HttpMethod.Post)
|
||||
requestType = "application/jose+json";
|
||||
|
||||
request.Content.Headers.Remove("Content-Type");
|
||||
request.Content.Headers.Add("Content-Type", requestType);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
|
||||
return isPostAsGet
|
||||
? state.JwsService.Encode(jwsHeader).ToJson()
|
||||
: state.JwsService.Encode(requestModel, jwsHeader).ToJson();
|
||||
}
|
||||
|
||||
if (method == HttpMethod.Post)
|
||||
_nonce = response.Headers.GetValues("Replay-Nonce").First();
|
||||
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.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
}
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
private async Task UpdateStateNonceIfNeededAsync(HttpResponseMessage response, State state, HttpMethod method) {
|
||||
if (method == HttpMethod.Post && response.Headers.Contains("Replay-Nonce")) {
|
||||
state.Nonce = response.Headers.GetValues("Replay-Nonce").First();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
|
||||
private async Task HandleProblemResponseAsync(HttpResponseMessage response, string responseText) {
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
|
||||
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
||||
}
|
||||
}
|
||||
|
||||
private SendResult<TResult> ProcessResponseContent<TResult>(HttpResponseMessage response, string responseText) {
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
|
||||
return IDomainResult.Success(new SendResult<TResult> {
|
||||
return new SendResult<TResult> {
|
||||
Result = (TResult)(object)responseText
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var responseContent = responseText.ToObject<TResult>();
|
||||
|
||||
if (responseContent is IHasLocation ihl) {
|
||||
if (response.Headers.Location != null)
|
||||
if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
|
||||
ihl.Location = response.Headers.Location;
|
||||
}
|
||||
|
||||
return IDomainResult.Success(new SendResult<TResult> {
|
||||
return new SendResult<TResult> {
|
||||
Result = responseContent,
|
||||
ResponseText = responseText
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
#endregion
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<SendResult<TResult>?>(message);
|
||||
}
|
||||
private class State {
|
||||
public AcmeDirectory? Directory { get; set; }
|
||||
public JwsService? JwsService { get; set; }
|
||||
public Order? CurrentOrder { get; set; }
|
||||
public List<AuthorizationChallenge> Challenges { get; } = new List<AuthorizationChallenge>();
|
||||
public string? Nonce { get; set; }
|
||||
public RegistrationCache? Cache { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +1,21 @@
|
||||
namespace LetsEncryptServer {
|
||||
namespace MaksIT.LetsEncryptServer {
|
||||
|
||||
public class Site {
|
||||
public required string Name { get; set; }
|
||||
public required string[] Hosts { get; set; }
|
||||
public required string Challenge { get; set; }
|
||||
}
|
||||
|
||||
public class Customer {
|
||||
private string? _id;
|
||||
public string Id {
|
||||
get => _id ?? string.Empty;
|
||||
set => _id = value;
|
||||
}
|
||||
|
||||
public bool Active { get; set; }
|
||||
public string[]? Contacts { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public Site[]? Sites { get; set; }
|
||||
public class SSHClientConfing {
|
||||
public required string User { get; set; }
|
||||
public required string Key { get; set; }
|
||||
}
|
||||
|
||||
public class Server {
|
||||
public required string Address { get; set; }
|
||||
public required string PrivateKey { get; set; }
|
||||
public required string Path { get; set; }
|
||||
public required string Ip { get; set; }
|
||||
public required int Port { get; set; }
|
||||
public string Path { get; set; }
|
||||
public required SSHClientConfing SSH { get; set; }
|
||||
}
|
||||
|
||||
public class Configuration {
|
||||
public required string Production { get; set; }
|
||||
public required string Staging { get; set; }
|
||||
public required bool DevMode { get; set; }
|
||||
public required Server Server { get; set; }
|
||||
|
||||
public Customer[]? Customers { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,225 +1,115 @@
|
||||
using DomainResults.Mvc;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Models.Responses;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using MaksIT.LetsEncryptServer.Models.Requests;
|
||||
using Microsoft.AspNetCore.Identity.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
|
||||
namespace LetsEncryptServer.Controllers;
|
||||
using DomainResults.Mvc;
|
||||
|
||||
public class LetsEncryptSession {
|
||||
public RegistrationCache? RegistrationCache { get; set; }
|
||||
public Order? CurrentOrder { get; set; }
|
||||
public List<AuthorizationChallenge>? Challenges { get; set; }
|
||||
public string[] Hostnames { get; set; }
|
||||
}
|
||||
using MaksIT.LetsEncryptServer.Models.Requests;
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class CertsFlowController : ControllerBase {
|
||||
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ILetsEncryptService _letsEncryptService;
|
||||
|
||||
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
||||
private readonly string _certPath = Path.Combine();
|
||||
|
||||
MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions {
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
|
||||
SlidingExpiration = TimeSpan.FromMinutes(2)
|
||||
};
|
||||
private readonly IOptions<Configuration> _appSettings;
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
|
||||
public CertsFlowController(
|
||||
IOptions<Configuration> appSettings,
|
||||
IMemoryCache memoryCache,
|
||||
ILetsEncryptService letsEncryptService
|
||||
ICertsFlowService certsFlowService
|
||||
) {
|
||||
_memoryCache = memoryCache;
|
||||
_appSettings = appSettings.Value;
|
||||
_letsEncryptService = letsEncryptService;
|
||||
|
||||
if (!Directory.Exists(_acmePath))
|
||||
Directory.CreateDirectory(_acmePath);
|
||||
|
||||
Console.WriteLine(_acmePath);
|
||||
_appSettings = appSettings;
|
||||
_certsFlowService = certsFlowService;
|
||||
}
|
||||
|
||||
[HttpGet("[action]")]
|
||||
public async Task<IActionResult> TermsOfService() {
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
return Ok(config.Meta.TermsOfService);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initialize certificate flow session
|
||||
/// </summary>
|
||||
/// <returns>sessionId</returns>
|
||||
[HttpPost("[action]")]
|
||||
public async Task<IActionResult> Init([FromBody] InitRequest requestData) {
|
||||
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
var (cache, cacheResult) = await _letsEncryptService.Init(config.NewAccount, config.NewNonce, requestData.Contacts);
|
||||
if(!cacheResult.IsSuccess || cache == null)
|
||||
return cacheResult.ToActionResult();
|
||||
|
||||
var cacheData = new LetsEncryptSession {
|
||||
RegistrationCache = cache,
|
||||
};
|
||||
|
||||
var accountId = Guid.NewGuid().ToString();
|
||||
|
||||
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
|
||||
|
||||
return Ok(accountId);
|
||||
public async Task<IActionResult> ConfigureClient() {
|
||||
var result = await _certsFlowService.ConfigureClientAsync();
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
[HttpPost("[action]/{accountId}")]
|
||||
public async Task<IActionResult> NewOrder(string accountId, [FromBody] NewOrderRequest requestData) {
|
||||
|
||||
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
|
||||
if (cacheData?.RegistrationCache?.AccountKey == null)
|
||||
return BadRequest();
|
||||
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
|
||||
var (orderData, newOrderResult) = await _letsEncryptService.NewOrder(
|
||||
config.NewOrder,
|
||||
config.NewNonce,
|
||||
cacheData.RegistrationCache.AccountKey,
|
||||
cacheData.RegistrationCache.Location.ToString(),
|
||||
requestData.Hostnames,
|
||||
requestData.ChallengeType);
|
||||
|
||||
if (!newOrderResult.IsSuccess)
|
||||
return newOrderResult.ToActionResult();
|
||||
|
||||
var(currentOrder, results, challenges) = orderData;
|
||||
|
||||
if (results?.Count == 0)
|
||||
return StatusCode(500);
|
||||
|
||||
// TODO: save results to disk
|
||||
var fullPaths = new List<string>();
|
||||
foreach (var result in results) {
|
||||
string[] splitToken = result.Value.Split('.');
|
||||
|
||||
System.IO.File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), result.Value);
|
||||
|
||||
fullPaths.Add(splitToken[0]);
|
||||
[HttpGet("[action]/{sessionId}")]
|
||||
public IActionResult TermsOfService(Guid sessionId) {
|
||||
var result = _certsFlowService.GetTermsOfService(sessionId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
cacheData.CurrentOrder = currentOrder;
|
||||
cacheData.Challenges = challenges;
|
||||
cacheData.Hostnames = requestData.Hostnames;
|
||||
|
||||
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
|
||||
|
||||
return Ok(fullPaths);
|
||||
/// <summary>
|
||||
/// When new certificate session is created, create or retrieve cache data by accountId
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="accountId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns>accountId</returns>
|
||||
[HttpPost("[action]/{sessionId}/{accountId?}")]
|
||||
public async Task<IActionResult> Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) {
|
||||
var resurt = await _certsFlowService.InitAsync(sessionId, accountId, requestData);
|
||||
return resurt.ToActionResult();
|
||||
}
|
||||
|
||||
[HttpPut("[action]/{accountId}")]
|
||||
public async Task<IActionResult> CompleteChallenges(string accountId) {
|
||||
|
||||
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
|
||||
if (cacheData?.RegistrationCache?.AccountKey == null)
|
||||
return BadRequest();
|
||||
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
var challengeResult = await _letsEncryptService.CompleteChallenges(
|
||||
config.NewNonce,
|
||||
cacheData.RegistrationCache.AccountKey,
|
||||
cacheData.RegistrationCache.Location.ToString(),
|
||||
cacheData.CurrentOrder,
|
||||
cacheData.Challenges
|
||||
);
|
||||
|
||||
if (!challengeResult.IsSuccess)
|
||||
return challengeResult.ToActionResult();
|
||||
|
||||
return Ok();
|
||||
/// <summary>
|
||||
/// After account initialization create new order request
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("[action]/{sessionId}")]
|
||||
public async Task<IActionResult> NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) {
|
||||
var result = await _certsFlowService.NewOrderAsync(sessionId, requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
[HttpGet("[action]/{accountId}")]
|
||||
public async Task<IActionResult> GetOrder(string accountId) {
|
||||
|
||||
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
|
||||
if (cacheData?.RegistrationCache?.AccountKey == null)
|
||||
return BadRequest();
|
||||
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
|
||||
var (currentOrder, currentOrderResult) = await _letsEncryptService.GetOrder(
|
||||
config.NewOrder,
|
||||
config.NewNonce,
|
||||
cacheData.RegistrationCache.AccountKey,
|
||||
cacheData.RegistrationCache.Location.ToString(),
|
||||
cacheData.Hostnames
|
||||
);
|
||||
|
||||
if(!currentOrderResult.IsSuccess)
|
||||
return currentOrderResult.ToActionResult();
|
||||
|
||||
cacheData.CurrentOrder = currentOrder;
|
||||
|
||||
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
|
||||
|
||||
return Ok();
|
||||
/// <summary>
|
||||
/// After new order request complete challenges
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("[action]/{sessionId}")]
|
||||
public async Task<IActionResult> CompleteChallenges(Guid sessionId) {
|
||||
var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
[HttpPost("[action]/{accountId}")]
|
||||
public async Task<IActionResult> GetCertificate(string accountId) {
|
||||
|
||||
var cacheData = (LetsEncryptSession?)_memoryCache.Get(accountId);
|
||||
if (cacheData?.RegistrationCache?.AccountKey == null)
|
||||
return BadRequest();
|
||||
|
||||
var (config, configResult) = await _letsEncryptService.ConfigureClient("https://acme-staging-v02.api.letsencrypt.org/directory");
|
||||
if (!configResult.IsSuccess || config == null)
|
||||
return configResult.ToActionResult();
|
||||
|
||||
var (cachedCerts, certsResult) = await _letsEncryptService.GetCertificate(
|
||||
config.NewOrder,
|
||||
config.NewNonce,
|
||||
cacheData.RegistrationCache.AccountKey,
|
||||
cacheData.CurrentOrder,
|
||||
cacheData.RegistrationCache.Location.ToString(),
|
||||
cacheData.Hostnames
|
||||
);
|
||||
|
||||
if (!certsResult.IsSuccess || cachedCerts == null)
|
||||
return certsResult.ToActionResult();
|
||||
|
||||
// TODO: write certs to filesystem
|
||||
foreach (var (subject, cachedCert) in cachedCerts) {
|
||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(cachedCert.Cert));
|
||||
/// <summary>
|
||||
/// Get order status before certs retrieval
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("[action]/{sessionId}")]
|
||||
public async Task<IActionResult> GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) {
|
||||
var result = await _certsFlowService.GetOrderAsync(sessionId, requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
if (!certsResult.IsSuccess)
|
||||
return BadRequest();
|
||||
|
||||
return Ok();
|
||||
/// <summary>
|
||||
/// Download certs to local cache
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("[action]/{sessionId}")]
|
||||
public async Task<IActionResult> GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
|
||||
var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Apply certs from local cache to remote server
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="requestData"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("[action]/{sessionId}")]
|
||||
public IActionResult ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
|
||||
var result = _certsFlowService.ApplyCertificates(sessionId, requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +1,28 @@
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace LetsEncryptServer.Controllers;
|
||||
using DomainResults.Mvc;
|
||||
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route(".well-known")]
|
||||
public class WellKnownController : ControllerBase {
|
||||
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ILetsEncryptService _letsEncryptService;
|
||||
private readonly ICertsFlowServiceBase _certsFlowService;
|
||||
|
||||
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
||||
|
||||
public WellKnownController(
|
||||
IOptions<Configuration> appSettings,
|
||||
ILetsEncryptService letsEncryptService
|
||||
ICertsFlowService certsFlowService
|
||||
) {
|
||||
_appSettings = appSettings.Value;
|
||||
_letsEncryptService = letsEncryptService;
|
||||
_certsFlowService = certsFlowService;
|
||||
|
||||
if (!Directory.Exists(_acmePath))
|
||||
Directory.CreateDirectory(_acmePath);
|
||||
@ -27,12 +31,8 @@ public class WellKnownController : ControllerBase {
|
||||
|
||||
[HttpGet("acme-challenge/{fileName}")]
|
||||
public IActionResult AcmeChallenge(string fileName) {
|
||||
|
||||
var fileContent = System.IO.File.ReadAllText(Path.Combine(_acmePath, fileName));
|
||||
if (fileContent == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(fileContent);
|
||||
var result = _certsFlowService.AcmeChallenge(fileName);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
namespace MaksIT.LetsEncryptServer.Models.Requests {
|
||||
public class GetCertificatesRequest {
|
||||
public string[] Hostnames { get; set; }
|
||||
}
|
||||
}
|
||||
5
src/LetsEncryptServer/Models/Requests/GetOrderRequest.cs
Normal file
5
src/LetsEncryptServer/Models/Requests/GetOrderRequest.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace MaksIT.LetsEncryptServer.Models.Requests {
|
||||
public class GetOrderRequest {
|
||||
public string[] Hostnames { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,21 @@
|
||||
using MaksIT.LetsEncryptServer;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Extract configuration
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
// Configure strongly typed settings objects
|
||||
var configurationSection = configuration.GetSection("Configuration");
|
||||
var appSettings = configurationSection.Get<Configuration>() ?? throw new ArgumentNullException();
|
||||
|
||||
// Allow configurations to be available through IOptions<Configuration>
|
||||
builder.Services.Configure<Configuration>(configurationSection);
|
||||
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
@ -12,6 +26,7 @@ builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
|
||||
builder.Services.AddScoped<ICertsFlowService, CertsFlowService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
208
src/LetsEncryptServer/Services/CertsFlowService.cs
Normal file
208
src/LetsEncryptServer/Services/CertsFlowService.cs
Normal file
@ -0,0 +1,208 @@
|
||||
using System.Text;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using DomainResults.Common;
|
||||
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using MaksIT.LetsEncryptServer.Models.Requests;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
public interface ICertsFlowServiceBase {
|
||||
(string?, IDomainResult) AcmeChallenge(string fileName);
|
||||
}
|
||||
|
||||
public interface ICertsFlowService : ICertsFlowServiceBase {
|
||||
Task<(Guid?, IDomainResult)> ConfigureClientAsync();
|
||||
(string?, IDomainResult) GetTermsOfService(Guid sessionId);
|
||||
Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
|
||||
Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, NewOrderRequest requestData);
|
||||
Task<IDomainResult> CompleteChallengesAsync(Guid sessionId);
|
||||
Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
|
||||
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||
(Dictionary<string, string>?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData);
|
||||
}
|
||||
|
||||
public class CertsFlowService : ICertsFlowService {
|
||||
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ILogger<CertsFlowService> _logger;
|
||||
private readonly ILetsEncryptService _letsEncryptService;
|
||||
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
||||
|
||||
public CertsFlowService(
|
||||
IOptions<Configuration> appSettings,
|
||||
ILogger<CertsFlowService> logger,
|
||||
ILetsEncryptService letsEncryptService
|
||||
) {
|
||||
_appSettings = appSettings.Value;
|
||||
_logger = logger;
|
||||
_letsEncryptService = letsEncryptService;
|
||||
|
||||
if (!Directory.Exists(_acmePath))
|
||||
Directory.CreateDirectory(_acmePath);
|
||||
}
|
||||
|
||||
public async Task<(Guid?, IDomainResult)> ConfigureClientAsync() {
|
||||
var sessionId = Guid.NewGuid();
|
||||
|
||||
var url = _appSettings.DevMode
|
||||
? _appSettings.Staging
|
||||
: _appSettings.Production;
|
||||
|
||||
var result = await _letsEncryptService.ConfigureClient(sessionId, url);
|
||||
if (!result.IsSuccess)
|
||||
return (null, result);
|
||||
|
||||
return IDomainResult.Success(sessionId);
|
||||
}
|
||||
|
||||
public (string?, IDomainResult) GetTermsOfService(Guid sessionId) {
|
||||
var (terms, getTermsResult) = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||
if (!getTermsResult.IsSuccess || terms == null)
|
||||
return (null, getTermsResult);
|
||||
|
||||
return IDomainResult.Success<string>(terms);
|
||||
}
|
||||
|
||||
public async Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData) {
|
||||
var cache = default(RegistrationCache);
|
||||
if (accountId == null) {
|
||||
accountId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
var result = await _letsEncryptService.Init(sessionId, requestData.Contacts, cache);
|
||||
return result.IsSuccess ? IDomainResult.Success<Guid>(accountId.Value) : (null, result);
|
||||
}
|
||||
|
||||
public async Task<(List<string>?, IDomainResult)> NewOrderAsync(Guid sessionId, NewOrderRequest requestData) {
|
||||
var (results, newOrderResult) = await _letsEncryptService.NewOrder(sessionId, requestData.Hostnames, requestData.ChallengeType);
|
||||
if (!newOrderResult.IsSuccess || results == null)
|
||||
return (null, newOrderResult);
|
||||
|
||||
var challenges = new List<string>();
|
||||
foreach (var result in results) {
|
||||
string[] splitToken = result.Value.Split('.');
|
||||
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), result.Value);
|
||||
challenges.Add(splitToken[0]);
|
||||
}
|
||||
|
||||
return IDomainResult.Success(challenges);
|
||||
}
|
||||
|
||||
public async Task<IDomainResult> CompleteChallengesAsync(Guid sessionId) {
|
||||
return await _letsEncryptService.CompleteChallenges(sessionId);
|
||||
}
|
||||
|
||||
public async Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData) {
|
||||
return await _letsEncryptService.GetOrder(sessionId, requestData.Hostnames);
|
||||
}
|
||||
|
||||
public async Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData) {
|
||||
foreach (var subject in requestData.Hostnames) {
|
||||
var result = await _letsEncryptService.GetCertificate(sessionId, subject);
|
||||
if (!result.IsSuccess)
|
||||
return result;
|
||||
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
|
||||
public (Dictionary<string, string>?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData) {
|
||||
var haproxyHelper = new HaproxyCertificateUpdater();
|
||||
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
foreach (var subject in requestData.Hostnames) {
|
||||
|
||||
var (cert, getCertResult) = _letsEncryptService.TryGetCachedCertificate(sessionId, subject);
|
||||
if (!getCertResult.IsSuccess || cert == null)
|
||||
return (null, getCertResult);
|
||||
|
||||
//haproxyHelper.ApplyCertificates(subject, cert.Certificate, cert.PrivateKeyPem);
|
||||
var content = $"{cert.Certificate}\n{cert.PrivateKeyPem}";
|
||||
result.Add(subject, content);
|
||||
}
|
||||
|
||||
return IDomainResult.Success(result);
|
||||
}
|
||||
|
||||
|
||||
public (string?, IDomainResult) AcmeChallenge(string fileName) {
|
||||
|
||||
//var currentDate = DateTime.Now;
|
||||
|
||||
//foreach (var file in Directory.GetFiles(_acmePath)) {
|
||||
// var creationTime = System.IO.File.GetCreationTime(file);
|
||||
|
||||
// // Calculate the time difference
|
||||
// var timeDifference = currentDate - creationTime;
|
||||
|
||||
// // If the file is older than 1 day, delete it
|
||||
// if (timeDifference.TotalDays > 1) {
|
||||
// File.Delete(file);
|
||||
// _logger.LogInformation($"Deleted file: {file}");
|
||||
// }
|
||||
//}
|
||||
|
||||
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
|
||||
if (fileContent == null)
|
||||
return IDomainResult.NotFound<string?>();
|
||||
|
||||
return IDomainResult.Success(fileContent);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public class HaproxyCertificateUpdater {
|
||||
private readonly string haproxySocketAddress = "192.168.1.4";
|
||||
private readonly int haproxySocketPort = 9999;
|
||||
|
||||
public void ApplyCertificates(string subject, string certPem, string keyPem) {
|
||||
if (string.IsNullOrEmpty(certPem) || string.IsNullOrEmpty(keyPem)) {
|
||||
Console.WriteLine($"Certificate or key for {subject} is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
string certFileName = $"/etc/haproxy/certs/{subject}.pem";
|
||||
string fullCert = $"{certPem}\n{keyPem}";
|
||||
|
||||
try {
|
||||
SendCommand($"new ssl cert {certFileName}");
|
||||
SendCommand($"set ssl cert {certFileName} <<\n{fullCert}\n");
|
||||
SendCommand($"commit ssl cert {certFileName}");
|
||||
|
||||
Console.WriteLine($"Certificate for {subject} updated successfully");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Console.WriteLine($"Exception while updating certificate for {subject}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SendCommand(string command) {
|
||||
using (var client = new TcpClient(haproxySocketAddress, haproxySocketPort))
|
||||
using (var stream = client.GetStream())
|
||||
using (var writer = new StreamWriter(stream))
|
||||
using (var reader = new StreamReader(stream)) {
|
||||
writer.WriteLine(command);
|
||||
writer.Flush();
|
||||
|
||||
string response = reader.ReadToEnd();
|
||||
if (!response.Contains("Success")) {
|
||||
throw new Exception($"Command failed: {response}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,16 @@
|
||||
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
|
||||
"ServerPath": "/etc/haproxy/certs"
|
||||
"DevMode": true,
|
||||
|
||||
"Server": {
|
||||
"Ip": "192.168.1.4",
|
||||
"Port": 9999,
|
||||
"Path": "/etc/haproxy/certs",
|
||||
"SSH": {
|
||||
"User": "",
|
||||
"Key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user