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.
 
 

264 lines
8.8 KiB

const { deleteFromMap, getMapId, dynamicResponse } = require('../util.js');
const { createCIDR, parse } = require('ip6addr');
const url = require('url');
/**
* GET /maps/:name
* Show map filtering to users domains
*/
exports.mapData = async (req, res, next) => {
let map,
mapId,
showValues = false;
try {
mapId = await getMapId(res.locals.haproxy, req.params.name);
if (!mapId) {
return dynamicResponse(req, res, 400, { error: 'Invalid map' });
}
map = await res.locals.haproxy
.showMap(mapId.index);
} catch (e) {
return next(e);
}
switch (req.params.name) {
case process.env.DDOS_MAP_NAME:
showValues = true;
case process.env.BACKENDS_MAP_NAME:
case process.env.HOSTS_MAP_NAME:
if (process.env.CUSTOM_BACKENDS_ENABLED) {
showValues = true;
}
case process.env.MAINTENANCE_MAP_NAME:
map = map.filter(a => {
const [id, key, value] = a.split(' ');
const { hostname, pathname } = url.parse(`https://${key}`);
return res.locals.user.domains.includes(hostname);
});
break;
case process.env.BLOCKED_MAP_NAME:
case process.env.WHITELIST_MAP_NAME:
map = map.filter(a => {
const [id, key, value] = a.split(' ');
return res.locals.user.username === value;
});
break;
default:
return dynamicResponse(req, res, 400, { error: 'Invalid map' });
}
return {
mapValueNames: { '0': 'None', '1': 'Proof-of-work', '2': 'hCaptcha' },
mapId,
map,
csrf: req.csrfToken(),
name: req.params.name,
showValues,
};
};
exports.mapPage = async (app, req, res, next) => {
const data = await exports.mapData(req, res, next);
return app.render(req, res, '/map/[mapname]', data);
};
exports.mapJson = async (req, res, next) => {
const data = await exports.mapData(req, res, next);
return res.json({ ...data, user: res.locals.user });
};
/**
* POST /maps/:name/delete
* Delete the map entries of the body 'domain'
*/
exports.deleteMapForm = async (req, res, next) => {
if(!req.body || !req.body.key || typeof req.body.key !== 'string' || req.body.key.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid value' });
}
if (req.params.name === process.env.HOSTS_MAP_NAME
|| req.params.name === process.env.DDOS_MAP_NAME
|| req.params.name === process.env.MAINTENANCE_MAP_NAME) {
const { hostname } = url.parse(`https://${req.body.key}`);
const allowed = res.locals.user.domains.includes(hostname);
if (!allowed) {
return dynamicResponse(req, res, 403, { error: 'No permission for that domain' });
}
} else if (req.params.name === process.env.BLOCKED_MAP_NAME
|| req.params.name === process.env.WHITELIST_MAP_NAME) {
//permission check, see https://gitgud.io/fatchan/haproxy-panel/-/issues/10
}
try {
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
//refactor -> getServer(hostname)
const backendMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
const backendMapEntry = await res.locals.haproxy
.showMap(backendMapId.index)
.then(map => map.find(m => m.split(' ')[1] === req.body.key));
if (backendMapEntry) {
const serverName = backendMapEntry.split(' ')[2];
const server = await res.locals.haproxy
.backend(process.env.BACKEND_NAME)
.then(backend => backend.server(serverName));
await Promise.all([
server.setState('disable'),
//server.setAddress(),
//server.setPort(),
deleteFromMap(res.locals.haproxy, process.env.BACKENDS_MAP_NAME, req.body.key),
]);
} else {
console.warn('no backend found to remove');
//dont return because otherwise they will have a domain stuck in the hosts map
}
}
await deleteFromMap(res.locals.haproxy, req.params.name, req.body.key);
return dynamicResponse(req, res, 302, { redirect: `/map/${req.params.name}` });
} catch (e) {
return next(e);
}
};
/**
* POST /maps/:name/add
* Add map entries of the body 'domain'
*/
exports.patchMapForm = async (req, res, next) => {
if(req.body && req.body.key && typeof req.body.key === 'string') {
//ddos must have valid 0, 1, 2
if (req.params.name === process.env.DDOS_MAP_NAME
&& (!req.body.value || !['0', '1', '2'].includes(req.body.value))) {
return dynamicResponse(req, res, 400, { error: 'Invalid value' });
}
//ddos and hosts must have valid hostname
if (req.params.name === process.env.DDOS_MAP_NAME
|| req.params.name === process.env.HOSTS_MAP_NAME
|| req.params.name === process.env.MAINTENANCE_MAP_NAME) {
const { hostname, pathname } = url.parse(`https://${req.body.key}`);
const allowed = res.locals.user.domains.includes(hostname);
if (!allowed) {
return dynamicResponse(req, res, 403, { error: 'No permission for that domain' });
}
}
//validate key is valid ip address
if (req.params.name === process.env.BLOCKED_MAP_NAME
|| req.params.name === process.env.WHITELIST_MAP_NAME) {
let parsedIp, parsedSubnet;
try {
parsedIp = parse(req.body.key);
} catch (e) { parsedIp = null; /*invalid ip, or a subnet*/ }
try {
parsedSubnet = createCIDR(req.body.key);
} catch (e) { parsedSubnet = null; /*invalid subnet or just an ip*/ }
const parsedIpOrSubnet = parsedIp || parsedSubnet;
if (!parsedIpOrSubnet) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
req.body.key = parsedIpOrSubnet.toString({zeroElide: false, zeroPad:false});
}
//validate value is IP:port
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
let parsedValue;
try {
parsedValue = url.parse(`https://${req.body.value}`)
if (!parsedValue.host || !parsedValue.port) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
parse(parsedValue.hostname); //better ip parsing, will error if invalid
} catch (e) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
req.body.value = parsedValue.host; //host includes port
}
let value;
switch (req.params.name) {
case process.env.DDOS_MAP_NAME:
value = req.body.value;
break;
case process.env.HOSTS_MAP_NAME:
if (process.env.CUSTOM_BACKENDS_ENABLED) {
value = req.body.value;
} else {
value = 0;
}
break;
case process.env.BLOCKED_MAP_NAME:
case process.env.WHITELIST_MAP_NAME:
case process.env.MAINTENANCE_MAP_NAME:
value = res.locals.user.username;
break;
default:
return dynamicResponse(req, res, 400, { error: 'Invalid map' });
}
try {
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
//refactor -> getServer(hostname)
const backendMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
let backendMapSize;
const backendMapEntry = await res.locals.haproxy
.showMap(backendMapId.index)
.then(map => {
backendMapSize = map.length;
return map.find(m => m.split(' ')[1] === req.body.key)
});
const backend = await res.locals.haproxy
.backend(process.env.BACKEND_NAME);
let server;
if (backendMapEntry) {
return dynamicResponse(req, res, 400, { error: `this domain is active already and has a backend server mapping: "${backendMapEntry}"` });
} else {
//no existing backend map entry (i.e. didnt exist at startup to get constructed in the lua script)
let backendCounter = 0;
let backendMapCheckId = 1;
const maxServers = (await backend.servers()).length;
if (backendMapSize > 0 && backendMapSize < maxServers) {
//try and skip to an empty index for speed improvement.
//will depend if any early servers are removed, but probably will be faster overall.
backendMapCheckId = backendMapSize;
}
while (backendCounter < maxServers) {
try {
server = await backend.server(`${process.env.SERVER_PREFIX}${backendMapCheckId}`);
const status = await server.status();
if (status === 'MAINT') { //would atively used servers ever enter this state?
break;
}
} catch (e) {
server = null; //probably out of servers
}
backendMapCheckId = (backendMapCheckId+1) % maxServers;
backendCounter++;
}
if (!server) {
return dynamicResponse(req, res, 400, { error: 'No server slots available' });
}
const backendsMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
await res.locals.haproxy
.addMap(backendsMapId.index, req.body.key, server.name);
}
await server.setState('enable');
await server.setAddress(value.split(':')[0]);
await server.setPort(value.split(':')[1]);
}
const mapId = await getMapId(res.locals.haproxy, req.params.name);
await res.locals.haproxy
.addMap(mapId.index, req.body.key, value);
return dynamicResponse(req, res, 302, { redirect: `/map/${req.params.name}` });
} catch (e) {
return next(e);
}
}
return dynamicResponse(req, res, 400, { error: 'Invalid value' });
};