mirror of
https://github.com/certd/certd.git
synced 2026-04-14 12:30:54 +08:00
Small crypto docs fix 2 Small crypto docs fix Bump v5.3.1 Discourage use of cert subject common name, examples and docs Style refactor docs and examples Bump dependencies
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);
|
|
}
|
|
})();
|