Browse Source

now certs management

* Move cert handling to index.js
* Add 'now certs' commands for certificate management
master
Olli Vanhoja 8 years ago
parent
commit
4db4353d5d
  1. 4
      bin/now
  2. 235
      bin/now-certs
  3. 44
      lib/alias.js
  4. 82
      lib/certs.js
  5. 65
      lib/index.js

4
bin/now

@ -19,8 +19,8 @@ const exit = (code) => {
};
const defaultCommand = 'deploy';
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains', 'secret', 'secrets']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains'], ['secret', 'secrets']]);
const commands = new Set([defaultCommand, 'list', 'ls', 'rm', 'remove', 'alias', 'aliases', 'ln', 'domain', 'domains', 'cert', 'certs', 'secret', 'secrets']);
const aliases = new Map([['ls', 'list'], ['rm', 'remove'], ['ln', 'alias'], ['aliases', 'alias'], ['domain', 'domains'], ['cert', 'certs'], ['secret', 'secrets']]);
let cmd = argv._[0];
let args = [];

235
bin/now-certs

@ -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;
}

44
lib/alias.js

@ -324,48 +324,4 @@ export default class Alias extends Now {
}
});
}
createCert (domain) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /now/certs #${attempt}`);
const res = await this._fetch('/now/certs', {
method: 'POST',
body: {
domains: [domain]
}
});
if (304 === res.status) {
console.log('> Certificate already issued.');
return;
}
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /now/certs #${attempt}`);
if (body.error) {
const { code } = body.error;
if ('verification_failed' === code) {
const err = new Error('The certificate issuer failed to verify ownership of the domain. ' +
'This likely has to do with DNS propagation and caching issues. Please retry later!');
err.userError = true;
// retry
throw err;
} else if ('rate_limited' === code) {
const err = new Error(body.error.message);
err.userError = true;
// dont retry
return bail(err);
}
throw new Error(body.error.message);
}
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
}, { retries: 5, minTimeout: 30000, maxTimeout: 90000 });
}
}

82
lib/certs.js

@ -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;
});
}
}

65
lib/index.js

@ -440,6 +440,71 @@ export default class Now extends EventEmitter {
});
}
createCert (domain, { renew } = {}) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /now/certs #${attempt}`);
const res = await this._fetch('/now/certs', {
method: 'POST',
body: {
domains: [domain],
renew
}
});
if (304 === res.status) {
console.log('> Certificate already issued.');
return;
}
const body = await res.json();
if (this._debug) console.timeEnd(`> [debug] /now/certs #${attempt}`);
if (body.error) {
const { code } = body.error;
if ('verification_failed' === code) {
const err = new Error('The certificate issuer failed to verify ownership of the domain. ' +
'This likely has to do with DNS propagation and caching issues. Please retry later!');
err.userError = true;
// retry
throw err;
} else if ('rate_limited' === code) {
const err = new Error(body.error.message);
err.userError = true;
// dont retry
return bail(err);
}
throw new Error(body.error.message);
}
if (200 !== res.status && 304 !== res.status) {
throw new Error('Unhandled error');
}
return body;
}, { retries: 5, minTimeout: 30000, maxTimeout: 90000 });
}
deleteCert (domain) {
return this.retry(async (bail, attempt) => {
if (this._debug) console.time(`> [debug] /now/certs #${attempt}`);
const res = await this._fetch(`/now/certs/${domain}`, {
method: 'DELETE'
});
if (200 !== res.status) {
const err = new Error(body.error.message);
err.userError = false;
if (400 === res.status || 404 === res.status) {
return bail(err);
} else {
throw err;
}
}
});
}
async remove (deploymentId, { hard }) {
const data = { deploymentId, hard };

Loading…
Cancel
Save