From 1a47905beb74f3e7a7e57b8f86bea51c19e95d00 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 12 May 2026 20:43:34 +0700 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D1=83=20=D1=86=D0=B5=D0=BF=D0=BE=D1=87=D0=B5=D0=BA?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/services/acme-cloudflare.ts | 27 +++++++++++++++++++++++-- backend/src/services/mikrotik.ts | 13 ++---------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/backend/src/services/acme-cloudflare.ts b/backend/src/services/acme-cloudflare.ts index 1f4dd4b..c0baafb 100644 --- a/backend/src/services/acme-cloudflare.ts +++ b/backend/src/services/acme-cloudflare.ts @@ -228,6 +228,12 @@ async function ensureAcmeAccount(client: acme.Client): Promise { } } +function splitPemCertificates(pem: string): string[] { + return [...pem.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)] + .map((match) => match[0].trim()) + .filter(Boolean) +} + export async function issueCertificateWithCloudflareDns(params: { server: Server certName: string @@ -300,9 +306,12 @@ export async function issueCertificateWithCloudflareDns(params: { params.onStep?.("import") const safeBase = params.certName.replace(/[^a-zA-Z0-9._-]+/g, "_") + const chain = splitPemCertificates(certPem) + const leafPem = chain[0] ?? certPem + const chainRest = chain.slice(1) const certFile = `${safeBase}.crt` const keyFile = `${safeBase}.key` - const certRouterFile = await clientRos.uploadTextFile(certFile, certPem) + const certRouterFile = await clientRos.uploadTextFile(certFile, leafPem) const keyRouterFile = await clientRos.uploadTextFile(keyFile, privateKey.toString("utf8")) await clientRos.importCertificate({ fileName: certRouterFile, @@ -316,7 +325,21 @@ export async function issueCertificateWithCloudflareDns(params: { trusted: true, trustStore: trustStoreCsv, }) - await clientRos.applyCertificateToServices(params.certName, effectiveTrustStores) + + for (let index = 0; index < chainRest.length; index += 1) { + const isRoot = index === chainRest.length - 1 + const caName = isRoot ? `root_${params.certName}` : `root_${params.certName}_${index + 1}` + const caFile = `${safeBase}_ca_${index + 1}.crt` + const caRouterFile = await clientRos.uploadTextFile(caFile, chainRest[index]!) + await clientRos.importCertificate({ + fileName: caRouterFile, + name: caName, + trusted: true, + trustStore: trustStoreCsv, + }) + } + + await clientRos.applyCertificateToServices(params.certName) } finally { params.onStep?.("cleanup") for (const item of txtCleanups) { diff --git a/backend/src/services/mikrotik.ts b/backend/src/services/mikrotik.ts index 7b8970e..7696797 100644 --- a/backend/src/services/mikrotik.ts +++ b/backend/src/services/mikrotik.ts @@ -571,21 +571,12 @@ export class MikrotikClient { return this.post("/certificate/import", body, 60_000) } - async applyCertificateToServices( - certName: string, - trustStores: string[] = ["www", "api"], - ): Promise { - const stores = trustStores.length > 0 ? trustStores : ["www", "api"] - const targets: string[] = [] - if (stores.includes("www")) targets.push("www-ssl") - if (stores.includes("api")) targets.push("api-ssl") - if (targets.length === 0) targets.push("www-ssl") - + async applyCertificateToServices(certName: string): Promise { const body = { disabled: "no", certificate: certName, } - for (const serviceName of targets) { + for (const serviceName of ["www-ssl", "api-ssl"]) { await this.patchIpService(serviceName, body) } }