Files
proxy/nodejs/utils/letsencrypt.js
T

157 lines
4.2 KiB
JavaScript

'use strict';
const fs = require('fs')
const axios = require('axios');
const AcmeClient = require('acme-client');
const sleep = require('./sleep');
// https://dns.google/resolve?name=${name}&type=TXT
class LetsEncrypt{
static AcmeClient = AcmeClient;
constructor(options){
this.loadAccountKey(options.accountKeyPath || './le_key.cert', (key)=>{
this.client = new AcmeClient.Client({
directoryUrl: options.directoryUrl || AcmeClient.directory.letsencrypt.production,
accountKey: key,
});
});
}
loadAccountKey(accountKeyPath, cb){
try{
// Load the account key if it exists
cb(fs.readFileSync(accountKeyPath, 'utf8'));
}catch(error){
if(error.code === 'ENOENT'){
// Generate a new account key if it doesn't exist
AcmeClient.crypto.createPrivateKey().then(function(accountKey){
fs.writeFileSync(accountKeyPath, accountKey.toString(), 'utf8');
cb(accountKey.toString());
});
}else{
throw error;
}
}
}
async dnsWildcard(domain, options){
/*
https://github.com/publishlab/node-acme-client/tree/master/examples/dns-01
*/
try{
domain = domain.replace(/^\*\./, '');
const [key, csr] = await AcmeClient.crypto.createCsr({
altNames: [domain, `*.${domain}`],
});
let dnsToAdd = 0;
let dnsFound = 0;
const cert = await this.client.auto({
csr,
email: 'wmantly@gmail.com',
termsOfServiceAgreed: true,
challengePriority: ['dns-01'],
skipChallengeVerification: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
try{
dnsToAdd++;
let resCheck = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`);
if(resCheck.data.Answer && resCheck.data.Answer.some(record => record.data === keyAuthorization)){
dnsFound++;
if(dnsFound === dnsToAdd){
options.onDnsCheckFound(authz, dnsFound);
}
return;
}
await options.challengeCreateFn(authz, challenge, keyAuthorization);
let checkCount = 0;
while(true){
options.onDnsCheck(authz, checkCount);
let res = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`);
if(res.data.Answer && res.data.Answer.some(record => record.data === keyAuthorization)){
dnsFound++;
if(dnsFound === dnsToAdd){
options.onDnsCheckFound(authz, dnsFound);
}
await sleep(10000);
break;
}
if(checkCount++ > 60) throw new Error('challengeCreateFn validation timed out');
await sleep(1500);
}
}catch(error){
console.log('dns check failed error:', error)
options.onDnsCheckFail(authz, error)
}
},
challengeRemoveFn: options.challengeRemoveFn,
});
return {
key,
csr,
cert,
};
}catch(error){
console.log('Error in LetsEncrypt.dnsChallenge', error)
}
}
}
module.exports = LetsEncrypt;
if(require.main === module){(async function(){try{
const tldExtract = require('tld-extract').parse_host;
const PorkBun = require('./porkbun');
const conf = require('@simpleworkjs/conf');
let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey);
let letsEncrypt = new LetsEncrypt({
directoryUrl: AcmeClient.directory.letsencrypt.staging,
});
let cert = await letsEncrypt.dnsWildcard('dev.test.holycore.quest', {
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
let parts = tldExtract(authz.identifier.value);
let res = await porkBun.createRecordForce(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`});
},
challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{
let parts = tldExtract(authz.identifier.value);
await porkBun.deleteRecords(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`});
},
});
console.log('IIFE csr', cert.csr.toString())
// console.log(cert.cert.split('\n\n')[0])
// console.log('IIFE cert info', +AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, 'IIFE cert:\n', String(cert.cert).split('\n\n') , /*'IIFE privKey\n', cert.key.toString()*/);
}catch(error){
console.log('IIFE Error:', error)
}})()}