174 lines
4.6 KiB
JavaScript
Executable File
174 lines
4.6 KiB
JavaScript
Executable File
// 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();
|