UNPKG

traefikjam

Version:

A tiny (zero dependency) Node.js CLI utility to convert Traefik Let's Encrypt certificates from acme.json to PEM files

202 lines (177 loc) 4.9 kB
import fs from 'node:fs' import path from 'node:path' /** * Read certificates from acme.json with optional filter * * @example * // all certs in file * exportCerts('./acme.json') * * // only certs for github.com in file * exportCerts('./acme.json', 'github.com') * * // save certs to `certificates/` dir * exportCerts('./acme.json', null, 'certificates/') * * @param {string} file path to acme.json * @param {string} [search=null] for this domain * @param {string} [outputDir='certs/'] output directory for certificates * * @void */ export async function exportCerts( file, search = null, outputDir = 'certs/' ) { console.info('export traefik certs for domain:', search) let fileData = await fs.promises.readFile( file === '.' ? `${file}/acme.json` : file, 'utf8' ) let data = await JSON.parse(fileData) if (!data) { return } if (!data.Certificates) { data = Object.values(data).flatMap( v => v.Certificates !== null ? v.Certificates : [] ) } else { data = data.Certificates } if (search !== null) { data = data.filter( v => v.Domain?.Main?.includes(search) || v.domain?.main?.includes(search) ) } data.forEach(async c => { let { Certificate, Key, Domain, certificate, key, domain, } = c let dom = Domain || domain let main = dom.Main || dom.main let cert = Certificate || certificate let skey = Key || key let d = main.replace('*.', '_.') const [cf] = await save(`${d}.crt`, Buffer.from(cert, 'base64').toString(), outputDir) const [kf] = await save(`${d}.key`, Buffer.from(skey, 'base64').toString(), outputDir) const [tf] = await save(`${d}.json`, JSON.stringify(dom), outputDir) console.info(`saved ${main}`, [cf, kf, tf]) }) } /** * Watch certificates directory * * @example * // watch all domains in file * await watchCerts('./acme.json') * * @param {string} [inputDir='data/'] input directory for certificates * @param {string[]} targetDomains Array of domain strings to find * * @void */ export async function watchCerts( inputDir = './data/', targetDomains = process.argv.slice(3) || [], ) { const pp = path.parse(inputDir) const ac = new AbortController(); const { signal } = ac; try { const watcher = fs.promises.watch(inputDir, { signal, persistent: true }); const changes = {} console.group('watching') console.info('path', path.resolve(inputDir)) console.info('domains', targetDomains) console.groupEnd() for await (const event of watcher) { const dir = path.resolve(path.join(pp.dir, event.filename)) // console.info('event', event.eventType, dir) if (changes[event.filename]) { clearTimeout(changes[event.filename]) } // prevent editors triggering multiple exports changes[event.filename] = setTimeout(() => { cmd(dir, targetDomains) delete changes[event.filename] }, 1000) } } catch (err) { if (err.name === 'AbortError') return; throw err; } } /** * Save file * * @example * let [filepath, dir] = save('./foobar.txt', 'foo', 'data/') * // filepath === './data/foobar.txt' * // dir === './data' * * @param {string} file Name of file to save * @param {string} data Data to save to file * @param {string} [outputDir='certs/'] Directory to output files * * @returns {[string,string]} File path and output directory */ export async function save(file, data, outputDir = 'certs/') { const fp = path.join(outputDir, file) fs.mkdirSync(outputDir, { recursive: true }) await fs.createWriteStream(fp).write(data) return [fp, outputDir] } /** * Array contains any * * @example * let contains = containsAny(['a', 'b', 'c'], ['x', 'b']) * // contains === true * let contains = containsAny(['a', 'b', 'c'], ['x', 'y', 'z']) * // contains === false * * @param {string[]} hay Haystack * @param {string[]} needles Needles * * @returns {boolean} At least one of Needles exist in Haystack */ export function containsAny(hay, needles) { let found = false for (let needle of needles) { if (hay.includes(needle)) { found = true break } } return found } /** * Filter based on domains from Array | CLI Args * * @example * cmd('./acme.json', ['foo','bar']) * // e.g. given `traefik-jam ./acme.json foo bar` * // targetDomains === ['foo', 'bar'] * * @param {string} acmeFile Path to acme.json file * @param {string[]} targetDomains Array of domain strings to find * * @void */ export function cmd( acmeFile = process.argv[2] || './data/acme.json', targetDomains = process.argv.slice(3) || [], ) { if (targetDomains?.length > 0) { targetDomains.forEach(domain => exportCerts(acmeFile, domain)) } else { exportCerts(acmeFile) } } export default exportCerts