362 lines
8.2 KiB
JavaScript
Executable File
362 lines
8.2 KiB
JavaScript
Executable File
// 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}`);
|
|
});
|