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; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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]);
|
||||
}
|
||||
|
||||
cacheData.CurrentOrder = currentOrder;
|
||||
cacheData.Challenges = challenges;
|
||||
cacheData.Hostnames = requestData.Hostnames;
|
||||
|
||||
_memoryCache.Set(accountId, cacheData, _cacheEntryOptions);
|
||||
|
||||
return Ok(fullPaths);
|
||||
[HttpGet("[action]/{sessionId}")]
|
||||
public IActionResult TermsOfService(Guid sessionId) {
|
||||
var result = _certsFlowService.GetTermsOfService(sessionId);
|
||||
return result.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>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
[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 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();
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
if (!certsResult.IsSuccess)
|
||||
return BadRequest();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
|
||||
/// <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