Browse Source
* Move cert handling to index.js * Add 'now certs' commands for certificate managementmaster
Olli Vanhoja
8 years ago
5 changed files with 384 additions and 46 deletions
@ -0,0 +1,235 @@ |
|||
#!/usr/bin/env node |
|||
import chalk from 'chalk'; |
|||
import table from 'text-table'; |
|||
import minimist from 'minimist'; |
|||
import * as cfg from '../lib/cfg'; |
|||
import { handleError, error } from '../lib/error'; |
|||
import NowCerts from '../lib/certs'; |
|||
import path from 'path'; |
|||
import fs from 'fs-promise'; |
|||
import ms from 'ms'; |
|||
|
|||
const argv = minimist(process.argv.slice(2), { |
|||
string: ['config', 'token', 'crt', 'key', 'ca'], |
|||
boolean: ['help', 'debug'], |
|||
alias: { |
|||
help: 'h', |
|||
config: 'c', |
|||
debug: 'd', |
|||
token: 't' |
|||
} |
|||
}); |
|||
const subcommand = argv._[0]; |
|||
|
|||
// options |
|||
const help = () => { |
|||
console.log(` |
|||
${chalk.bold('𝚫 now certs')} <ls | create | renew | replace | rm> <cn> |
|||
|
|||
${chalk.dim('Note:')} |
|||
|
|||
This command is intended for advanced use only, normally ${chalk.bold('now')} manages your certificates automatically. |
|||
|
|||
${chalk.dim('Options:')} |
|||
|
|||
-h, --help output usage information |
|||
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} config file |
|||
-d, --debug debug mode [off] |
|||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} login token |
|||
--crt ${chalk.bold.underline('FILE')} certificate file |
|||
--key ${chalk.bold.underline('FILE')} certificate key file |
|||
--ca ${chalk.bold.underline('FILE')} CA certificate chain file |
|||
|
|||
${chalk.dim('Examples:')} |
|||
|
|||
${chalk.gray('–')} Listing all your certificates: |
|||
|
|||
${chalk.cyan('$ now certs ls')} |
|||
|
|||
${chalk.gray('–')} Creating a new certificate: |
|||
|
|||
${chalk.cyan('$ now certs create domain.com')} |
|||
|
|||
${chalk.gray('–')} Renewing an existing certificate issued with ${chalk.bold('now')}: |
|||
|
|||
${chalk.cyan('$ now certs renew domain.com')} |
|||
|
|||
${chalk.gray('–')} Replacing an existing certificate with a user-supplied certificate: |
|||
|
|||
${chalk.cyan('$ now certs replace --crt domain.crt --key domain.key --ca ca_chain.crt domain.com')} |
|||
`); |
|||
}; |
|||
|
|||
// options |
|||
const debug = argv.debug; |
|||
const apiUrl = argv.url || 'https://api.zeit.co'; |
|||
if (argv.config) cfg.setConfigFile(argv.config); |
|||
|
|||
const exit = (code) => { |
|||
// we give stdout some time to flush out |
|||
// because there's a node bug where |
|||
// stdout writes are asynchronous |
|||
// https://github.com/nodejs/node/issues/6456 |
|||
setTimeout(() => process.exit(code || 0), 100); |
|||
}; |
|||
|
|||
if (argv.help || !subcommand) { |
|||
help(); |
|||
exit(0); |
|||
} else { |
|||
const config = cfg.read(); |
|||
|
|||
Promise.resolve(argv.token || config.token || login(apiUrl)) |
|||
.then(async (token) => { |
|||
try { |
|||
await run(token); |
|||
} catch (err) { |
|||
handleError(err); |
|||
exit(1); |
|||
} |
|||
}) |
|||
.catch((e) => { |
|||
error(`Authentication error – ${e.message}`); |
|||
exit(1); |
|||
}); |
|||
} |
|||
|
|||
async function run (token) { |
|||
const certs = new NowCerts(apiUrl, token, { debug }); |
|||
const args = argv._.slice(1); |
|||
const start = Date.now(); |
|||
|
|||
if ('ls' === subcommand || 'list' === subcommand) { |
|||
if (0 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs ls`')}`); |
|||
return exit(1); |
|||
} |
|||
const list = await certs.ls(); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`> ${list.length} certificate${list.length > 1 ? 's' : ''} found ${chalk.gray(`[${elapsed}]`)}`); |
|||
const cur = Date.now(); |
|||
list.sort((a, b) => { |
|||
return a.cn.localeCompare(b.cn); |
|||
}); |
|||
const out = table(list.map((cert) => { |
|||
const cn = chalk.bold(cert.cn); |
|||
const time = chalk.gray(ms(cur - new Date(cert.created)) + ' ago'); |
|||
return [ |
|||
'', |
|||
cert.uid ? cert.uid : 'unknown', |
|||
cn, |
|||
time |
|||
]; |
|||
}), { align: ['l', 'r', 'l'], hsep: ' '.repeat(2) }); |
|||
if (out) console.log('\n' + out + '\n'); |
|||
} else if ('create' === subcommand) { |
|||
if (1 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs create <cn>`')}`); |
|||
return exit(1); |
|||
} |
|||
const cn = args[0]; |
|||
const cert = await certs.create(cn); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cn)} ${chalk.gray(`(${cert.uid})`)} issued ${chalk.gray(`[${elapsed}]`)}`); |
|||
} else if ('renew' === subcommand) { |
|||
if (1 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs renew <id | cn>`')}`); |
|||
return exit(1); |
|||
} |
|||
|
|||
const cert = await getCertIdCn(certs, args[0]); |
|||
const yes = await readConfirmation(cert, 'The following certificate will be renewed\n'); |
|||
if (!yes) { |
|||
error('User abort'); |
|||
return exit(0); |
|||
} |
|||
|
|||
await certs.renew(cert.cn); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} renewed ${chalk.gray(`[${elapsed}]`)}`); |
|||
} else if ('replace' === subcommand) { |
|||
if (!argv.crt || !argv.key) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs replace --crt DOMAIN.CRT --key DOMAIN.KEY [--ca CA.CRT] <id | cn>`')}`); |
|||
return exit(1); |
|||
} |
|||
|
|||
const crt = readX509File(argv.crt); |
|||
const key = readX509File(argv.key); |
|||
const ca = argv.ca ? readX509File(argv.ca) : ''; |
|||
|
|||
const cert = await getCertIdCn(certs, args[0]); |
|||
const yes = await readConfirmation(cert, 'The following certificate will be replaced permanently\n'); |
|||
if (!yes) { |
|||
error('User abort'); |
|||
return exit(0); |
|||
} |
|||
|
|||
await certs.replace(cert.cn, crt, key, ca); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} replaced ${chalk.gray(`[${elapsed}]`)}`); |
|||
} else if ('rm' === subcommand || 'remove' === subcommand) { |
|||
if (1 !== args.length) { |
|||
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now certs rm <id | cn>`')}`); |
|||
return exit(1); |
|||
} |
|||
|
|||
const cert = await getCertIdCn(certs, args[0]); |
|||
const yes = await readConfirmation(cert, 'The following certificate will be removed permanently\n'); |
|||
if (!yes) { |
|||
error('User abort'); |
|||
return exit(0); |
|||
} |
|||
|
|||
await certs.delete(cert.cn); |
|||
const elapsed = ms(new Date() - start); |
|||
console.log(`${chalk.cyan('> Success!')} Certificate ${chalk.bold(cert.cn)} ${chalk.gray(`(${cert.uid})`)} removed ${chalk.gray(`[${elapsed}]`)}`); |
|||
} else { |
|||
error('Please specify a valid subcommand: ls | create | renew | replace | rm'); |
|||
help(); |
|||
exit(1); |
|||
} |
|||
return certs.close(); |
|||
} |
|||
|
|||
process.on('uncaughtException', (err) => { |
|||
handleError(err); |
|||
exit(1); |
|||
}); |
|||
|
|||
function readConfirmation (cert, msg) { |
|||
return new Promise((resolve, reject) => { |
|||
const time = chalk.gray(ms(new Date() - new Date(cert.created)) + ' ago'); |
|||
const tbl = table( |
|||
[[cert.uid, chalk.bold(cert.cn), time]], |
|||
{ align: ['l', 'r', 'l'], hsep: ' '.repeat(6) } |
|||
); |
|||
|
|||
process.stdout.write(`> ${msg}`); |
|||
process.stdout.write(' ' + tbl + '\n'); |
|||
|
|||
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`); |
|||
|
|||
process.stdin.on('data', (d) => { |
|||
process.stdin.pause(); |
|||
resolve('y' === d.toString().trim().toLowerCase()); |
|||
}).resume(); |
|||
}); |
|||
} |
|||
|
|||
function readX509File (file) { |
|||
return fs.readFileSync(path.resolve(file), 'utf8'); |
|||
} |
|||
|
|||
async function getCertIdCn (certs, idOrCn) { |
|||
const list = await certs.ls(); |
|||
const thecert = list.filter((cert) => { |
|||
return cert.uid === idOrCn || cert.cn === idOrCn; |
|||
})[0]; |
|||
|
|||
if (!thecert) { |
|||
error(`No certificate found by id or cn "${idOrCn}"`); |
|||
return exit(1); |
|||
} |
|||
|
|||
return thecert; |
|||
} |
@ -0,0 +1,82 @@ |
|||
import Now from '../lib'; |
|||
|
|||
export default class Certs extends Now { |
|||
|
|||
ls () { |
|||
return this.retry(async (bail, attempt) => { |
|||
if (this._debug) console.time(`> [debug] #${attempt} GET now/certs`); |
|||
const res = await this._fetch('/now/certs'); |
|||
if (this._debug) console.timeEnd(`> [debug] #${attempt} GET now/certs`); |
|||
const body = await res.json(); |
|||
return body.certs; |
|||
}); |
|||
} |
|||
|
|||
create (cn) { |
|||
return this.createCert(cn); |
|||
} |
|||
|
|||
renew (cn) { |
|||
return this.createCert(cn, { renew: true }); |
|||
} |
|||
|
|||
replace (cn, crt, key, ca) { |
|||
return this.retry(async (bail, attempt) => { |
|||
if (this._debug) console.time(`> [debug] #${attempt} PUT now/certs`); |
|||
const res = await this._fetch('/now/certs', { |
|||
method: 'PUT', |
|||
body: { |
|||
domains: [cn], |
|||
ca: ca, |
|||
cert: crt, |
|||
key: key |
|||
} |
|||
}); |
|||
if (this._debug) console.timeEnd(`> [debug] #${attempt} PUT now/certs`); |
|||
|
|||
if (403 === res.status) { |
|||
return bail(new Error('Unauthorized')); |
|||
} |
|||
|
|||
const body = await res.json(); |
|||
|
|||
if (res.status !== 200) { |
|||
if (404 === res.status || 400 === res.status) { |
|||
const err = new Error(body.error.message); |
|||
err.userError = true; |
|||
return bail(err); |
|||
} else { |
|||
throw new Error(body.error.message); |
|||
} |
|||
} |
|||
|
|||
return body; |
|||
}); |
|||
} |
|||
|
|||
delete (cn) { |
|||
return this.retry(async (bail, attempt) => { |
|||
if (this._debug) console.time(`> [debug] #${attempt} DELETE now/certs/${cn}`); |
|||
const res = await this._fetch(`/now/certs/${cn}`, { method: 'DELETE' }); |
|||
if (this._debug) console.timeEnd(`> [debug] #${attempt} DELETE now/certs/${cn}`); |
|||
|
|||
if (403 === res.status) { |
|||
return bail(new Error('Unauthorized')); |
|||
} |
|||
|
|||
const body = await res.json(); |
|||
|
|||
if (res.status !== 200) { |
|||
if (404 === res.status || 400 === res.status) { |
|||
const err = new Error(body.error.message); |
|||
err.userError = true; |
|||
return bail(err); |
|||
} else { |
|||
throw new Error(body.error.message); |
|||
} |
|||
} |
|||
|
|||
return body; |
|||
}); |
|||
} |
|||
} |
Loading…
Reference in new issue