From de0b7d1fe0b5a98de95481fc9d12eb6f21952e1f Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 16 Feb 2026 02:50:25 +0000 Subject: [PATCH] fix(dcrouter): persist proxy certificate validity dates and improve certificate status initialization --- changelog.md | 10 +++ package.json | 2 +- pnpm-lock.yaml | 10 +-- ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 71 ++++++++++++++------ ts/opsserver/handlers/certificate.handler.ts | 10 ++- ts_web/00_commitinfo_data.ts | 2 +- 7 files changed, 78 insertions(+), 29 deletions(-) diff --git a/changelog.md b/changelog.md index 9be8bb2..840c78f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-16 - 6.2.3 - fix(dcrouter) +persist proxy certificate validity dates and improve certificate status initialization + +- Bump @push.rocks/smartacme dependency from ^9.0.0 to ^9.1.3 +- Store validFrom and validUntil alongside proxy cert entries (/proxy-certs) when saving, extracting values by parsing PEM where possible +- Use stored cert entries (domain, publicKey, validUntil, validFrom) to populate certificateStatusMap at startup +- Fallback to SmartAcme /certs/ metadata and finally to parsing X.509 from stored PEM to determine expiry/issuedAt when initializing status +- Update opsserver certificate handler to parse publicKey PEM from cert-store and set expiry/issuedAt and issuer accordingly +- Adjust variable names and logging to reflect stored cert entry usage + ## 2026-02-16 - 6.2.2 - fix(certs) Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler diff --git a/package.json b/package.json index 74590f5..afccaa2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@design.estate/dees-element": "^2.1.6", "@push.rocks/projectinfo": "^5.0.2", "@push.rocks/qenv": "^6.1.3", - "@push.rocks/smartacme": "^9.0.0", + "@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdns": "^7.8.1", "@push.rocks/smartfile": "^13.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cfad29..75b523e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^6.1.3 version: 6.1.3 '@push.rocks/smartacme': - specifier: ^9.0.0 - version: 9.1.2(socks@2.8.7) + specifier: ^9.1.3 + version: 9.1.3(socks@2.8.7) '@push.rocks/smartdata': specifier: ^7.0.15 version: 7.0.15(socks@2.8.7) @@ -852,8 +852,8 @@ packages: '@push.rocks/qenv@6.1.3': resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==} - '@push.rocks/smartacme@9.1.2': - resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==} + '@push.rocks/smartacme@9.1.3': + resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==} '@push.rocks/smartarchive@4.2.4': resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==} @@ -5782,7 +5782,7 @@ snapshots: '@push.rocks/smartlog': 3.1.11 '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartacme@9.1.2(socks@2.8.7)': + '@push.rocks/smartacme@9.1.3(socks@2.8.7)': dependencies: '@apiclient.xyz/cloudflare': 7.1.0 '@peculiar/x509': 1.14.3 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0fba044..245456f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.2.2', + version: '6.2.3', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index dcba7ac..3463702 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -445,8 +445,8 @@ export class DcRouter { if (routes.length > 0 || this.options.smartProxyConfig) { console.log('Setting up SmartProxy with combined configuration'); - // Track domains loaded from cert store so we can populate certificateStatusMap after start - const loadedCertDomains: string[] = []; + // Track cert entries loaded from cert store so we can populate certificateStatusMap after start + const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = []; // Create SmartProxy configuration const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { @@ -461,14 +461,21 @@ export class DcRouter { const data = await this.storageManager.getJSON(key); if (data) { certs.push(data); - loadedCertDomains.push(data.domain); + loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom }); } } return certs; }, save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => { + let validUntil: number | undefined; + let validFrom: number | undefined; + try { + const x509 = new plugins.crypto.X509Certificate(publicKey); + validUntil = new Date(x509.validTo).getTime(); + validFrom = new Date(x509.validFrom).getTime(); + } catch { /* PEM parsing failed */ } await this.storageManager.setJSON(`/proxy-certs/${domain}`, { - domain, publicKey, privateKey, ca, + domain, publicKey, privateKey, ca, validUntil, validFrom, }); }, remove: async (domain: string) => { @@ -587,22 +594,46 @@ export class DcRouter { console.log('[DcRouter] SmartProxy started successfully'); // Populate certificateStatusMap for certs loaded from store at startup - for (const domain of loadedCertDomains) { - if (!this.certificateStatusMap.has(domain)) { - const routeNames = this.findRouteNamesForDomain(domain); + for (const entry of loadedCertEntries) { + if (!this.certificateStatusMap.has(entry.domain)) { + const routeNames = this.findRouteNamesForDomain(entry.domain); let expiryDate: string | undefined; let issuedAt: string | undefined; - try { - const cleanDomain = domain.replace(/^\*\.?/, ''); - const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`); - if (certMeta?.validUntil) { - expiryDate = new Date(certMeta.validUntil).toISOString(); - } - if (certMeta?.created) { - issuedAt = new Date(certMeta.created).toISOString(); - } - } catch { /* no metadata available */ } - this.certificateStatusMap.set(domain, { + + // Use validUntil/validFrom from stored proxy-certs data if available + if (entry.validUntil) { + expiryDate = new Date(entry.validUntil).toISOString(); + } + if (entry.validFrom) { + issuedAt = new Date(entry.validFrom).toISOString(); + } + + // Try SmartAcme /certs/ metadata as secondary source + if (!expiryDate) { + try { + const cleanDomain = entry.domain.replace(/^\*\.?/, ''); + const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`); + if (certMeta?.validUntil) { + expiryDate = new Date(certMeta.validUntil).toISOString(); + } + if (certMeta?.created && !issuedAt) { + issuedAt = new Date(certMeta.created).toISOString(); + } + } catch { /* no metadata available */ } + } + + // Fallback: parse X509 from PEM to get expiry + if (!expiryDate && entry.publicKey) { + try { + const x509 = new plugins.crypto.X509Certificate(entry.publicKey); + expiryDate = new Date(x509.validTo).toISOString(); + if (!issuedAt) { + issuedAt = new Date(x509.validFrom).toISOString(); + } + } catch { /* PEM parsing failed */ } + } + + this.certificateStatusMap.set(entry.domain, { status: 'valid', routeNames, expiryDate, @@ -611,8 +642,8 @@ export class DcRouter { }); } } - if (loadedCertDomains.length > 0) { - console.log(`[DcRouter] Populated certificate status for ${loadedCertDomains.length} store-loaded domain(s)`); + if (loadedCertEntries.length > 0) { + console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`); } console.log(`SmartProxy started with ${routes.length} routes`); diff --git a/ts/opsserver/handlers/certificate.handler.ts b/ts/opsserver/handlers/certificate.handler.ts index 106f4b7..3a53da3 100644 --- a/ts/opsserver/handlers/certificate.handler.ts +++ b/ts/opsserver/handlers/certificate.handler.ts @@ -167,8 +167,16 @@ export class CertificateHandler { issuedAt = new Date(certData.created).toISOString(); } issuer = 'smartacme-dns-01'; + } else if (certData?.publicKey) { + // certStore has the cert — parse PEM for expiry + try { + const x509 = new plugins.crypto.X509Certificate(certData.publicKey); + expiryDate = new Date(x509.validTo).toISOString(); + issuedAt = new Date(x509.validFrom).toISOString(); + } catch { /* PEM parsing failed */ } + status = 'valid'; + issuer = 'cert-store'; } else if (certData) { - // certStore has the cert (no expiry metadata) — it's loaded and serving status = 'valid'; issuer = 'cert-store'; } diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 0fba044..245456f 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '6.2.2', + version: '6.2.3', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }