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.
182 lines
8.9 KiB
182 lines
8.9 KiB
const express = require('express')
|
|
, dev = process.env.NODE_ENV !== 'production'
|
|
, session = require('express-session')
|
|
, MongoStore = require('connect-mongo')
|
|
, db = require('./db.js')
|
|
, csrf = require('csurf')
|
|
, OpenAPIClientAxios = require('openapi-client-axios').default
|
|
, { dynamicResponse } = require('./util.js')
|
|
, definition = require('./openapi-definition.js');
|
|
|
|
const testRouter = (server, app) => {
|
|
|
|
const sessionStore = session({
|
|
secret: process.env.COOKIE_SECRET,
|
|
store: MongoStore.create({ mongoUrl: process.env.DB_URL }),
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
rolling: true,
|
|
cookie: {
|
|
httpOnly: true,
|
|
secure: !dev, //TODO: check https
|
|
sameSite: 'strict',
|
|
maxAge: 1000 * 60 * 60 * 24 * 30, //month
|
|
}
|
|
});
|
|
|
|
const useSession = (req, res, next) => {
|
|
sessionStore(req, res, next);
|
|
};
|
|
|
|
const fetchSession = async (req, res, next) => {
|
|
if (req.session.user) {
|
|
const account = await db.db.collection('accounts')
|
|
.findOne({ _id: req.session.user });
|
|
if (account) {
|
|
const numCerts = await db.db.collection('certs')
|
|
.countDocuments({ username: account._id });
|
|
const strippedClusters = account.clusters
|
|
.map(c => {
|
|
return c.split(',')
|
|
.map(clusterString => {
|
|
const clusterUrl = new URL(clusterString);
|
|
clusterUrl.username = '';
|
|
clusterUrl.password = '';
|
|
return clusterUrl.toString();
|
|
})
|
|
.join(',');
|
|
});
|
|
res.locals.clusters = account.clusters;
|
|
res.locals.user = {
|
|
username: account._id,
|
|
domains: account.domains,
|
|
clusters: strippedClusters,
|
|
activeCluster: account.activeCluster,
|
|
numCerts,
|
|
};
|
|
return next();
|
|
}
|
|
req.session.destroy();
|
|
}
|
|
next();
|
|
};
|
|
|
|
const checkSession = (req, res, next) => {
|
|
if (!res.locals.user) {
|
|
return dynamicResponse(req, res, 302, { redirect: '/login' });
|
|
}
|
|
next();
|
|
};
|
|
|
|
const csrfMiddleware = csrf();
|
|
|
|
//dataplaneapi middleware
|
|
const useHaproxy = async (req, res, next) => {
|
|
if (res.locals.clusters.length === 0) {
|
|
return next();
|
|
}
|
|
try {
|
|
res.locals.fMap = server.locals.fMap;
|
|
res.locals.mapValueNames = server.locals.mapValueNames;
|
|
const clusterUrls = res.locals.clusters[res.locals.user.activeCluster]
|
|
.split(',')
|
|
.map(u => new URL(u));
|
|
const firstClusterURL = clusterUrls[0];
|
|
|
|
//NOTE: all servers in cluster must have same credentials for now
|
|
const base64Auth = Buffer.from(`${firstClusterURL.username}:${firstClusterURL.password}`).toString("base64");
|
|
const api = new OpenAPIClientAxios({
|
|
//definition: `${firstClusterURL.origin}/v2/specification_openapiv3`,
|
|
definition,
|
|
axiosConfigDefaults: {
|
|
headers: {
|
|
'authorization': `Basic ${base64Auth}`,
|
|
}
|
|
}
|
|
});
|
|
const apiInstance = api.initSync();
|
|
apiInstance.defaults.baseURL = `${firstClusterURL.origin}/v2`;
|
|
res.locals.dataPlane = apiInstance;
|
|
|
|
res.locals.dataPlaneAll = async (operationId, parameters, data, config, all=false) => {
|
|
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
|
|
const singleApi = new OpenAPIClientAxios({ definition, axiosConfigDefaults: { headers: { 'authorization': `Basic ${base64Auth}` } } });
|
|
const singleApiInstance = singleApi.initSync();
|
|
singleApiInstance.defaults.baseURL = `${clusterUrl.origin}/v2`;
|
|
return singleApiInstance[operationId](parameters, data, { ...config, baseUrl: `${clusterUrl.origin}/v2` });
|
|
}));
|
|
return all ? promiseResults.map(p => p.data) : promiseResults[0]; //TODO: better desync handling
|
|
}
|
|
res.locals.fetchAll = async (path, options) => {
|
|
//used for stuff that dataplaneapi with axios seems to struggle with e.g. multipart body
|
|
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
|
|
return fetch(`${clusterUrl.origin}${path}`, options).then(resp => resp.json());
|
|
}));
|
|
return promiseResults[0]; //TODO: better desync handling
|
|
}
|
|
next();
|
|
} catch (e) {
|
|
console.error(e)
|
|
return dynamicResponse(req, res, 500, { error: e });
|
|
}
|
|
};
|
|
|
|
const hasCluster = (req, res, next) => {
|
|
if (res.locals.user.clusters.length > 0 || (req.baseUrl+req.path) === '/forms/cluster/add') {
|
|
return next();
|
|
}
|
|
return dynamicResponse(req, res, 302, { redirect: '/clusters' });
|
|
};
|
|
|
|
//Controllers
|
|
const accountController = require('./controllers/account')
|
|
, mapsController = require('./controllers/maps')
|
|
, clustersController = require('./controllers/clusters')
|
|
, certsController = require('./controllers/certs')
|
|
, domainsController = require('./controllers/domains');
|
|
|
|
//unauthed pages
|
|
server.get('/', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/index') });
|
|
server.get('/login', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/login') });
|
|
server.get('/register', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/register') });
|
|
|
|
//register/login/logout forms
|
|
server.post('/forms/login', useSession, accountController.login);
|
|
server.post('/forms/logout', useSession, accountController.logout);
|
|
server.post('/forms/register', useSession, accountController.register);
|
|
|
|
const mapNames = [process.env.BLOCKED_MAP_NAME, process.env.MAINTENANCE_MAP_NAME, process.env.WHITELIST_MAP_NAME,
|
|
process.env.BACKENDS_MAP_NAME, process.env.DDOS_MAP_NAME, process.env.HOSTS_MAP_NAME, process.env.REWRITE_MAP_NAME]
|
|
, mapNamesOrString = mapNames.join('|');
|
|
|
|
//authed pages
|
|
server.get('/account', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.accountPage.bind(null, app));
|
|
server.get('/onboarding', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.onboardingPage.bind(null, app));
|
|
server.get('/account.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.accountJson);
|
|
server.get(`/map/:name(${mapNamesOrString})`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.mapPage.bind(null, app));
|
|
server.get(`/map/:name(${mapNamesOrString}).json`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.mapJson);
|
|
server.get('/clusters', useSession, fetchSession, checkSession, csrfMiddleware, clustersController.clustersPage.bind(null, app));
|
|
server.get('/clusters.json', useSession, fetchSession, checkSession, csrfMiddleware, clustersController.clustersJson);
|
|
server.get('/domains', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsPage.bind(null, app));
|
|
server.get('/domains.json', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsJson);
|
|
server.get('/certs', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, certsController.certsPage.bind(null, app));
|
|
server.get('/certs.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, certsController.certsJson);
|
|
server.get('/stats', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsPage.bind(null, app));
|
|
server.get('/stats.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsJson);
|
|
const clusterRouter = express.Router({ caseSensitive: true });
|
|
clusterRouter.post('/global/toggle', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, accountController.globalToggle);
|
|
clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.patchMapForm); //add to MAP
|
|
clusterRouter.post(`/map/:name(${mapNamesOrString})/delete`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.deleteMapForm); //delete from MAP
|
|
clusterRouter.post('/cluster', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.setCluster);
|
|
clusterRouter.post('/cluster/add', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.addCluster);
|
|
clusterRouter.post('/cluster/delete', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.deleteClusters);
|
|
clusterRouter.post('/domain/add', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, domainsController.addDomain);
|
|
clusterRouter.post('/domain/delete', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, domainsController.deleteDomain);
|
|
clusterRouter.post('/cert/add', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.addCert);
|
|
clusterRouter.post('/cert/upload', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.uploadCert);
|
|
clusterRouter.post('/cert/delete', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, certsController.deleteCert);
|
|
server.use('/forms', clusterRouter);
|
|
|
|
};
|
|
|
|
module.exports = testRouter;
|
|
|