Last active
July 25, 2024 14:22
-
-
Save wmantly/95db6d13b1ddf4a3eeaf70d0a9a4a2f7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const fs = require('fs') | |
const axios = require('axios'); | |
const AcmeClient = require('acme-client'); | |
const conf = require('../conf/conf'); | |
const sleep = require('./sleep'); | |
// https://dns.google/resolve?name=${name}&type=TXT | |
class LetsEncrypt{ | |
constructor(options){ | |
this.loadAccountKey(options.accountKeyPath || './le_key', (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{ | |
const [key, csr] = await AcmeClient.crypto.createCsr({ | |
altNames: [domain, `*.${domain}`], | |
}); | |
const cert = await this.client.auto({ | |
csr, | |
email: '[email protected]', | |
termsOfServiceAgreed: true, | |
challengePriority: ['dns-01'], | |
challengeCreateFn: async (authz, challenge, keyAuthorization) => { | |
console.log(`start TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) | |
let resCheck = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); | |
if(resCheck.data.Answer.some(record => record.data === keyAuthorization)) return; | |
await options.challengeCreateFn(authz, challenge, keyAuthorization); | |
let checkCount = 0; | |
while(true){ | |
await sleep(1500); | |
let res = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); | |
if(res.data.Answer.some(record => record.data === keyAuthorization)){ | |
console.log(`found record for key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) | |
break; | |
} | |
if(checkCount++ > 60) throw new Error('challengeCreateFn validation timed out'); | |
} | |
}, | |
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'); | |
let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); | |
let letsEncrypt = new LetsEncrypt({ | |
directoryUrl: AcmeClient.directory.letsencrypt.staging, | |
}); | |
console.log('wtf') | |
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 cert:\n', cert.cert); | |
}catch(error){ | |
console.log('IIFE Error:', error) | |
}})()} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const axios = require('axios'); | |
const conf = require('../conf/conf'); | |
class PorkBun{ | |
baseUrl = 'https://api.porkbun.com/api/json/v3'; | |
constructor(apiKey, secretApiKey){ | |
this.apiKey = apiKey; | |
this.secretApiKey = secretApiKey; | |
} | |
async post(url, data){ | |
let res; | |
try{ | |
data = { | |
...(data || {}), | |
secretapikey: this.secretApiKey, | |
apikey: this.apiKey, | |
}; | |
res = await axios.post(`${this.baseUrl}${url}`, data); | |
return res; | |
}catch(error){ | |
throw new Error(`PorkPun API ${error.response.status}: ${error.response.data.message}`) | |
} | |
} | |
__typeCheck(type){ | |
if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') | |
} | |
__parseName(domain, name){ | |
if(name && !name.endsWith('.'+domain)){ | |
return `${name}.${domain}` | |
} | |
return name; | |
} | |
async getRecords(domain, options){ | |
let res = await this.post(`/dns/retrieve/${domain}`); | |
if(!options) return res.data.records; | |
if(options.type) this.__typeCheck(options.type); | |
if(options.name) options.name = this.__parseName(domain, options.name); | |
let records = []; | |
for(let record of res.data.records){ | |
let matchCount = 0 | |
for(let option in options){ | |
if(record[option] === options[option] && ++matchCount === Object.keys(options).length){ | |
records.push(record) | |
} | |
// console.log('option', option, options[option], record[option], matchCount) | |
} | |
} | |
return records; | |
} | |
async deleteRecordById(domain, id){ | |
let res = this.post(`/dns/delete/${domain}/${id}`); | |
return res.data; | |
} | |
async deleteRecords(domain, options){ | |
let records = await this.getRecords(domain, options); | |
console.log('PorkBun.deleteRecords', records) | |
for(let record of records){ | |
await this.deleteRecordById(domain, record.id) | |
} | |
} | |
async createRecord(domain, options){ | |
this.__typeCheck(options.type); | |
if(!options.content) throw new Error('PorkBun API: `content` key is required for this action') | |
// if(options.name) options.name = this.__parseName(domain, options.name); | |
console.log('PorkBun.createRecord to send:', domain, options) | |
let res = this.post(`/dns/create/${domain}`, options); | |
return res.data; | |
} | |
async createRecordForce(domain, options){ | |
let {content, ...removed} = options; | |
// console.log('new options', removed) | |
let records = await this.getRecords(domain, removed); | |
console.log('createRecordForce', records) | |
if(records.length){ | |
// console.log('calling delete on', records[0].id) | |
// process.exit(0) | |
await this.deleteRecordById(domain, records[0].id) | |
} | |
return await this.createRecord(domain, options) | |
} | |
} | |
module.exports = PorkBun; | |
if(require.main === module){(async function(){try{ | |
let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); | |
// console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) | |
// console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) | |
console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) | |
}catch(error){ | |
console.log('IIFE Error:', error) | |
}})()} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment