Implement acme client, no writing to db/uploading with dataplane (YET)

Speed improvements, some parallel dataplane calls e.g. account page
Fix registration
Change cluster -> server wording for now on frontend
Frontend dixes re overflowing server line
develop
Thomas Lynch 1 year ago
parent fd2989a435
commit bf3c71b7df
  1. 100
      acme.js
  2. 7
      controllers/account.js
  3. 6
      controllers/domains.js
  4. 124
      package-lock.json
  5. 1
      package.json
  6. 43
      pages/_app.js
  7. 20
      pages/account.js
  8. 6
      pages/clusters.js
  9. 2
      pages/register.js
  10. 15
      providers/GlobalProvider.js
  11. 14
      providers/GlobalReducer.js
  12. 2
      server.js

@ -0,0 +1,100 @@
'use strict';
const fs = require('fs').promises;
const acme = require('acme-client');
/**
* Function used to satisfy an ACME challenge
*
* @param {object} authz Authorization object
* @param {object} challenge Selected challenge
* @param {string} keyAuthorization Authorization key
* @returns {Promise}
*/
async function challengeCreateFn(authz, challenge, keyAuthorization) {
console.log('Triggered challengeCreateFn()');
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/tmp/acme-tests/.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
console.log(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
await fs.writeFile(filePath, fileContents);
}
/* dns-01 */
else if (challenge.type === 'dns-01') {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
console.log(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
console.log(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue);
}
}
/**
* Function used to remove an ACME challenge response
*
* @param {object} authz Authorization object
* @param {object} challenge Selected challenge
* @param {string} keyAuthorization Authorization key
* @returns {Promise}
*/
async function challengeRemoveFn(authz, challenge, keyAuthorization) {
console.log('Triggered challengeRemoveFn()');
/* http-01 */
if (challenge.type === 'http-01') {
const filePath = `/tmp/acme-tests/.well-known/acme-challenge/${challenge.token}`;
console.log(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
await fs.unlink(filePath);
}
/* dns-01 */
else if (challenge.type === 'dns-01') {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
const recordValue = keyAuthorization;
console.log(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
/* Replace this */
console.log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.removeRecord(dnsRecord, 'TXT');
}
}
module.exports = {
client: null,
init: async function() {
/* Init client */
module.exports.client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
});
},
generate: async function(domain, email='tom@69420.me') {
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: domain
});
/* Certificate */
const cert = await module.exports.client.auto({
csr,
email,
termsOfServiceAgreed: true,
challengeCreateFn,
challengeRemoveFn
});
/* Done */
const haproxyCert = `${cert.toString()}\n${key.toString()}`;
return { key, csr, cet, haproxyCert, date: new Date() };
},
};

@ -9,16 +9,17 @@ exports.accountData = async (req, res, next) => {
let maps = []
, globalAcl;
if (res.locals.user.clusters.length > 0) {
maps = await res.locals.dataPlane
maps = 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)));
globalAcl = await res.locals.dataPlane
globalAcl = res.locals.dataPlane
.getOneRuntimeMap('ddos_global')
.then(res => res.data.description.split('').reverse()[0])
}
([maps, globalAcl] = await Promise.all([maps, globalAcl]));
return {
csrf: req.csrfToken(),
maps,
@ -86,7 +87,7 @@ exports.globalToggle = async (req, res, next) => {
exports.login = async (req, res) => {
const username = req.body.username.toLowerCase();
const password = req.body.password;
const account = await db.db.collection('accounts').findOne({_id:username});
const account = await db.db.collection('accounts').findOne({ _id: username });
if (!account) {
return dynamicResponse(req, res, 403, { error: 'Incorrect username or password' });
}

@ -1,4 +1,5 @@
const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse } = require('../util.js');
@ -39,7 +40,10 @@ exports.addDomain = async (req, res) => {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
//todo: somehow enforce payment? or probably not for now, lol
//const { csr, key, cert, haproxyCert, date } = await acme.generate(req.body.domain);
//TODO: save (replace) in db with domain as key and username.
//TODO: add scheduled task to aggregate domains and upload certs to clusters of that username through dataplane
//TODO: make scheduled task also run this again for certs close to expiry and repeat ^
await db.db.collection('accounts')
.updateOne({_id: res.locals.user.username}, {$addToSet: {domains: req.body.domain }});

124
package-lock.json generated

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "AGPL-3.0-only",
"dependencies": {
"acme-client": "^5.0.0",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.0",
"bootstrap": "^5.2.3",
@ -1747,6 +1748,50 @@
"node": ">= 0.6"
}
},
"node_modules/acme-client": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/acme-client/-/acme-client-5.0.0.tgz",
"integrity": "sha512-sNeNiBKoxfLZi3wkEp27vq7coiuKBFz9LYBK00sWcODYupeHwMk8sXJOqLola9gkpTVdo569ApDqbwVa2FAd4w==",
"dependencies": {
"axios": "0.27.2",
"debug": "^4.1.1",
"jsrsasign": "^10.5.26",
"node-forge": "^1.3.1"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/acme-client/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/acme-client/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/acme-client/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@ -2337,8 +2382,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"peer": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/atob": {
"version": "2.1.2",
@ -2974,7 +3018,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -3334,7 +3377,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
@ -4727,7 +4769,6 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"peer": true,
"engines": {
"node": ">=4.0"
},
@ -4769,7 +4810,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@ -6105,6 +6145,14 @@
"verror": "1.10.0"
}
},
"node_modules/jsrsasign": {
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.6.1.tgz",
"integrity": "sha512-emiQ05haY9CRj1Ho/LiuCqr/+8RgJuWdiHYNglIg2Qjfz0n+pnUq9I2QHplXuOMO2EnAW1oCGC1++aU5VoWSlw==",
"funding": {
"url": "https://github.com/kjur/jsrsasign#donations"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@ -7017,6 +7065,14 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@ -11234,6 +11290,41 @@
"negotiator": "0.6.3"
}
},
"acme-client": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/acme-client/-/acme-client-5.0.0.tgz",
"integrity": "sha512-sNeNiBKoxfLZi3wkEp27vq7coiuKBFz9LYBK00sWcODYupeHwMk8sXJOqLola9gkpTVdo569ApDqbwVa2FAd4w==",
"requires": {
"axios": "0.27.2",
"debug": "^4.1.1",
"jsrsasign": "^10.5.26",
"node-forge": "^1.3.1"
},
"dependencies": {
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@ -11676,8 +11767,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"peer": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"atob": {
"version": "2.1.2",
@ -12146,7 +12236,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"peer": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@ -12425,8 +12514,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"peer": true
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"delegates": {
"version": "1.0.0",
@ -13534,8 +13622,7 @@
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"peer": true
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"for-each": {
"version": "0.3.3",
@ -13563,7 +13650,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@ -14538,6 +14624,11 @@
"verror": "1.10.0"
}
},
"jsrsasign": {
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.6.1.tgz",
"integrity": "sha512-emiQ05haY9CRj1Ho/LiuCqr/+8RgJuWdiHYNglIg2Qjfz0n+pnUq9I2QHplXuOMO2EnAW1oCGC1++aU5VoWSlw=="
},
"jsx-ast-utils": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
@ -15224,6 +15315,11 @@
}
}
},
"node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
},
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",

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

@ -2,31 +2,28 @@ import 'bootstrap/dist/css/bootstrap.css';
import NProgress from 'nprogress';
import Layout from '../components/Layout.js';
import "nprogress/nprogress.css";
import GlobalProvider from '../providers/GlobalProvider.js';
export default function App({ Component, pageProps }) {
return (
<GlobalProvider>
<Layout>
<style>
{`
html, body { font-family: arial,helvetica,sans-serif; height: 100%; }
.green { color: green; }
.red { color: red; }
footer { margin-top: auto; }
.btn { font-weight: bold; }
@media (prefers-color-scheme: dark) {
:root { --bs-body-color: #fff; --bs-body-bg: #000000; }
.text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; }
.list-group-item { color: #fff; background-color: #111111; }
input:not(.btn), option, select.form-select { color: #fff!important; background-color: #111111!important; border: 1px solid black!important; }
.list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; }
.table { color: #fff; border-color: transparent !important; }
}
`}
</style>
<Component {...pageProps} />
</Layout>
</GlobalProvider>
<Layout>
<style>
{`
html, body { font-family: arial,helvetica,sans-serif; height: 100%; }
.green { color: green; }
.red { color: red; }
footer { margin-top: auto; }
.btn { font-weight: bold; }
@media (prefers-color-scheme: dark) {
:root { --bs-body-color: #fff; --bs-body-bg: #000000; }
.text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; }
.list-group-item { color: #fff; background-color: #111111; }
input:not(.btn), option, select.form-select { color: #fff!important; background-color: #111111!important; border: 1px solid black!important; }
.list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; }
.table { color: #fff; border-color: transparent !important; }
}
`}
</style>
<Component {...pageProps} />
</Layout>
);
}

@ -19,7 +19,7 @@ const Account = (props) => {
API.getAccount(dispatch, setError, router);
}
}, [state.user, router]);
const loadingSection = useMemo(() => {
return (
<div className="list-group-item list-group-item-action d-flex align-items-start">
@ -31,7 +31,7 @@ const Account = (props) => {
let innerData;
if (state.maps != null) {
const { user, maps, globalAcl, csrf } = state;
// isAdmin for showing global override option
@ -81,22 +81,26 @@ const Account = (props) => {
<div className="flex-row d-flex w-100">
<div className="ms-2 me-auto">
<div className="fw-bold">
Manage Clusters
Manage Servers
<span className="fw-normal">
{' '}- Add/Delete/Select a cluster
{' '}- Add/Delete/Select server
</span>
</div>
</div>
<span className="ml-auto badge bg-info rounded-pill" style={{ maxHeight: "1.6em" }}>
Cluster: 1
Managing: 1
</span>
</div>
<div className="d-flex w-100 justify-content-between mt-2">
<div className="ms-2">
<div className="fw-bold">
<div className="ms-2 overflow-hidden">
<div className="fw-bold overflow-hidden text-truncate">
Servers ({user.clusters.length === 0 ? 0 : user.clusters[user.activeCluster].split(',').length})
{user.clusters.length > 0 && (<span className="fw-normal">
: {user.clusters[user.activeCluster].split(',').map(x => x.substring(0, x.length/2)+'...').join(', ')}
: {user.clusters[user.activeCluster].split(',').map(x => {
const cUrl = new URL(x);
cUrl.password = ''; //visual only
return new URL(cUrl).toString();
}).join(', ')}
</span>)}
</div>
</div>

@ -60,13 +60,13 @@ export default function Clusters(props) {
return (
<>
<Head>
<title>Clusters</title>
<title>Servers</title>
</Head>
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
Clusters ({user.clusters.length}):
Servers ({user.clusters.length}):
</h5>
{/* Clusters table */}
@ -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="http://host1:port,http://host2:port,..." required />
<input className="form-control mx-3" type="text" name="cluster" placeholder="http://username:password@host:port" required />
</form>
</td>
</tr>

@ -14,7 +14,7 @@ const Register = () => {
await API.register({
username: e.target.username.value,
password: e.target.password.value,
rpasword: e.target.repeat_password.value,
repeat_password: e.target.repeat_password.value,
}, null, setError, router);
router.push('/login');
}

@ -1,15 +0,0 @@
import { createContext, useReducer } from 'react';
import reducer from './GlobalReducer.js';
export const GlobalContext = createContext();
const initialState = {};
export default function GlobalProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<GlobalContext.Provider value={[state, dispatch]}>
{props.children}
</GlobalContext.Provider>
);
}

@ -1,14 +0,0 @@
// reducer.js
export default function reducer(state, action) {
//could do somethig na bit more smart than this
switch (action.type) {
case 'error':
// Add error, keep existing state
return { error: action.payload, ...state };
case 'state':
// Keep state, overwrite or add new values from payload, and null error
return { ...state, ...action.payload, error: null };
default:
throw new Error();
}
}

@ -17,12 +17,14 @@ const server = require('express')
, express = require('express')
, bodyParser = require('body-parser')
, cookieParser = require('cookie-parser')
, acme = require('./acme.js')
, db = require('./db.js');
app.prepare()
.then(async () => {
await db.connect();
await acme.init();
const server = express();
server.set('query parser', 'simple');

Loading…
Cancel
Save