// sslCron.mjs import cron from "node-cron"; import axios from "axios"; import { promises as fs } from "fs"; import path from "path"; // Base URL of your SSL manager Express API // e.g. the server where you defined /dns/domains and /ssl/refresh const API_BASE = process.env.SSL_MANAGER_URL || "https://api.hestiacp.metatronhost.com"; // Logging const LOG_DIR = process.env.LOG_DIR || "/var/log/ssl-manager"; const LOG_FILE = process.env.LOG_FILE || path.join(LOG_DIR, "sslCron.log"); async function ensureLogPath() { await fs.mkdir(LOG_DIR, { recursive: true }); } function safeStringify(obj) { try { return JSON.stringify(obj); } catch { return String(obj); } } async function logLine(level, message, meta = null) { const ts = new Date().toISOString(); const line = `[${ts}] [${level}] ${message}` + (meta ? ` | ${safeStringify(meta)}` : "") + "\n"; if (level === "ERROR") console.error(line.trim()); else console.log(line.trim()); try { await ensureLogPath(); await fs.appendFile(LOG_FILE, line, "utf8"); } catch (e) { console.error( `[${ts}] [ERROR] Failed to write cron log file: ${e.message || e}` ); } } /** * Parse "notAfter" like: "Mar 11 19:08:24 2026 GMT" */ function parseNotAfter(notAfterStr) { if (!notAfterStr) return null; const d = new Date(notAfterStr); if (isNaN(d.getTime())) return null; return d; } /** * Return days left until SSL expiry from notAfter. * If invalid → return large negative number. */ function daysLeftFromSsl(notAfterStr) { const expiry = parseNotAfter(notAfterStr); if (!expiry) return -9999; const now = new Date(); const diffMs = expiry.getTime() - now.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); return diffDays; } /** * Decide if SSL is expired based on notAfter */ function isSslExpired(notAfterStr) { const expiry = parseNotAfter(notAfterStr); if (!expiry) return true; const now = new Date(); return now > expiry; } /** * One cycle: * - Fetch domain list (/dns/domains) * - For each domain, look at ssl.notAfter * - If expired -> call POST /ssl/refresh * (Your API handles: remove+add + notify reverse proxy + logs) */ async function checkAndRenewOnce() { await logLine("INFO", "🔍 [SSL CRON] Cycle started", { API_BASE }); try { await logLine("INFO", "Fetching domains", { url: `${API_BASE}/dns/domains` }); const res = await axios.get(`${API_BASE}/dns/domains`, { timeout: 60_000 }); if (!res.data?.success) { await logLine("ERROR", "/dns/domains failed", { data: res.data }); return; } const rawParsed = res.data.rawParsed || []; await logLine("INFO", "Domains fetched", { count: rawParsed.length }); for (const row of rawParsed) { const domain = row?.domain; const ssl = row?.ssl || null; const notAfter = ssl?.notAfter || null; const daysLeft = daysLeftFromSsl(notAfter); const expired = isSslExpired(notAfter); await logLine("INFO", "Domain status", { domain, notAfter, daysLeft, expired, }); if (!domain) continue; if (!expired) continue; // ✅ Actually renew via your API (which also notifies reverse proxy if exists) try { await logLine("INFO", "Renewing SSL via /ssl/refresh", { domain, url: `${API_BASE}/ssl/refresh`, }); const refreshRes = await axios.post( `${API_BASE}/ssl/refresh`, { domain }, { timeout: 120_000 } ); if (refreshRes.data?.success) { await logLine("INFO", "✅ SSL renewed", { domain, response: refreshRes.data, }); } else { await logLine("ERROR", "⚠️ Renew failed (API returned success=false)", { domain, response: refreshRes.data, }); } } catch (err) { await logLine("ERROR", "❌ Error renewing SSL", { domain, error: err?.response?.data || err?.message || String(err), status: err?.response?.status || null, }); } } await logLine("INFO", "🎯 [SSL CRON] Cycle completed"); } catch (err) { await logLine("ERROR", "❌ Error fetching domains", { error: err?.response?.data || err?.message || String(err), status: err?.response?.status || null, }); } } // 🔁 Schedule to run once per day at 03:00 server time cron.schedule("0 3 * * *", async () => { await logLine("INFO", "⏰ [SSL CRON] Daily job triggered"); await checkAndRenewOnce(); }); // Optional: run immediately when starting the script checkAndRenewOnce();