mirror of https://github.com/certd/certd
169 lines
5.0 KiB
JavaScript
169 lines
5.0 KiB
JavaScript
/**
|
|
* Example using http-01 challenge to generate certificates on-demand
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const tls = require('tls');
|
|
const acme = require('./../../');
|
|
|
|
const HTTP_SERVER_PORT = 80;
|
|
const HTTPS_SERVER_PORT = 443;
|
|
const VALID_DOMAINS = ['example.com', 'example.org'];
|
|
const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key'));
|
|
const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt'));
|
|
|
|
const pendingDomains = {};
|
|
const challengeResponses = {};
|
|
const certificateStore = {};
|
|
|
|
function log(m) {
|
|
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
|
}
|
|
|
|
/**
|
|
* On-demand certificate generation using http-01
|
|
*/
|
|
|
|
async function getCertOnDemand(client, servername, attempt = 0) {
|
|
/* Invalid domain */
|
|
if (!VALID_DOMAINS.includes(servername)) {
|
|
throw new Error(`Invalid domain: ${servername}`);
|
|
}
|
|
|
|
/* Certificate exists */
|
|
if (servername in certificateStore) {
|
|
return certificateStore[servername];
|
|
}
|
|
|
|
/* Waiting on certificate order to go through */
|
|
if (servername in pendingDomains) {
|
|
if (attempt >= 10) {
|
|
throw new Error(`Gave up waiting on certificate for ${servername}`);
|
|
}
|
|
|
|
await new Promise((resolve) => { setTimeout(resolve, 1000); });
|
|
return getCertOnDemand(client, servername, (attempt + 1));
|
|
}
|
|
|
|
/* Create CSR */
|
|
log(`Creating CSR for ${servername}`);
|
|
const [key, csr] = await acme.crypto.createCsr({
|
|
altNames: [servername],
|
|
});
|
|
|
|
/* Order certificate */
|
|
log(`Ordering certificate for ${servername}`);
|
|
const cert = await client.auto({
|
|
csr,
|
|
email: 'test@example.com',
|
|
termsOfServiceAgreed: true,
|
|
challengePriority: ['http-01'],
|
|
challengeCreateFn: (authz, challenge, keyAuthorization) => {
|
|
challengeResponses[challenge.token] = keyAuthorization;
|
|
},
|
|
challengeRemoveFn: (authz, challenge) => {
|
|
delete challengeResponses[challenge.token];
|
|
},
|
|
});
|
|
|
|
/* Done, store certificate */
|
|
log(`Certificate for ${servername} created successfully`);
|
|
certificateStore[servername] = [key, cert];
|
|
delete pendingDomains[servername];
|
|
return certificateStore[servername];
|
|
}
|
|
|
|
/**
|
|
* Main
|
|
*/
|
|
|
|
(async () => {
|
|
try {
|
|
/**
|
|
* Initialize ACME client
|
|
*/
|
|
|
|
log('Initializing ACME client');
|
|
const client = new acme.Client({
|
|
directoryUrl: acme.directory.letsencrypt.staging,
|
|
accountKey: await acme.crypto.createPrivateKey(),
|
|
});
|
|
|
|
/**
|
|
* HTTP server
|
|
*/
|
|
|
|
const httpServer = http.createServer((req, res) => {
|
|
if (req.url.match(/\/\.well-known\/acme-challenge\/.+/)) {
|
|
const token = req.url.split('/').pop();
|
|
log(`Received challenge request for token=${token}`);
|
|
|
|
/* ACME challenge response */
|
|
if (token in challengeResponses) {
|
|
log(`Serving challenge response HTTP 200 token=${token}`);
|
|
res.writeHead(200);
|
|
res.end(challengeResponses[token]);
|
|
return;
|
|
}
|
|
|
|
/* Challenge response not found */
|
|
log(`Oops, challenge response not found for token=${token}`);
|
|
res.writeHead(404);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
/* HTTP 302 redirect */
|
|
log(`HTTP 302 ${req.headers.host}${req.url}`);
|
|
res.writeHead(302, { Location: `https://${req.headers.host}${req.url}` });
|
|
res.end();
|
|
});
|
|
|
|
httpServer.listen(HTTP_SERVER_PORT, () => {
|
|
log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
|
|
});
|
|
|
|
/**
|
|
* HTTPS server
|
|
*/
|
|
|
|
const requestListener = (req, res) => {
|
|
log(`HTTP 200 ${req.headers.host}${req.url}`);
|
|
res.writeHead(200);
|
|
res.end('Hello world\n');
|
|
};
|
|
|
|
const httpsServer = https.createServer({
|
|
/* Fallback certificate */
|
|
key: FALLBACK_KEY,
|
|
cert: FALLBACK_CERT,
|
|
|
|
/* Serve certificate based on servername */
|
|
SNICallback: async (servername, cb) => {
|
|
try {
|
|
log(`Handling SNI request for ${servername}`);
|
|
const [key, cert] = await getCertOnDemand(client, servername);
|
|
|
|
log(`Found certificate for ${servername}, serving secure context`);
|
|
cb(null, tls.createSecureContext({ key, cert }));
|
|
}
|
|
catch (e) {
|
|
log(`[ERROR] ${e.message}`);
|
|
cb(e.message);
|
|
}
|
|
},
|
|
}, requestListener);
|
|
|
|
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
|
log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
|
|
});
|
|
}
|
|
catch (e) {
|
|
log(`[FATAL] ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
})();
|