Clean repo: removed node_modules and added gitignore
This commit is contained in:
commit
489de02f49
1
.env
Executable file
1
.env
Executable file
@ -0,0 +1 @@
|
||||
SSL_MANAGER_URL="https://api.hestiacp.metatronhost.com"
|
||||
125
BAK/SSL_CRON copy 2.js
Executable file
125
BAK/SSL_CRON copy 2.js
Executable file
@ -0,0 +1,125 @@
|
||||
// sslCron.mjs
|
||||
|
||||
import cron from "node-cron";
|
||||
import axios from "axios";
|
||||
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* Parse "notAfter" like: "Mar 11 19:08:24 2026 GMT"
|
||||
*/
|
||||
function parseNotAfter(notAfterStr) {
|
||||
if (!notAfterStr) return null;
|
||||
const d = new Date(notAfterStr); // JS can parse this format directly
|
||||
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; // if we can't parse it, treat as expired
|
||||
const now = new Date();
|
||||
return now > expiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* One cycle:
|
||||
* - Fetch domain list (/dns/domains)
|
||||
* - For each domain, look at ssl.notAfter
|
||||
* - If expired -> (for now) just console.log the renew call
|
||||
*/
|
||||
async function checkAndRenewOnce() {
|
||||
try {
|
||||
console.log("🔍 [SSL CRON] Fetching domains...");
|
||||
const res = await axios.get(`${API_BASE}/dns/domains`);
|
||||
|
||||
if (!res.data?.success) {
|
||||
console.error("❌ [SSL CRON] /dns/domains failed:", res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawParsed = res.data.rawParsed || [];
|
||||
console.log(`📦 [SSL CRON] Found ${rawParsed.length} DNS entries`);
|
||||
|
||||
for (const row of rawParsed) {
|
||||
const { domain, ssl } = row;
|
||||
|
||||
const notAfter = ssl?.notAfter || null;
|
||||
const left = daysLeftFromSsl(notAfter);
|
||||
const expired = isSslExpired(notAfter);
|
||||
|
||||
console.log(
|
||||
`🌐 Domain: ${domain} | notAfter: ${notAfter} | daysLeft: ${left} | expired: ${expired}`
|
||||
);
|
||||
|
||||
if (!expired) {
|
||||
continue; // SSL still valid, skip
|
||||
}
|
||||
|
||||
// 🔁 For now: only log what we *would* do
|
||||
console.log(
|
||||
`🔁 [SSL CRON] Would renew SSL for ${domain} via POST ${API_BASE}/ssl/refresh`
|
||||
);
|
||||
|
||||
// When you're ready to actually renew, uncomment this block:
|
||||
/*
|
||||
try {
|
||||
console.log(`🔁 [SSL CRON] Renewing SSL for ${domain}...`);
|
||||
const refreshRes = await axios.post(`${API_BASE}/ssl/refresh`, {
|
||||
domain,
|
||||
});
|
||||
|
||||
if (refreshRes.data?.success) {
|
||||
console.log(`✅ [SSL CRON] SSL renewed for ${domain}`);
|
||||
} else {
|
||||
console.error(
|
||||
`⚠️ [SSL CRON] Failed to renew ${domain}:`,
|
||||
refreshRes.data
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`❌ [SSL CRON] Error renewing ${domain}:`,
|
||||
err.response?.data || err.message
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
console.log("🎯 [SSL CRON] Cycle completed.");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"❌ [SSL CRON] Error fetching domains:",
|
||||
err.response?.data || err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔁 Schedule to run once per day at 03:00 server time
|
||||
cron.schedule("0 3 * * *", () => {
|
||||
console.log("⏰ [SSL CRON] Daily job started...");
|
||||
checkAndRenewOnce();
|
||||
});
|
||||
|
||||
// Optional: run immediately when starting the script
|
||||
checkAndRenewOnce();
|
||||
117
BAK/SSL_CRON copy.js
Executable file
117
BAK/SSL_CRON copy.js
Executable file
@ -0,0 +1,117 @@
|
||||
// sslCron.mjs
|
||||
|
||||
import cron from "node-cron";
|
||||
import axios from "axios";
|
||||
|
||||
// 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";
|
||||
|
||||
// How many days total you treat the cert as valid
|
||||
// You said: "add the 89 days to the ssl date" → 1 (start) + 89 = 90 days
|
||||
const CERT_LIFETIME_DAYS = 90;
|
||||
|
||||
/**
|
||||
* Given the "date" from rawParsed (e.g. "2025-06-27"),
|
||||
* calculate expiry date (date + 89 days) and compare with now.
|
||||
*/
|
||||
function isExpired(dateStr) {
|
||||
if (!dateStr) return true;
|
||||
|
||||
const issued = new Date(dateStr + "T00:00:00Z"); // force UTC-ish
|
||||
if (isNaN(issued.getTime())) return true;
|
||||
|
||||
const expiry = new Date(issued);
|
||||
expiry.setDate(expiry.getDate() + (CERT_LIFETIME_DAYS - 1)); // +89 days
|
||||
|
||||
const now = new Date();
|
||||
return now > expiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional helper: return days left until expiry (can be useful for logging/threshholds)
|
||||
*/
|
||||
function daysLeft(dateStr) {
|
||||
const issued = new Date(dateStr + "T00:00:00Z");
|
||||
if (isNaN(issued.getTime())) return -9999;
|
||||
|
||||
const expiry = new Date(issued);
|
||||
expiry.setDate(expiry.getDate() + (CERT_LIFETIME_DAYS - 1));
|
||||
|
||||
const diffMs = expiry.getTime() - new Date().getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* This runs one cycle:
|
||||
* - Fetch domain list
|
||||
* - For each domain, decide if expired
|
||||
* - If expired, call /ssl/refresh
|
||||
*/
|
||||
async function checkAndRenewOnce() {
|
||||
try {
|
||||
console.log("🔍 [SSL CRON] Fetching domains...");
|
||||
const res = await axios.get(`${API_BASE}/dns/domains`);
|
||||
|
||||
if (!res.data?.success) {
|
||||
console.error("❌ [SSL CRON] /dns/domains failed:", res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawParsed = res.data.rawParsed || [];
|
||||
console.log(`📦 [SSL CRON] Found ${rawParsed.length} DNS entries`);
|
||||
|
||||
for (const row of rawParsed) {
|
||||
const { domain, date } = row;
|
||||
|
||||
const left = daysLeft(date);
|
||||
const expired = isExpired(date);
|
||||
|
||||
console.log(
|
||||
`🌐 Domain: ${domain} | date: ${date} | daysLeft: ${left} | expired: ${expired}`
|
||||
);
|
||||
|
||||
if (!expired) {
|
||||
continue; // still valid, skip
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🔁 [SSL CRON] Renewing SSL for ${domain}...`);
|
||||
const refreshRes = await axios.post(`${API_BASE}/ssl/refresh`, {
|
||||
domain,
|
||||
});
|
||||
|
||||
if (refreshRes.data?.success) {
|
||||
console.log(`✅ [SSL CRON] SSL renewed for ${domain}`);
|
||||
} else {
|
||||
console.error(
|
||||
`⚠️ [SSL CRON] Failed to renew ${domain}:`,
|
||||
refreshRes.data
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`❌ [SSL CRON] Error renewing ${domain}:`,
|
||||
err.response?.data || err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎯 [SSL CRON] Cycle completed.");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"❌ [SSL CRON] Error fetching domains:",
|
||||
err.response?.data || err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔁 Schedule to run once per day at 03:00 server time
|
||||
cron.schedule("0 3 * * *", () => {
|
||||
console.log("⏰ [SSL CRON] Daily job started...");
|
||||
checkAndRenewOnce();
|
||||
});
|
||||
|
||||
// Optional: run immediately when starting the script
|
||||
checkAndRenewOnce();
|
||||
361
BAK/index copy 2.js
Executable file
361
BAK/index copy 2.js
Executable file
@ -0,0 +1,361 @@
|
||||
// server.mjs (or server.js with "type": "module")
|
||||
|
||||
import express from "express";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
import NodeCache from "node-cache";
|
||||
|
||||
// 👉 Environment / Secrets
|
||||
const API = process.env.HESTIA_API_HASH || "vvQjauyYQ2YDY2Au-_DC4QO5MUPFiHNn";
|
||||
const SERVER =
|
||||
process.env.HESTIA_SERVER_URL || "https://host.metatronhost.com:8083/api/";
|
||||
|
||||
// 👉 FIXED Hestia username
|
||||
const HESTIA_USER = "user";
|
||||
|
||||
// 🧠 SSL info cache (per domain, 1 day TTL)
|
||||
const sslCache = new NodeCache({
|
||||
stdTTL: 60 * 60 * 24, // 24 hours
|
||||
checkperiod: 60 * 60, // check every hour
|
||||
});
|
||||
|
||||
// Ignore self-signed SSL
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// 🔧 H E S T I A H E L P E R S
|
||||
// ===============================
|
||||
|
||||
async function hestia(cmd, args = {}) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd,
|
||||
...args,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Fetch raw DNS domain table
|
||||
async function getRawDomainList() {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd: "v-list-dns-domains",
|
||||
arg1: HESTIA_USER,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Parse domains from raw table (only domain names)
|
||||
function parseDomainsFromTable(text) {
|
||||
const lines = text.split("\n");
|
||||
const domains = [];
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.includes(".")) continue;
|
||||
if (line.startsWith("DOMAIN")) continue;
|
||||
if (line.startsWith("------")) continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
const domain = parts[0];
|
||||
|
||||
if (domain && domain.includes(".")) {
|
||||
domains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// Parse the full DNS table into structured rows
|
||||
function parseRawTable(text) {
|
||||
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
|
||||
const dataRows = [];
|
||||
|
||||
// Skip first 2 header lines
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const parts = line.split(/\s+/);
|
||||
|
||||
// Expect at least 8 columns
|
||||
if (parts.length < 8) continue;
|
||||
|
||||
const [domain, ip, tpl, ttl, dnssec, rec, spnd, date] = parts;
|
||||
|
||||
dataRows.push({
|
||||
domain,
|
||||
ip,
|
||||
tpl,
|
||||
ttl: Number(ttl),
|
||||
dnssec,
|
||||
rec: Number(rec),
|
||||
spnd,
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
// Remove SSL certificate
|
||||
async function removeSsl(domain) {
|
||||
return hestia("v-delete-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Let's Encrypt SSL
|
||||
async function addSsl(domain) {
|
||||
return hestia("v-add-letsencrypt-domain", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove + Add (renew)
|
||||
async function refreshSsl(domain) {
|
||||
const removed = await removeSsl(domain);
|
||||
const added = await addSsl(domain);
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
// ✅ Get REAL SSL info for a web domain, but return ONLY summary (no big CRT/KEY/CA)
|
||||
async function getWebSslInfoSummary(domain) {
|
||||
try {
|
||||
// v-list-web-domain-ssl USER DOMAIN [FORMAT]
|
||||
const data = await hestia("v-list-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
arg3: "json",
|
||||
});
|
||||
|
||||
let parsed = data;
|
||||
|
||||
// If Hestia returns a string, try parse it
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
// if it fails, we just keep raw string, but there's no safe way to summarize
|
||||
return {
|
||||
success: true,
|
||||
subject: null,
|
||||
aliases: null,
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
issuer: null,
|
||||
signature: null,
|
||||
pubKey: null,
|
||||
sslForce: null,
|
||||
rawType: "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// parsed usually looks like:
|
||||
// { "metatronhost.com": { CRT, KEY, CA, SUBJECT, ALIASES, NOT_BEFORE, NOT_AFTER, ... } }
|
||||
const keys = Object.keys(parsed || {});
|
||||
if (!keys.length) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No SSL data found in response",
|
||||
};
|
||||
}
|
||||
|
||||
const cert = parsed[keys[0]] || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subject: cert.SUBJECT || null,
|
||||
aliases: cert.ALIASES || null,
|
||||
notBefore: cert.NOT_BEFORE || null,
|
||||
notAfter: cert.NOT_AFTER || null,
|
||||
issuer: cert.ISSUER || null,
|
||||
signature: cert.SIGNATURE || null,
|
||||
pubKey: cert.PUB_KEY || null,
|
||||
sslForce: cert.SSL_FORCE || null,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err.response?.data || err.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🚀 E X P R E S S A P I
|
||||
// ===============================
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Health
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ running: true, user: HESTIA_USER });
|
||||
});
|
||||
|
||||
// Get all DNS domains + SSL summary (cached)
|
||||
app.get("/dns/domains", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
const rawParsed = parseRawTable(raw);
|
||||
|
||||
const sslInfoByDomain = {};
|
||||
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `ssl-${domain}`;
|
||||
let summary = sslCache.get(cacheKey);
|
||||
|
||||
if (!summary) {
|
||||
// Not in cache → fetch from Hestia and cache it
|
||||
summary = await getWebSslInfoSummary(domain);
|
||||
sslCache.set(cacheKey, summary);
|
||||
}
|
||||
|
||||
sslInfoByDomain[domain] = summary;
|
||||
}
|
||||
|
||||
const rawParsedWithSsl = rawParsed.map((row) => ({
|
||||
...row,
|
||||
ssl: sslInfoByDomain[row.domain] || null,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
domains,
|
||||
rawParsed: rawParsedWithSsl,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove SSL
|
||||
app.post("/ssl/remove", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await removeSsl(domain);
|
||||
// Invalidate cache for that domain, since SSL changed
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add SSL
|
||||
app.post("/ssl/add", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await addSsl(domain);
|
||||
// Invalidate cache so next /dns/domains pulls fresh SSL info
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL
|
||||
app.post("/ssl/refresh", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await refreshSsl(domain);
|
||||
// Invalidate cache so next /dns/domains pulls fresh SSL info
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL for ALL domains
|
||||
app.post("/ssl/refresh-all", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
const result = await refreshSsl(domain);
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
results.push({ domain, success: true, result });
|
||||
} catch (e) {
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3015;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🔥 SSL Manager API running at port ${PORT}`);
|
||||
});
|
||||
265
BAK/index copy.js
Executable file
265
BAK/index copy.js
Executable file
@ -0,0 +1,265 @@
|
||||
// server.mjs (or server.js with "type": "module")
|
||||
|
||||
import express from "express";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
// 👉 Environment / Secrets
|
||||
const API = process.env.HESTIA_API_HASH || "vvQjauyYQ2YDY2Au-_DC4QO5MUPFiHNn";
|
||||
const SERVER = process.env.HESTIA_SERVER_URL || "https://host.metatronhost.com:8083/api/";
|
||||
|
||||
// 👉 FIXED Hestia username
|
||||
const HESTIA_USER = "user";
|
||||
|
||||
// Ignore self-signed SSL
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// 🔧 H E S T I A H E L P E R S
|
||||
// ===============================
|
||||
|
||||
async function hestia(cmd, args = {}) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd,
|
||||
...args,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Fetch raw DNS domain table
|
||||
async function getRawDomainList() {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd: "v-list-dns-domains",
|
||||
arg1: HESTIA_USER,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Parse domains from raw table
|
||||
function parseDomainsFromTable(text) {
|
||||
const lines = text.split("\n");
|
||||
const domains = [];
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.includes(".")) continue;
|
||||
if (line.startsWith("DOMAIN")) continue;
|
||||
if (line.startsWith("------")) continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
const domain = parts[0];
|
||||
|
||||
if (domain && domain.includes(".")) {
|
||||
domains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// Remove SSL certificate
|
||||
async function removeSsl(domain) {
|
||||
return hestia("v-delete-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Let's Encrypt SSL
|
||||
async function addSsl(domain) {
|
||||
return hestia("v-add-letsencrypt-domain", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove + Add (renew)
|
||||
async function refreshSsl(domain) {
|
||||
const removed = await removeSsl(domain);
|
||||
const added = await addSsl(domain);
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
function parseRawTable(text) {
|
||||
const lines = text.split("\n").map(l => l.trim()).filter(Boolean);
|
||||
|
||||
const dataRows = [];
|
||||
|
||||
// Skip first 2 header lines
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const parts = line.split(/\s+/);
|
||||
|
||||
// Expect at least 8 columns
|
||||
if (parts.length < 8) continue;
|
||||
|
||||
const [
|
||||
domain,
|
||||
ip,
|
||||
tpl,
|
||||
ttl,
|
||||
dnssec,
|
||||
rec,
|
||||
spnd,
|
||||
date
|
||||
] = parts;
|
||||
|
||||
dataRows.push({
|
||||
domain,
|
||||
ip,
|
||||
tpl,
|
||||
ttl: Number(ttl),
|
||||
dnssec,
|
||||
rec: Number(rec),
|
||||
spnd,
|
||||
date
|
||||
});
|
||||
}
|
||||
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ===============================
|
||||
// 🚀 E X P R E S S A P I
|
||||
// ===============================
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Health
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ running: true, user: HESTIA_USER });
|
||||
});
|
||||
|
||||
// Get all DNS domains
|
||||
app.get("/dns/domains", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
const rawParsed = parseRawTable(raw);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
domains,
|
||||
rawParsed
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Remove SSL
|
||||
app.post("/ssl/remove", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await removeSsl(domain);
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add SSL
|
||||
app.post("/ssl/add", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await addSsl(domain);
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL
|
||||
app.post("/ssl/refresh", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res.status(400).json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await refreshSsl(domain);
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL for ALL domains
|
||||
app.post("/ssl/refresh-all", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
const result = await refreshSsl(domain);
|
||||
results.push({ domain, success: true, result });
|
||||
} catch (e) {
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3015;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🔥 SSL Manager API running at port ${PORT}`);
|
||||
});
|
||||
173
SSL_CRON.js
Executable file
173
SSL_CRON.js
Executable file
@ -0,0 +1,173 @@
|
||||
// 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();
|
||||
125
Test.js
Normal file
125
Test.js
Normal file
@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
import dns from "node:dns";
|
||||
dns.setDefaultResultOrder("ipv4first");
|
||||
|
||||
|
||||
|
||||
const API = "vvQjauyYQ2YDY2Au-_DC4QO5MUPFiHNn";
|
||||
// const SERVER = "https://host.metatronhost.com:8083/api/";
|
||||
const SERVER = "https://127.0.0.1:8083/api/";
|
||||
|
||||
// Ignore self-signed SSL
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// Generic Hestia API function
|
||||
async function hestia(cmd, args = {}) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd,
|
||||
...args,
|
||||
});
|
||||
|
||||
// const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
const res = await axios.post(SERVER, form, {
|
||||
httpsAgent,
|
||||
family: 4,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// 1) Fetch raw DNS table (your way)
|
||||
async function getRawDomainList(user) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd: "v-list-dns-domains",
|
||||
arg1: user,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, {
|
||||
httpsAgent,
|
||||
family: 4,
|
||||
});
|
||||
// const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data; // plain text
|
||||
}
|
||||
|
||||
// 2) Parse domains from the table
|
||||
function parseDomainsFromTable(text) {
|
||||
const lines = text.split("\n");
|
||||
const domains = [];
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.includes(".")) continue;
|
||||
if (line.startsWith("DOMAIN")) continue;
|
||||
if (line.startsWith("------")) continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
const domain = parts[0];
|
||||
|
||||
if (domain && domain.includes(".")) {
|
||||
domains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// 3) Remove SSL
|
||||
async function removeSsl(user, domain) {
|
||||
return hestia("v-delete-web-domain-ssl", {
|
||||
arg1: user,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// 4) Add Let's Encrypt SSL (no email param)
|
||||
async function addLetsEncrypt(user, domain) {
|
||||
// v-add-letsencrypt-domain USER DOMAIN [ALIASES] [MAIL]
|
||||
// Here: generate SSL for domain + www.domain, no mail cert
|
||||
return hestia("v-add-letsencrypt-domain", {
|
||||
arg1: user,
|
||||
arg2: domain,
|
||||
//arg3: `${domain}`, // aliases
|
||||
// no arg4 (MAIL) -> defaults to "no"
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const user = "user";
|
||||
|
||||
try {
|
||||
console.log("🔍 Fetching DNS domain table...");
|
||||
const table = await getRawDomainList(user);
|
||||
console.log(table);
|
||||
|
||||
console.log("\n🔍 Parsing domain names...");
|
||||
const domains = parseDomainsFromTable(table);
|
||||
console.log("Parsed domains:", domains);
|
||||
|
||||
if (domains.length === 0) {
|
||||
console.log("❌ No domains found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = "test.metatronhost.com"; // or domains[0] if you want auto-pick
|
||||
console.log("\n🟦 Selected domain:", domain);
|
||||
|
||||
console.log("\n🔧 Removing old SSL...");
|
||||
console.log(await removeSsl(user, domain));
|
||||
|
||||
console.log("\n🔐 Adding Let's Encrypt SSL...");
|
||||
console.log(await addLetsEncrypt(user, domain));
|
||||
|
||||
console.log("\n✅ SSL refreshed successfully for:", domain);
|
||||
} catch (err) {
|
||||
console.error("❌ ERROR:", err.response?.data || err.message || err);
|
||||
}
|
||||
})();
|
||||
|
||||
713
index copy.js
Executable file
713
index copy.js
Executable file
@ -0,0 +1,713 @@
|
||||
// server.mjs (or server.js with "type": "module")
|
||||
|
||||
import express from "express";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
import NodeCache from "node-cache";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
|
||||
|
||||
import dns from "node:dns";
|
||||
dns.setDefaultResultOrder("ipv4first");
|
||||
|
||||
|
||||
// 👉 Environment / Secrets
|
||||
const API = process.env.HESTIA_API_HASH || "GCzPEYiK2NYDgq0Pz2d-CRctVyStsxiE";
|
||||
const SERVER =
|
||||
process.env.HESTIA_SERVER_URL ||"https://127.0.0.1:8083/api/" || "https://host.metatronnest.com:8083/api/";
|
||||
|
||||
// 👉 FIXED Hestia username
|
||||
const HESTIA_USER = "user";
|
||||
|
||||
// 🧠 SSL info cache (per domain, 1 day TTL)
|
||||
const sslCache = new NodeCache({
|
||||
stdTTL: 60 * 60 * 24, // 24 hours
|
||||
checkperiod: 60 * 60, // check every hour
|
||||
});
|
||||
|
||||
// Ignore self-signed SSL
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// 🧾 L O G G I N G
|
||||
// ===============================
|
||||
|
||||
const LOG_DIR = process.env.LOG_DIR || "/var/log/ssl-manager";
|
||||
const LOG_FILE = process.env.LOG_FILE || path.join(LOG_DIR, "server.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";
|
||||
|
||||
// console log also
|
||||
if (level === "ERROR") console.error(line.trim());
|
||||
else console.log(line.trim());
|
||||
|
||||
try {
|
||||
await ensureLogPath();
|
||||
await fs.appendFile(LOG_FILE, line, "utf8");
|
||||
} catch (e) {
|
||||
// if log file fails, don't crash server
|
||||
console.error(
|
||||
`[${ts}] [ERROR] Failed to write log file: ${e.message || e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔎 N G I N X P R O X Y P O R T
|
||||
// ===============================
|
||||
|
||||
const WEB_CONF_ROOT = process.env.WEB_CONF_ROOT || "/home/user/conf/web";
|
||||
const SERVER_IP = process.env.SERVER_IP || "147.93.40.215";
|
||||
|
||||
// Endpoint to notify when reverse proxy exists
|
||||
const NEST_PROXY_ENDPOINT =
|
||||
process.env.NEST_PROXY_ENDPOINT ||"http://147.93.40.215:9999/api/nginx/app"||
|
||||
"https://manage.api.metatronnest.com/proxy/nginx-app";
|
||||
|
||||
// Optional auth header if needed
|
||||
const NEST_PROXY_TOKEN = process.env.NEST_PROXY_TOKEN || "";
|
||||
|
||||
// Separate agent (if you ever have self-signed on metatronnest, set to false)
|
||||
// by default, keep secure for external endpoint
|
||||
const nestHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: true,
|
||||
});
|
||||
|
||||
function extractProxyPort(nginxConfText) {
|
||||
// strip comments
|
||||
const text = nginxConfText.replace(/#.*$/gm, "");
|
||||
|
||||
// match: proxy_pass http://127.0.0.1:3022;
|
||||
// or: proxy_pass http://localhost:3022;
|
||||
const m = text.match(
|
||||
/proxy_pass\s+https?:\/\/(?:127\.0\.0\.1|localhost):(\d+)\s*;/i
|
||||
);
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
const port = Number(m[1]);
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
async function getDomainProxyInfo(domain) {
|
||||
if (!domain || typeof domain !== "string") {
|
||||
throw new Error("domain must be a non-empty string");
|
||||
}
|
||||
|
||||
const confPath = path.join(WEB_CONF_ROOT, domain, "nginx.conf");
|
||||
|
||||
let nginxConf;
|
||||
try {
|
||||
nginxConf = await fs.readFile(confPath, "utf8");
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ [Proxy Info] nginx.conf not found for domain ${domain}`,e);
|
||||
return {
|
||||
success: false,
|
||||
domain,
|
||||
serverIp: SERVER_IP,
|
||||
port: null,
|
||||
error: "nginx.conf not found",
|
||||
};
|
||||
}
|
||||
|
||||
const port = extractProxyPort(nginxConf);
|
||||
|
||||
if (!port) {
|
||||
return {
|
||||
success: false,
|
||||
domain,
|
||||
serverIp: SERVER_IP,
|
||||
port: null,
|
||||
error: "proxy_pass not found in nginx.conf",
|
||||
};
|
||||
}
|
||||
console.log( { success: true, domain, serverIp: SERVER_IP, port })
|
||||
return { success: true, domain, serverIp: SERVER_IP, port };
|
||||
}
|
||||
|
||||
/**
|
||||
* If reverse proxy exists for domain, notify metatronnest endpoint
|
||||
*/
|
||||
async function notifyNestIfReverseProxy(domain, proxyInfo) {
|
||||
try {
|
||||
|
||||
|
||||
await logLine("INFO", "Reverse proxy check", { domain, proxyInfo });
|
||||
|
||||
if (!proxyInfo.success || !proxyInfo.port) {
|
||||
await logLine("INFO", "No reverse proxy found, skipping notify", {
|
||||
domain,
|
||||
});
|
||||
return { notified: false, reason: "no_proxy", proxyInfo };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
server_ip: proxyInfo.serverIp,
|
||||
domain: proxyInfo.domain,
|
||||
port: String(proxyInfo.port),
|
||||
};
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (NEST_PROXY_TOKEN) headers["Authorization"] = `Bearer ${NEST_PROXY_TOKEN}`;
|
||||
|
||||
await logLine("INFO", "Notifying metatronnest /proxy/nginx-app", {
|
||||
endpoint: NEST_PROXY_ENDPOINT,
|
||||
payload,
|
||||
});
|
||||
|
||||
const resp = await axios.post(NEST_PROXY_ENDPOINT, payload, {
|
||||
headers,
|
||||
httpsAgent: nestHttpsAgent,
|
||||
timeout: 20_000,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
await logLine("INFO", "metatronnest notify success", {
|
||||
status: resp.status,
|
||||
data: resp.data,
|
||||
});
|
||||
|
||||
return { notified: true, payload, response: resp.data };
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "metatronnest notify failed", {
|
||||
domain,
|
||||
error: err?.response?.data || err?.message || String(err),
|
||||
status: err?.response?.status || null,
|
||||
});
|
||||
|
||||
return {
|
||||
notified: false,
|
||||
reason: "notify_failed",
|
||||
error: err?.response?.data || err?.message || String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔧 H E S T I A H E L P E R S
|
||||
// ===============================
|
||||
|
||||
async function hestia(cmd, args = {}) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd,
|
||||
...args,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
// Fetch raw DNS domain table
|
||||
async function getRawDomainList() {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd: "v-list-dns-domains",
|
||||
arg1: HESTIA_USER,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Parse domains from raw table (only domain names)
|
||||
function parseDomainsFromTable(text) {
|
||||
const lines = text.split("\n");
|
||||
const domains = [];
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.includes(".")) continue;
|
||||
if (line.startsWith("DOMAIN")) continue;
|
||||
if (line.startsWith("------")) continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
const domain = parts[0];
|
||||
|
||||
if (domain && domain.includes(".")) {
|
||||
domains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// Parse the full DNS table into structured rows
|
||||
function parseRawTable(text) {
|
||||
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
|
||||
const dataRows = [];
|
||||
|
||||
// Skip first 2 header lines
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const parts = line.split(/\s+/);
|
||||
|
||||
// Expect at least 8 columns
|
||||
if (parts.length < 8) continue;
|
||||
|
||||
const [domain, ip, tpl, ttl, dnssec, rec, spnd, date] = parts;
|
||||
|
||||
dataRows.push({
|
||||
domain,
|
||||
ip,
|
||||
tpl,
|
||||
ttl: Number(ttl),
|
||||
dnssec,
|
||||
rec: Number(rec),
|
||||
spnd,
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
// Remove SSL certificate
|
||||
async function removeSsl(domain) {
|
||||
await logLine("INFO", "Removing SSL", { domain });
|
||||
return hestia("v-delete-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Let's Encrypt SSL
|
||||
async function addSsl(domain) {
|
||||
await logLine("INFO", "Adding Let's Encrypt SSL", { domain });
|
||||
return hestia("v-add-letsencrypt-domain", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove + Add (renew)
|
||||
async function refreshSsl(domain) {
|
||||
await logLine("INFO", "Refreshing SSL (remove + add)", { domain });
|
||||
const removed = await removeSsl(domain);
|
||||
const added = await addSsl(domain);
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
// ✅ Get REAL SSL info for a web domain, but return ONLY summary (no big CRT/KEY/CA)
|
||||
async function getWebSslInfoSummary(domain) {
|
||||
try {
|
||||
// v-list-web-domain-ssl USER DOMAIN [FORMAT]
|
||||
const data = await hestia("v-list-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
arg3: "json",
|
||||
});
|
||||
|
||||
let parsed = data;
|
||||
|
||||
// If Hestia returns a string, try parse it
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
return {
|
||||
success: true,
|
||||
subject: null,
|
||||
aliases: null,
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
issuer: null,
|
||||
signature: null,
|
||||
pubKey: null,
|
||||
sslForce: null,
|
||||
rawType: "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const keys = Object.keys(parsed || {});
|
||||
if (!keys.length) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No SSL data found in response",
|
||||
};
|
||||
}
|
||||
|
||||
const cert = parsed[keys[0]] || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subject: cert.SUBJECT || null,
|
||||
aliases: cert.ALIASES || null,
|
||||
notBefore: cert.NOT_BEFORE || null,
|
||||
notAfter: cert.NOT_AFTER || null,
|
||||
issuer: cert.ISSUER || null,
|
||||
signature: cert.SIGNATURE || null,
|
||||
pubKey: cert.PUB_KEY || null,
|
||||
sslForce: cert.SSL_FORCE || null,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err.response?.data || err.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🚀 E X P R E S S A P I
|
||||
// ===============================
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// request logging
|
||||
app.use(async (req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on("finish", async () => {
|
||||
await logLine("INFO", "HTTP", {
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
status: res.statusCode,
|
||||
ms: Date.now() - start,
|
||||
});
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Health
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ running: true, user: HESTIA_USER });
|
||||
});
|
||||
|
||||
// ✅ Get proxy info for one domain
|
||||
// Example: /proxy/info?domain=gmb.metatronhost.com
|
||||
app.get("/proxy/info", async (req, res) => {
|
||||
const domain = req.query.domain;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain query param is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await getDomainProxyInfo(String(domain));
|
||||
return res.json(info);
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.message || "Unknown error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Get proxy info for all domains (based on Hestia DNS list)
|
||||
app.get("/proxy/all", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
for (const domain of domains) {
|
||||
results.push(await getDomainProxyInfo(domain));
|
||||
}
|
||||
|
||||
res.json({ success: true, serverIp: SERVER_IP, results });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get all DNS domains + SSL summary (cached)
|
||||
app.get("/dns/domains", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
const rawParsed = parseRawTable(raw);
|
||||
|
||||
const sslInfoByDomain = {};
|
||||
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `ssl-${domain}`;
|
||||
let summary = sslCache.get(cacheKey);
|
||||
|
||||
if (!summary) {
|
||||
summary = await getWebSslInfoSummary(domain);
|
||||
sslCache.set(cacheKey, summary);
|
||||
}
|
||||
|
||||
sslInfoByDomain[domain] = summary;
|
||||
}
|
||||
|
||||
const rawParsedWithSsl = rawParsed.map((row) => ({
|
||||
...row,
|
||||
ssl: sslInfoByDomain[row.domain] || null,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
domains,
|
||||
rawParsed: rawParsedWithSsl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in /dns/domains:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/remove", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL REMOVE requested", { domain });
|
||||
|
||||
// const notify = await notifyNestIfReverseProxy(domain);
|
||||
|
||||
const result = await removeSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REMOVE failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/add", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL ADD requested", { domain });
|
||||
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
const result = await addSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
res.json({ success: true, domain, notify, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL ADD failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/refresh", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH requested", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
const result = await refreshSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
res.json({ success: true, domain, notify, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL for ALL domains (only notify if reverse proxy exists)
|
||||
app.post("/ssl/refresh-all", async (req, res) => {
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH-ALL requested", { user: HESTIA_USER });
|
||||
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
await logLine("INFO", "Processing domain for refresh-all", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
const result = await refreshSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
// results.push({ domain, success: true,proxyInfo });
|
||||
results.push({ domain, success: true, notify, result });
|
||||
} catch (e) {
|
||||
await logLine("ERROR", "refresh-all domain failed", {
|
||||
domain,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH-ALL failed", {
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/reverse-proxy-info", async (req, res) => {
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH-ALL requested", { user: HESTIA_USER });
|
||||
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
await logLine("INFO", "Processing domain for refresh-all", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
// const result = await refreshSsl(domain);
|
||||
|
||||
// sslCache.del(`ssl-${domain}`);
|
||||
|
||||
// const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
results.push({ domain, success: true,proxyInfo });
|
||||
// results.push({ domain, success: true, notify, result });
|
||||
} catch (e) {
|
||||
await logLine("ERROR", "reverse-proxy domain failed", {
|
||||
domain,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH-ALL failed", {
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3015;
|
||||
app.listen(PORT, async () => {
|
||||
await logLine("INFO", "🔥 SSL Manager API started", {
|
||||
port: PORT,
|
||||
user: HESTIA_USER,
|
||||
serverIp: SERVER_IP,
|
||||
webConfRoot: WEB_CONF_ROOT,
|
||||
logFile: LOG_FILE,
|
||||
nestEndpoint: NEST_PROXY_ENDPOINT,
|
||||
});
|
||||
});
|
||||
714
index.js
Executable file
714
index.js
Executable file
@ -0,0 +1,714 @@
|
||||
// server.mjs (or server.js with "type": "module")
|
||||
|
||||
import express from "express";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
import NodeCache from "node-cache";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
|
||||
|
||||
import dns from "node:dns";
|
||||
dns.setDefaultResultOrder("ipv4first");
|
||||
|
||||
|
||||
// 👉 Environment / Secrets
|
||||
const API = "vvQjauyYQ2YDY2Au-_DC4QO5MUPFiHNn"|| process.env.HESTIA_API_HASH || "GCzPEYiK2NYDgq0Pz2d-CRctVyStsxiE";
|
||||
const SERVER =
|
||||
process.env.HESTIA_SERVER_URL ||"https://127.0.0.1:8083/api/" || "https://host.metatronnest.com:8083/api/";
|
||||
|
||||
// 👉 FIXED Hestia username
|
||||
const HESTIA_USER = "user";
|
||||
|
||||
// 🧠 SSL info cache (per domain, 1 day TTL)
|
||||
const sslCache = new NodeCache({
|
||||
stdTTL: 60 * 60 * 24, // 24 hours
|
||||
checkperiod: 60 * 60, // check every hour
|
||||
});
|
||||
|
||||
// Ignore self-signed SSL
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// 🧾 L O G G I N G
|
||||
// ===============================
|
||||
|
||||
const LOG_DIR = process.env.LOG_DIR || "/var/log/ssl-manager";
|
||||
const LOG_FILE = process.env.LOG_FILE || path.join(LOG_DIR, "server.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";
|
||||
|
||||
// console log also
|
||||
if (level === "ERROR") console.error(line.trim());
|
||||
else console.log(line.trim());
|
||||
|
||||
try {
|
||||
await ensureLogPath();
|
||||
await fs.appendFile(LOG_FILE, line, "utf8");
|
||||
} catch (e) {
|
||||
// if log file fails, don't crash server
|
||||
console.error(
|
||||
`[${ts}] [ERROR] Failed to write log file: ${e.message || e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔎 N G I N X P R O X Y P O R T
|
||||
// ===============================
|
||||
|
||||
const WEB_CONF_ROOT = process.env.WEB_CONF_ROOT || "/home/user/conf/web";
|
||||
const SERVER_IP = process.env.SERVER_IP || "147.93.40.215";
|
||||
|
||||
// Endpoint to notify when reverse proxy exists
|
||||
const NEST_PROXY_ENDPOINT =
|
||||
process.env.NEST_PROXY_ENDPOINT ||"http://147.93.40.215:9999/api/nginx/app"||
|
||||
"https://manage.api.metatronnest.com/proxy/nginx-app";
|
||||
|
||||
// Optional auth header if needed
|
||||
const NEST_PROXY_TOKEN = process.env.NEST_PROXY_TOKEN || "";
|
||||
|
||||
// Separate agent (if you ever have self-signed on metatronnest, set to false)
|
||||
// by default, keep secure for external endpoint
|
||||
const nestHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: true,
|
||||
});
|
||||
|
||||
function extractProxyPort(nginxConfText) {
|
||||
// strip comments
|
||||
const text = nginxConfText.replace(/#.*$/gm, "");
|
||||
|
||||
// match: proxy_pass http://127.0.0.1:3022;
|
||||
// or: proxy_pass http://localhost:3022;
|
||||
const m = text.match(
|
||||
/proxy_pass\s+https?:\/\/(?:127\.0\.0\.1|localhost):(\d+)\s*;/i
|
||||
);
|
||||
|
||||
if (!m) return null;
|
||||
|
||||
const port = Number(m[1]);
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
async function getDomainProxyInfo(domain) {
|
||||
if (!domain || typeof domain !== "string") {
|
||||
throw new Error("domain must be a non-empty string");
|
||||
}
|
||||
|
||||
const confPath = path.join(WEB_CONF_ROOT, domain, "nginx.conf");
|
||||
|
||||
let nginxConf;
|
||||
try {
|
||||
nginxConf = await fs.readFile(confPath, "utf8");
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ [Proxy Info] nginx.conf not found for domain ${domain}`,e);
|
||||
return {
|
||||
success: false,
|
||||
domain,
|
||||
serverIp: SERVER_IP,
|
||||
port: null,
|
||||
error: "nginx.conf not found",
|
||||
};
|
||||
}
|
||||
|
||||
const port = extractProxyPort(nginxConf);
|
||||
|
||||
if (!port) {
|
||||
return {
|
||||
success: false,
|
||||
domain,
|
||||
serverIp: SERVER_IP,
|
||||
port: null,
|
||||
error: "proxy_pass not found in nginx.conf",
|
||||
};
|
||||
}
|
||||
console.log( { success: true, domain, serverIp: SERVER_IP, port })
|
||||
return { success: true, domain, serverIp: SERVER_IP, port };
|
||||
}
|
||||
|
||||
/**
|
||||
* If reverse proxy exists for domain, notify metatronnest endpoint
|
||||
*/
|
||||
async function notifyNestIfReverseProxy(domain, proxyInfo) {
|
||||
try {
|
||||
|
||||
|
||||
await logLine("INFO", "Reverse proxy check", { domain, proxyInfo });
|
||||
|
||||
if (!proxyInfo.success || !proxyInfo.port) {
|
||||
await logLine("INFO", "No reverse proxy found, skipping notify", {
|
||||
domain,
|
||||
});
|
||||
return { notified: false, reason: "no_proxy", proxyInfo };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
server_ip: proxyInfo.serverIp,
|
||||
domain: proxyInfo.domain,
|
||||
port: String(proxyInfo.port),
|
||||
};
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (NEST_PROXY_TOKEN) headers["Authorization"] = `Bearer ${NEST_PROXY_TOKEN}`;
|
||||
|
||||
await logLine("INFO", "Notifying metatronnest /proxy/nginx-app", {
|
||||
endpoint: NEST_PROXY_ENDPOINT,
|
||||
payload,
|
||||
});
|
||||
|
||||
const resp = await axios.post(NEST_PROXY_ENDPOINT, payload, {
|
||||
headers,
|
||||
httpsAgent: nestHttpsAgent,
|
||||
timeout: 20_000,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
await logLine("INFO", "metatronnest notify success", {
|
||||
status: resp.status,
|
||||
data: resp.data,
|
||||
});
|
||||
|
||||
return { notified: true, payload, response: resp.data };
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "metatronnest notify failed", {
|
||||
domain,
|
||||
error: err?.response?.data || err?.message || String(err),
|
||||
status: err?.response?.status || null,
|
||||
});
|
||||
|
||||
return {
|
||||
notified: false,
|
||||
reason: "notify_failed",
|
||||
error: err?.response?.data || err?.message || String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔧 H E S T I A H E L P E R S
|
||||
// ===============================
|
||||
|
||||
async function hestia(cmd, args = {}) {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd,
|
||||
...args,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
// Fetch raw DNS domain table
|
||||
async function getRawDomainList() {
|
||||
const form = new URLSearchParams({
|
||||
hash: API,
|
||||
cmd: "v-list-dns-domains",
|
||||
arg1: HESTIA_USER,
|
||||
});
|
||||
|
||||
const res = await axios.post(SERVER, form, { httpsAgent });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// Parse domains from raw table (only domain names)
|
||||
function parseDomainsFromTable(text) {
|
||||
const lines = text.split("\n");
|
||||
const domains = [];
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line.includes(".")) continue;
|
||||
if (line.startsWith("DOMAIN")) continue;
|
||||
if (line.startsWith("------")) continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
const domain = parts[0];
|
||||
|
||||
if (domain && domain.includes(".")) {
|
||||
domains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
// Parse the full DNS table into structured rows
|
||||
function parseRawTable(text) {
|
||||
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
|
||||
const dataRows = [];
|
||||
|
||||
// Skip first 2 header lines
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const parts = line.split(/\s+/);
|
||||
|
||||
// Expect at least 8 columns
|
||||
if (parts.length < 8) continue;
|
||||
|
||||
const [domain, ip, tpl, ttl, dnssec, rec, spnd, date] = parts;
|
||||
|
||||
dataRows.push({
|
||||
domain,
|
||||
ip,
|
||||
tpl,
|
||||
ttl: Number(ttl),
|
||||
dnssec,
|
||||
rec: Number(rec),
|
||||
spnd,
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
// Remove SSL certificate
|
||||
async function removeSsl(domain) {
|
||||
await logLine("INFO", "Removing SSL", { domain });
|
||||
return hestia("v-delete-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Let's Encrypt SSL
|
||||
async function addSsl(domain) {
|
||||
await logLine("INFO", "Adding Let's Encrypt SSL", { domain });
|
||||
return hestia("v-add-letsencrypt-domain", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove + Add (renew)
|
||||
async function refreshSsl(domain) {
|
||||
await logLine("INFO", "Refreshing SSL (remove + add)", { domain });
|
||||
// const removed = await removeSsl(domain);
|
||||
const added = await addSsl(domain);
|
||||
// return { removed, added };
|
||||
return { added };
|
||||
}
|
||||
|
||||
// ✅ Get REAL SSL info for a web domain, but return ONLY summary (no big CRT/KEY/CA)
|
||||
async function getWebSslInfoSummary(domain) {
|
||||
try {
|
||||
// v-list-web-domain-ssl USER DOMAIN [FORMAT]
|
||||
const data = await hestia("v-list-web-domain-ssl", {
|
||||
arg1: HESTIA_USER,
|
||||
arg2: domain,
|
||||
arg3: "json",
|
||||
});
|
||||
|
||||
let parsed = data;
|
||||
|
||||
// If Hestia returns a string, try parse it
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
return {
|
||||
success: true,
|
||||
subject: null,
|
||||
aliases: null,
|
||||
notBefore: null,
|
||||
notAfter: null,
|
||||
issuer: null,
|
||||
signature: null,
|
||||
pubKey: null,
|
||||
sslForce: null,
|
||||
rawType: "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const keys = Object.keys(parsed || {});
|
||||
if (!keys.length) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No SSL data found in response",
|
||||
};
|
||||
}
|
||||
|
||||
const cert = parsed[keys[0]] || {};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
subject: cert.SUBJECT || null,
|
||||
aliases: cert.ALIASES || null,
|
||||
notBefore: cert.NOT_BEFORE || null,
|
||||
notAfter: cert.NOT_AFTER || null,
|
||||
issuer: cert.ISSUER || null,
|
||||
signature: cert.SIGNATURE || null,
|
||||
pubKey: cert.PUB_KEY || null,
|
||||
sslForce: cert.SSL_FORCE || null,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err.response?.data || err.message || "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🚀 E X P R E S S A P I
|
||||
// ===============================
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// request logging
|
||||
app.use(async (req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on("finish", async () => {
|
||||
await logLine("INFO", "HTTP", {
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
status: res.statusCode,
|
||||
ms: Date.now() - start,
|
||||
});
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Health
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ running: true, user: HESTIA_USER });
|
||||
});
|
||||
|
||||
// ✅ Get proxy info for one domain
|
||||
// Example: /proxy/info?domain=gmb.metatronhost.com
|
||||
app.get("/proxy/info", async (req, res) => {
|
||||
const domain = req.query.domain;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain query param is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await getDomainProxyInfo(String(domain));
|
||||
return res.json(info);
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.message || "Unknown error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Get proxy info for all domains (based on Hestia DNS list)
|
||||
app.get("/proxy/all", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
for (const domain of domains) {
|
||||
results.push(await getDomainProxyInfo(domain));
|
||||
}
|
||||
|
||||
res.json({ success: true, serverIp: SERVER_IP, results });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get all DNS domains + SSL summary (cached)
|
||||
app.get("/dns/domains", async (req, res) => {
|
||||
try {
|
||||
const raw = await getRawDomainList();
|
||||
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
const rawParsed = parseRawTable(raw);
|
||||
|
||||
const sslInfoByDomain = {};
|
||||
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `ssl-${domain}`;
|
||||
let summary = sslCache.get(cacheKey);
|
||||
|
||||
if (!summary) {
|
||||
summary = await getWebSslInfoSummary(domain);
|
||||
sslCache.set(cacheKey, summary);
|
||||
}
|
||||
|
||||
sslInfoByDomain[domain] = summary;
|
||||
}
|
||||
|
||||
const rawParsedWithSsl = rawParsed.map((row) => ({
|
||||
...row,
|
||||
ssl: sslInfoByDomain[row.domain] || null,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
domains,
|
||||
rawParsed: rawParsedWithSsl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in /dns/domains:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/remove", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL REMOVE requested", { domain });
|
||||
|
||||
// const notify = await notifyNestIfReverseProxy(domain);
|
||||
|
||||
const result = await removeSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
res.json({ success: true, domain, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REMOVE failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/add", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL ADD requested", { domain });
|
||||
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
const result = await addSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
res.json({ success: true, domain, notify, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL ADD failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL (only notify if reverse proxy exists)
|
||||
app.post("/ssl/refresh", async (req, res) => {
|
||||
const { domain } = req.body;
|
||||
|
||||
if (!domain) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "domain is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH requested", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
const result = await refreshSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
res.json({ success: true, domain, notify, result });
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH failed", {
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
domain,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh SSL for ALL domains (only notify if reverse proxy exists)
|
||||
app.post("/ssl/refresh-all", async (req, res) => {
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH-ALL requested", { user: HESTIA_USER });
|
||||
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
await logLine("INFO", "Processing domain for refresh-all", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
const result = await refreshSsl(domain);
|
||||
|
||||
sslCache.del(`ssl-${domain}`);
|
||||
|
||||
const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
// results.push({ domain, success: true,proxyInfo });
|
||||
results.push({ domain, success: true, notify, result });
|
||||
} catch (e) {
|
||||
await logLine("ERROR", "refresh-all domain failed", {
|
||||
domain,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH-ALL failed", {
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/reverse-proxy-info", async (req, res) => {
|
||||
try {
|
||||
await logLine("INFO", "SSL REFRESH-ALL requested", { user: HESTIA_USER });
|
||||
|
||||
const raw = await getRawDomainList();
|
||||
const domains = parseDomainsFromTable(raw);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
await logLine("INFO", "Processing domain for refresh-all", { domain });
|
||||
|
||||
const proxyInfo = await getDomainProxyInfo(domain);
|
||||
|
||||
// const result = await refreshSsl(domain);
|
||||
|
||||
// sslCache.del(`ssl-${domain}`);
|
||||
|
||||
// const notify = await notifyNestIfReverseProxy(domain, proxyInfo);
|
||||
|
||||
results.push({ domain, success: true,proxyInfo });
|
||||
// results.push({ domain, success: true, notify, result });
|
||||
} catch (e) {
|
||||
await logLine("ERROR", "reverse-proxy domain failed", {
|
||||
domain,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
|
||||
results.push({
|
||||
domain,
|
||||
success: false,
|
||||
error: e.response?.data || e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: HESTIA_USER,
|
||||
results,
|
||||
});
|
||||
} catch (err) {
|
||||
await logLine("ERROR", "SSL REFRESH-ALL failed", {
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.response?.data || err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3015;
|
||||
app.listen(PORT, async () => {
|
||||
await logLine("INFO", "🔥 SSL Manager API started", {
|
||||
port: PORT,
|
||||
user: HESTIA_USER,
|
||||
serverIp: SERVER_IP,
|
||||
webConfRoot: WEB_CONF_ROOT,
|
||||
logFile: LOG_FILE,
|
||||
nestEndpoint: NEST_PROXY_ENDPOINT,
|
||||
});
|
||||
});
|
||||
1054
package-lock.json
generated
Executable file
1054
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
8
package.json
Executable file
8
package.json
Executable file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"express": "^5.2.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^4.2.1"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user