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.
 
 

250 lines
6.7 KiB

const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse } = require('../util.js');
const { verifyCSR } = require('../ca.js');
/**
* GET /certs
* certs page
*/
exports.certsPage = async (app, req, res) => {
const dbCerts = await db.db.collection('certs')
.find({
username: res.locals.user.username,
}, {
projection: {
_id: 1,
subject: 1,
altnames: 1,
date: 1,
storageName: 1,
}
})
.toArray()
dbCerts.forEach(c => c.date = c.date.toISOString());
const clusterCerts = await res.locals.dataPlane
.getAllStorageSSLCertificates()
.then(certs => {
return certs.data.filter(c => {
const approxSubject = c.storage_name
.replace('_', '.')
.substr(0, c.storage_name.length-4);
return res.locals.user.domains.includes(approxSubject);
});
});
return app.render(req, res, '/certs', {
csrf: req.csrfToken(),
dbCerts,
clusterCerts,
});
};
/**
* GET /certs.json
* certs json data
*/
exports.certsJson = async (req, res) => {
const dbCerts = await db.db.collection('certs')
.find({
username: res.locals.user.username,
}, {
projection: {
_id: 1,
subject: 1,
altnames: 1,
date: 1,
storageName: 1,
}
})
.toArray()
dbCerts.forEach(c => c.date = c.date.toISOString());
const clusterCerts = await res.locals.dataPlane
.getAllStorageSSLCertificates()
.then(certs => {
return certs.data.filter(c => {
const approxSubject = c.storage_name
.replace('_', '.')
.substr(0, c.storage_name.length-4);
return res.locals.user.domains.includes(approxSubject);
});
});
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
dbCerts,
clusterCerts,
});
};
/**
* POST /cert/add
* add cert
*/
exports.addCert = async (req, res, next) => {
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0
|| !res.locals.user.domains.includes(req.body.subject)) {
return dynamicResponse(req, res, 400, { error: 'Invalid subject' });
}
if (!req.body.altnames || typeof req.body.altnames !== 'object'
|| req.body.altnames.some(d => !res.locals.user.domains.includes(d))) {
return dynamicResponse(req, res, 400, { error: 'Invalid altname(s)' });
}
const subject = req.body.subject.toLowerCase();
const altnames = req.body.altnames.map(a => a.toLowerCase());
const backendMap = await res.locals
.dataPlane.showRuntimeMap({
map: process.env.HOSTS_MAP_NAME
})
.then(res => res.data);
const backendDomainEntry = backendMap && backendMap.find(e => e.key === req.body.subject);
if (!backendDomainEntry) {
return dynamicResponse(req, res, 400, { error: 'Add a backend for the domain first before generating a certificate' });
}
const existingCert = await db.db.collection('certs').findOne({ _id: subject });
if (existingCert) {
return dynamicResponse(req, res, 400, { error: 'Cert with this subject already exists' });
}
try {
url.parse(`https://${subject}`);
altnames.forEach(d => {
url.parse(`https://${d}`);
});
} catch (e) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
try {
console.log('Add cert request:', subject, altnames);
const { csr, key, cert, haproxyCert, date } = await acme.generate(subject, altnames);
const { message, description, file, storage_name: storageName } = await res.locals.postFileAll(
'/v2/services/haproxy/storage/ssl_certificates?force_reload=true',
{
method: 'POST',
headers: { 'authorization': res.locals.dataPlane.defaults.headers.authorization },
},
haproxyCert,
{
filename: `${subject}.pem`,
contentType: 'text/plain',
}
);
if (message) {
return dynamicResponse(req, res, 400, { error: message });
}
let update = {
_id: subject,
subject: subject,
altnames: altnames,
username: res.locals.user.username,
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,
});
} catch (e) {
return next(e);
}
return dynamicResponse(req, res, 302, { redirect: req.body.onboarding ? '/onboarding' : '/certs' });
};
/**
* POST /cert/upload
* push existing db cert to cluster
*/
exports.uploadCert = async (req, res, next) => {
if (!req.body.domain || typeof req.body.domain !== 'string' || req.body.domain.length === 0
|| !res.locals.user.domains.includes(req.body.domain)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
const domain = req.body.domain.toLowerCase();
const existingCert = await db.db.collection('certs').findOne({ _id: domain, username: res.locals.user.username });
if (!existingCert || !existingCert.haproxyCert) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
try {
console.log('Upload cert:', existingCert.subject, existingCert.altnames);
const { message } = await res.locals.postFileAll(
'/v2/services/haproxy/storage/ssl_certificates?force_reload=true',
{
method: 'POST',
headers: { 'authorization': res.locals.dataPlane.defaults.headers.authorization },
},
existingCert.haproxyCert,
{
filename: `${existingCert.subject}.pem`,
contentType: 'text/plain',
}
);
if (message) {
return dynamicResponse(req, res, 400, { error: message });
}
} catch (e) {
return next(e);
}
return dynamicResponse(req, res, 302, { redirect: '/certs' });
};
/**
* POST /cert/delete
* delete cers
*/
exports.deleteCert = async (req, res) => {
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0
//|| !res.locals.user.domains.includes(req.body.subject)
) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
const subject = req.body.subject.toLowerCase();
await db.db.collection('certs')
.deleteOne({ _id: subject, username: res.locals.user.username });
return dynamicResponse(req, res, 302, { redirect: '/certs' });
};
/**
* POST /csr/verify
* Delete the map entries of the body 'domain'
*/
exports.verifyUserCSR = (req, res, next) => {
if (res.locals.user.username !== "admin") {
return dynamicResponse(req, res, 403, { error: 'CA signed origin certs are only supported on enterprise plans' });
}
if(!req.body || !req.body.csr || typeof req.body.csr !== 'string' || req.body.csr.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid csr' });
}
try {
const signedCert = verifyCSR(req.body.csr);
return dynamicResponse(req, res, 200, `<pre>${signedCert}</pre>`);
} catch (e) {
return next(e);
}
};