Finish convertion to data plane api, including the global acl. Makes things much more robust, safer, and faster by removing unnecessary api calls.

Remove haproxy-sdk calls and mentions
develop
Thomas Lynch 1 year ago
parent 01b4ce71cc
commit 3f02f5e592
  1. 2
      .env.example
  2. 7
      README.md
  3. 55
      controllers/account.js
  4. 33
      controllers/clusters.js
  5. 1
      controllers/domains.js
  6. 4
      gulpfile.js
  7. 15
      package-lock.json
  8. 1
      package.json
  9. 2
      pages/clusters.js
  10. 13
      router.js
  11. 1
      util.js

@ -10,3 +10,5 @@ DDOS_MAP_NAME="ddos"
BACKENDS_MAP_NAME="backends"
WHITELIST_MAP_NAME="whitelist"
MAINTENANCE_MAP_NAME="maintenance"
DATA_PLANE_USERNAME=admin
DATA_PLANE_PASSWORD=admin

@ -1,11 +1,11 @@
# HAProxy Panel
# BasedFlare Control Panel
Work in progress. Not recommended for production deployment. No instructions or help provided whatsoever.
Internally uses [haproxy-sdk](https://github.com/jackpinetech/haproxy-sdk).
Internally uses [haproxy dataplaneapi](https://github.com/haproxytech/dataplaneapi/).
Intended for use with [haproxy-protection](https://gitgud.io/fatchan/haproxy-protection).
Provides a control panel interface to conveniently manage clusters (groups of identically configured) HAProxy servers. Can be used with a single server cluster. Communicates with the HAProxy socket to update maps, acls, etc.
Provides a control panel interface to conveniently manage clusters (groups of identically configured) HAProxy servers. Can be used with a single server cluster. Uses haproxy runtime apis to update maps, acls, etc.
##### Features:
- List/add/remove clusters (server groups).
@ -19,7 +19,6 @@ Provides a control panel interface to conveniently manage clusters (groups of id
- Maintenance mode, disables proxying for selected domains and serves an "under maintenance" page from haproxy.
##### Todo:
- Update to use haproxy data plane api instead of haproxy-sdk
- Better Multi-user support (problems w/ ip whitelist and blacklist)
- Some kind of payment system
- SSL cert management, or letsencrypt integration

@ -9,27 +9,20 @@ exports.accountData = async (req, res, next) => {
let maps = []
, globalAcl;
if (res.locals.user.clusters.length > 0) {
maps = await res.locals.dataPlane.getAllRuntimeMapFiles()
maps = await res.locals.dataPlane
.getAllRuntimeMapFiles()
.then(res => res.data)
.then(data => data.map(extractMap))
.then(maps => maps.filter(n => n))
.then(maps => maps.sort((a, b) => a.fname.localeCompare(b.fname)));
const globalIndex = await res.locals.dataPlane.getAcls({
parent_name: 'http-in',
parent_type:'frontend'
})
.then(res => res.data.data)
.then(acls => acls.find(a => a.acl_name === 'ddos_mode_enabled_override').index)
globalAcl = await res.locals.dataPlane.getAcl({
index: globalIndex,
parent_name: 'http-in',
parent_type:'frontend'
})
.then(res => res.data.data)
globalAcl = await res.locals.dataPlane
.getOneRuntimeMap('ddos_global')
.then(res => res.data.description.split('').reverse()[0])
}
return {
csrf: req.csrfToken(),
maps,
globalAcl: globalAcl && globalAcl.value.endsWith(0),
globalAcl: globalAcl === '1',
}
};
@ -59,24 +52,26 @@ exports.globalToggle = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
return dynamicResponse(req, res, 403, { error: 'Only admin can toggle global' });
}
let globalIndex;
try {
await res.locals.haproxy
.showAcl()
.then(list => {
const hdrCntAcl = list.find(x => x.includes("acl 'hdr_cnt'"));
if (hdrCntAcl != null) {
globalIndex = hdrCntAcl.split(' ')[0];
}
});
const globalAcl = await res.locals.haproxy
.showAcl(globalIndex);
if (globalAcl.length === 1 && globalAcl[0].endsWith(0)) {
await res.locals.haproxy
.clearAcl(globalIndex);
const globalAcl = await res.locals.dataPlane
.getOneRuntimeMap('ddos_global')
.then(res => res.data.description.split('').reverse()[0])
if (globalAcl === '1') {
await res.locals.dataPlane
.deleteRuntimeMapEntry({
map: 'ddos_global',
id: 'true'
});
} else {
await res.locals.haproxy
.addAcl(globalIndex, '0');
await res.locals.dataPlane
.addPayloadRuntimeMap({
name: 'ddos_global'
}, [
{
key: 'true',
value: 'true'
}
]);
}
} catch (e) {
return next(e);

@ -14,39 +14,6 @@ exports.clustersJson = async (req, res, next) => {
});
}
/**
* POST /global/toggle
* toggle global ACL
*/
exports.globalToggle = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
res.status(403).send('only admin can toggle global');
}
let globalIndex;
try {
await res.locals.haproxy
.showAcl()
.then(list => {
const hdrCntAcl = list.find(x => x.includes("acl 'hdr_cnt'"));
if (hdrCntAcl != null) {
globalIndex = hdrCntAcl.split(' ')[0];
}
});
const globalAcl = await res.locals.haproxy
.showAcl(globalIndex);
if (globalAcl.length === 1 && globalAcl[0].endsWith(0)) {
await res.locals.haproxy
.clearAcl(globalIndex);
} else {
await res.locals.haproxy
.addAcl(globalIndex, '0');
}
} catch (e) {
return next(e);
}
return dynamicResponse(req, res, 302, { redirect: '/account' });
};
/**
* POST /cluster
* set active cluster

@ -60,7 +60,6 @@ exports.deleteDomain = async (req, res) => {
//will fail if domain is only in the hosts map for a different cluster, so we wont do it (for now)
//but will cause permission problems "invalid input" when trying to delete it from the other cluster later... hmmm...
//await deleteFromMap(res.locals.haproxy, process.env.HOSTS_MAP_NAME, [req.body.domain]);
await db.db.collection('accounts')
.updateOne({_id: res.locals.user.username}, {$pull: {domains: req.body.domain }});

@ -16,7 +16,7 @@ async function reset() {
_id: 'admin',
passwordHash: passwordHash,
domains: ['localhost'],
clusters: ['tcp://127.0.0.1:2000'],
clusters: ['http://127.0.0.1:2001'],
activeCluster: 0,
balance: 0,
});
@ -28,7 +28,7 @@ async function reset() {
$set: {
passwordHash,
domains: ['localhost'],
clusters: ['tcp://127.0.0.1:2000'],
clusters: ['http://127.0.0.1:2001'],
activeCluster: 0,
}
});

15
package-lock.json generated

@ -9,7 +9,6 @@
"version": "1.0.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@fatchan/haproxy-sdk": "^1.0.6",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.0",
"bootstrap": "^5.2.3",
@ -1226,15 +1225,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/@fatchan/haproxy-sdk": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@fatchan/haproxy-sdk/-/haproxy-sdk-1.0.6.tgz",
"integrity": "sha512-VJbQGtGuxNhpcmYQI9kXAKHpmWXqQ9hs0K+GzIDG7pJ2dwBwaDf71hcezvehFhgmQMqD7dDMZG+1QkMCJDbiMA==",
"engines": {
"node": ">=8.15.1",
"npm": ">=6.4.1"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
@ -10888,11 +10878,6 @@
}
}
},
"@fatchan/haproxy-sdk": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@fatchan/haproxy-sdk/-/haproxy-sdk-1.0.6.tgz",
"integrity": "sha512-VJbQGtGuxNhpcmYQI9kXAKHpmWXqQ9hs0K+GzIDG7pJ2dwBwaDf71hcezvehFhgmQMqD7dDMZG+1QkMCJDbiMA=="
},
"@humanwhocodes/config-array": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",

@ -14,7 +14,6 @@
"author": "Thomas Lynch (fatchan) <tom@69420.me>",
"license": "AGPL-3.0-only",
"dependencies": {
"@fatchan/haproxy-sdk": "^1.0.6",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.0",
"bootstrap": "^5.2.3",

@ -82,7 +82,7 @@ export default function Clusters(props) {
<form className="d-flex" onSubmit={addCluster} action="/forms/cluster/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="cluster" placeholder="tcp://host1:port,tcp://host2:port,..." required />
<input className="form-control mx-3" type="text" name="cluster" placeholder="http://host1:port,http://host2:port,..." required />
</form>
</td>
</tr>

@ -60,20 +60,22 @@ const testRouter = (server, app) => {
return next();
}
try {
//uses cluster from account
res.locals.haproxy = new HAProxy(res.locals.user.clusters[res.locals.user.activeCluster]);
//TODO: handle cluster
//const cluster = res.locals.user.clusters[res.locals.user.activeCluster]
res.locals.fMap = server.locals.fMap;
res.locals.mapValueNames = server.locals.mapValueNames;
const base64Auth = new Buffer(`${process.env.DATA_PLANE_USERNAME}:${process.env.DATA_PLANE_PASSWORD}`).toString("base64");
const firstCluster = res.locals.user.clusters[res.locals.user.activeCluster].split(',')[0];
const api = new OpenAPIClientAxios({
definition: 'http://127.0.0.1:2001/v2/specification_openapiv3',
definition: `${firstCluster}/v2/specification_openapiv3`,
axiosConfigDefaults: {
headers: {
'authorization': 'Basic YWRtaW46YWRtaW4=', // admin:admin for testing,
'authorization': `Basic ${base64Auth}`,
}
}
});
res.locals.dataPlane = await api.init();
res.locals.dataPlane.defaults.baseURL = 'http://127.0.0.1:2001/v2';
res.locals.dataPlane.defaults.baseURL = `${firstCluster}/v2`;
next();
} catch (e) {
return dynamicResponse(req, res, 500, { error: e });
@ -81,7 +83,6 @@ const testRouter = (server, app) => {
};
const hasCluster = (req, res, next) => {
console.log(req.path)
if (res.locals.user.clusters.length > 0 || (req.baseUrl+req.path) === '/forms/cluster/add') {
return next();
}

@ -55,6 +55,7 @@ module.exports = {
extractMap: (item) => {
const name = item.file && item.file.match(/\/etc\/haproxy\/map\/(?<name>.+).map/).groups.name;
if (!fMap[name]) { return null; }
const count = item.description && item.description.match(/(?:.+entry_cnt=(?<count>\d+)$)?/).groups.count;
return {
name,

Loading…
Cancel
Save