Next.js+React web interface for controlling HAProxy clusters (groups of servers), in conjunction with with https://gitgud.io/fatchan/haproxy-protection.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

126 lines
3.6 KiB

'use strict';
process
.on('uncaughtException', console.error)
.on('unhandledRejection', console.error);
import dotenv from 'dotenv';
await dotenv.config({ path: '.env' });
import * as db from '../db.js';
import FormData from 'form-data';
import agent from '../agent.js';
import * as acme from '../acme.js';
import fetch from 'node-fetch';
import { Resolver } from 'node:dns/promises';
import psl from 'psl';
const resolver = new Resolver();
resolver.setServers(process.env.NAMESERVERS.split(','));
const clusterUrls = process.env.DEFAULT_CLUSTER.split(',').map(u => new URL(u))
, firstClusterURL = clusterUrls[0]
, base64Auth = Buffer.from(`${firstClusterURL.username}:${firstClusterURL.password}`).toString('base64');
async function main() {
await db.connect();
await acme.init();
loop();
}
function getCertsOlderThan(days=60) {
return db.db().collection('certs')
.find({
// _id: '*.zeroddos.net',
date: {
'$lt': new Date(new Date().setDate(new Date().getDate()-days))
},
}, {
date: 1,
subject: 1,
altnames: 1,
})
.toArray();
}
async function postFileAll(path, options, file, fdOptions) {
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
const fd = new FormData();
fd.append('file_upload', file, fdOptions);
return fetch(`${clusterUrl.origin}${path}`, { ...options, body: fd, agent }).then(resp => resp.json());
}));
return promiseResults[0];
}
async function updateCert(dbCert) {
const { subject, altnames, email } = dbCert;
console.log('Renew cert request:', subject, altnames, email);
const { csr, key, cert, haproxyCert, date } = await acme.generate(subject, altnames, email, ['dns-01', 'http-01']);
const { message, description, file, storage_name: storageName } = await postFileAll('/v3/services/haproxy/storage/ssl_certificates', {
method: 'POST',
headers: {
'authorization': `Basic ${base64Auth}`,
},
}, haproxyCert,
{
filename: `${subject}.pem`,
contentType: 'text/plain',
}
);
if (message) {
return console.error('Problem renewing', subject, altnames, 'message:', message);
}
let update = {
_id: subject,
subject: subject,
altnames: altnames,
csr, key, cert, haproxyCert, // cert creation data
date,
};
if (description) {
//may be null due to "already exists", so we keep existing props
update = { ...update, description, file, storageName };
}
await db.db().collection('certs')
.updateOne({
'_id': subject,
}, {
'$set': update,
}, {
'upsert': true,
});
}
async function loop() {
try {
const expiringCerts = await getCertsOlderThan(60);
if (expiringCerts.length === 0) {
console.log('No certs close to expiry');
}
for (const c of expiringCerts) {
console.log('Renewing cert that expires', new Date(new Date(c.date).setDate(new Date(c.date).getDate()+90)), 'for', c.subject, c.altnames.toString());
const rootDomain = psl.parse(c.subject.replace('*', 'x')).domain;
let certDomainNameservers = [];
try {
certDomainNameservers = await resolver.resolve(rootDomain, 'NS');
} catch(e) {
console.warn(e); //probably just no NS records, bad domain
certDomainNameservers = null;
}
if (!certDomainNameservers || certDomainNameservers.some(d => ![ 'ns1.basedns.net', 'ns2.basedns.cloud', 'ns3.basedns.services' ].includes(d))) {
console.warn('Skipping', rootDomain, 'renewal because of incorrect NS records:', certDomainNameservers);
} else {
await updateCert(c);
await new Promise(res => setTimeout(res, 5000));
}
}
} catch(e) {
console.error(e);
console.log('Sleeping for', 60000);
process.exit(-1);
return;
}
console.log('Sleeping for', 3600000);
setTimeout(loop, 3600000);
}
main();