#!/usr/bin/env node // Native const path = require("path"); // Packages const chalk = require("chalk"); const table = require("text-table"); const minimist = require("minimist"); const fs = require("fs-promise"); const ms = require("ms"); // Ours const strlen = require("../lib/strlen"); const cfg = require("../lib/cfg"); const { handleError, error } = require("../lib/error"); const NowCerts = require("../lib/certs"); const login = require("../lib/login"); const exit = require("../lib/utils/exit"); const logo = require("../lib/utils/output/logo"); 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(`${logo} now certs`)} ${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); } 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); }); } function formatExpirationDate(date) { const diff = date - Date.now(); return diff < 0 ? chalk.gray(ms(-diff) + " ago") : chalk.gray("in " + ms(diff)); } async function run(token) { const certs = new NowCerts(apiUrl, token, { debug }); const args = argv._.slice(1); const start = Date.now(); if (subcommand === "ls" || subcommand === "list") { if (args.length !== 0) { 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}]`)}` ); if (list.length > 0) { const cur = Date.now(); list.sort((a, b) => { return a.cn.localeCompare(b.cn); }); const header = [ ["", "id", "cn", "created", "expiration", "auto-renew"].map(s => chalk.dim(s)) ]; const out = table( header.concat( list.map(cert => { const cn = chalk.bold(cert.cn); const time = chalk.gray(ms(cur - new Date(cert.created)) + " ago"); const expiration = formatExpirationDate(new Date(cert.expiration)); return [ "", cert.uid ? cert.uid : "unknown", cn, time, expiration, cert.autoRenew ? "yes" : "no" ]; }) ), { align: ["l", "r", "l", "l", "l"], hsep: " ".repeat(2), stringLength: strlen } ); if (out) { console.log("\n" + out + "\n"); } } } else if (subcommand === "create") { if (args.length !== 1) { error( `Invalid number of arguments. Usage: ${chalk.cyan("`now certs create `")}` ); return exit(1); } const cn = args[0]; let cert; if (argv.crt || argv.key || argv.ca) { // Issue a custom certificate if (!argv.crt || !argv.key) { error( `Missing required arguments for a custom certificate entry. Usage: ${chalk.cyan("`now certs create --crt DOMAIN.CRT --key DOMAIN.KEY [--ca CA.CRT] `")}` ); return exit(1); } const crt = readX509File(argv.crt); const key = readX509File(argv.key); const ca = argv.ca ? readX509File(argv.ca) : ""; cert = await certs.put(cn, crt, key, ca); } else { // Issue a standard certificate cert = await certs.create(cn); } if (!cert) { // Cert is undefined and "Cert is already issued" has been printed to stdout return exit(1); } const elapsed = ms(new Date() - start); console.log( `${chalk.cyan("> Success!")} Certificate entry ${chalk.bold(cn)} ${chalk.gray(`(${cert.uid})`)} created ${chalk.gray(`[${elapsed}]`)}` ); } else if (subcommand === "renew") { if (args.length !== 1) { error( `Invalid number of arguments. Usage: ${chalk.cyan("`now certs renew `")}` ); return exit(1); } const cert = await getCertIdCn(certs, args[0]); if (!cert) { return exit(1); } 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 (subcommand === "replace") { 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] `")}` ); 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]); if (!cert) { return exit(1); } const yes = await readConfirmation( cert, "The following certificate will be replaced permanently\n" ); if (!yes) { error("User abort"); return exit(0); } await certs.put(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 (subcommand === "rm" || subcommand === "remove") { if (args.length !== 1) { error( `Invalid number of arguments. Usage: ${chalk.cyan("`now certs rm `")}` ); return exit(1); } const cert = await getCertIdCn(certs, args[0]); if (!cert) { return exit(1); } 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 => { 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("[y/N] ")}` ); process.stdin .on("data", d => { process.stdin.pause(); resolve(d.toString().trim().toLowerCase() === "y"); }) .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 null; } return thecert; }