certonly.js 10.8 KB
'use strict';

var mkdirp = require('@root/mkdirp');
var cli = require('./cli.js');

cli.parse({
    'directory-url': [
        false,
        ' ACME Directory Resource URL',
        'string',
        'https://acme-v02.api.letsencrypt.org/directory',
        'server,acme-url'
    ],
    email: [
        false,
        ' Email used for registration and recovery contact. (default: null)',
        'email'
    ],
    'agree-tos': [
        false,
        " Agree to the Greenlock and Let's Encrypt Subscriber Agreements",
        'boolean',
        false
    ],
    'community-member': [
        false,
        ' Submit stats to and get updates from Greenlock',
        'boolean',
        false
    ],
    domains: [
        false,
        ' Domain names to apply. For multiple domains you can enter a comma separated list of domains as a parameter. (default: [])',
        'string'
    ],
    'renew-offset': [
        false,
        ' Positive (time after issue) or negative (time before expiry) offset, such as 30d or -45d',
        'string',
        '45d'
    ],
    'renew-within': [
        false,
        ' (ignored) use renew-offset instead',
        'ignore',
        undefined
    ],
    'cert-path': [
        false,
        ' Path to where new cert.pem is saved',
        'string',
        ':configDir/live/:hostname/cert.pem'
    ],
    'fullchain-path': [
        false,
        ' Path to where new fullchain.pem (cert + chain) is saved',
        'string',
        ':configDir/live/:hostname/fullchain.pem'
    ],
    'bundle-path': [
        false,
        ' Path to where new bundle.pem (fullchain + privkey) is saved',
        'string',
        ':configDir/live/:hostname/bundle.pem'
    ],
    'chain-path': [
        false,
        ' Path to where new chain.pem is saved',
        'string',
        ':configDir/live/:hostname/chain.pem'
    ],
    'privkey-path': [
        false,
        ' Path to where privkey.pem is saved',
        'string',
        ':configDir/live/:hostname/privkey.pem'
    ],
    'config-dir': [
        false,
        ' Configuration directory.',
        'string',
        '~/letsencrypt/etc/'
    ],
    store: [
        false,
        ' The name of the storage module to use',
        'string',
        'greenlock-store-fs'
    ],
    'store-xxxx': [
        false,
        ' An option for the chosen storage module, such as --store-apikey or --store-bucket',
        'bag'
    ],
    'store-json': [
        false,
        ' A JSON string containing all option for the chosen store module (instead of --store-xxxx)',
        'json',
        '{}'
    ],
    challenge: [
        false,
        ' The name of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use',
        'string',
        '@greenlock/acme-http-01-fs'
    ],
    'challenge-xxxx': [
        false,
        ' An option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket',
        'bag'
    ],
    'challenge-json': [
        false,
        ' A JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)',
        'json',
        '{}'
    ],
    'skip-dry-run': [
        false,
        ' Use with caution (and test with the staging url first). Creates an Order on the ACME server without a self-test.',
        'boolean'
    ],
    'skip-challenge-tests': [
        false,
        ' Use with caution (and with the staging url first). Presents challenges to the ACME server without first testing locally.',
        'boolean'
    ],
    'http-01-port': [
        false,
        ' Required to be 80 for live servers. Do not use. For special test environments only.',
        'int'
    ],
    'dns-01': [false, ' Use DNS-01 challange type', 'boolean', false],
    standalone: [
        false,
        ' Obtain certs using a "standalone" webserver.',
        'boolean',
        false
    ],
    manual: [
        false,
        ' Print the token and key to the screen and wait for you to hit enter, giving you time to copy it somewhere before continuing (uses acme-http-01-cli or acme-dns-01-cli)',
        'boolean',
        false
    ],
    debug: [false, ' show traces and logs', 'boolean', false],
    root: [
        false,
        ' public_html / webroot path (may use the :hostname template such as /srv/www/:hostname)',
        'string',
        undefined,
        'webroot-path'
    ],

    //
    // backwards compat
    //
    duplicate: [
        false,
        ' Allow getting a certificate that duplicates an existing one/is an early renewal',
        'boolean',
        false
    ],
    'rsa-key-size': [
        false,
        ' (ignored) use server-key-type or account-key-type instead',
        'ignore',
        2048
    ],
    'server-key-path': [
        false,
        ' Path to privkey.pem to use for certificate (default: generate new)',
        'string',
        undefined,
        'domain-key-path'
    ],
    'server-key-type': [
        false,
        " One of 'RSA' (2048), 'RSA-3084', 'RSA-4096', 'ECDSA' (P-256), or 'P-384'. For best compatibility, security, and efficiency use the default (More bits != More security)",
        'string',
        'RSA'
    ],
    'account-key-path': [
        false,
        ' Path to privkey.pem to use for account (default: generate new)',
        'string'
    ],
    'account-key-type': [
        false,
        " One of 'ECDSA' (P-256), 'P-384', 'RSA', 'RSA-3084', or 'RSA-4096'. Stick with 'ECDSA' (P-256) unless you need 'RSA' (2048) for legacy compatibility. (More bits != More security)",
        'string',
        'P-256'
    ],
    webroot: [false, ' (ignored) for certbot compatibility', 'ignore', false],
    //, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-alpn-01)", 'string', 'http-01']
    'work-dir': [
        false,
        ' for certbot compatibility (ignored)',
        'string',
        '~/letsencrypt/var/lib/'
    ],
    'logs-dir': [
        false,
        ' for certbot compatibility (ignored)',
        'string',
        '~/letsencrypt/var/log/'
    ],
    'acme-version': [
        false,
        ' (ignored) ACME is now RFC 8555 and prior drafts are no longer supported',
        'ignore',
        'rfc8555'
    ]
});

// ignore certonly and extraneous arguments
cli.main(function(_, options) {
    console.info('');

    [
        'configDir',
        'privkeyPath',
        'certPath',
        'chainPath',
        'fullchainPath',
        'bundlePath'
    ].forEach(function(k) {
        if (options[k]) {
            options.storeOpts[k] = options[k];
        }
        delete options[k];
    });

    if (options.workDir) {
        options.challengeOpts.workDir = options.workDir;
        delete options.workDir;
    }

    if (options.debug) {
        console.debug(options);
    }

    var args = {};
    var homedir = require('os').homedir();

    Object.keys(options).forEach(function(key) {
        var val = options[key];

        if ('string' === typeof val) {
            val = val.replace(/^~/, homedir);
        }

        key = key.replace(/\-([a-z0-9A-Z])/g, function(c) {
            return c[1].toUpperCase();
        });
        args[key] = val;
    });

    Object.keys(args).forEach(function(key) {
        var val = args[key];

        if ('string' === typeof val) {
            val = val.replace(/(\:configDir)|(\:config)/, args.configDir);
        }

        args[key] = val;
    });

    if (args.domains) {
        args.domains = args.domains.split(',');
    }

    if (
        !(Array.isArray(args.domains) && args.domains.length) ||
        !args.email ||
        !args.agreeTos ||
        (!args.server && !args.directoryUrl)
    ) {
        console.error('\nUsage:\n\ngreenlock certonly --standalone \\');
        console.error(
            '\t--agree-tos --email user@example.com --domains example.com \\'
        );
        console.error('\t--config-dir ~/acme/etc \\');
        console.error('\nSee greenlock --help for more details\n');
        return;
    }

    if (args.http01Port) {
        // [@agnat]: Coerce to string. cli returns a number although we request a string.
        args.http01Port = '' + args.http01Port;
        args.http01Port = args.http01Port.split(',').map(function(port) {
            return parseInt(port, 10);
        });
    }

    function run() {
        var challenges = {};
        if (/http.?01/i.test(args.challenge)) {
            challenges['http-01'] = args.challengeOpts;
        }
        if (/dns.?01/i.test(args.challenge)) {
            challenges['dns-01'] = args.challengeOpts;
        }
        if (/alpn.?01/i.test(args.challenge)) {
            challenges['tls-alpn-01'] = args.challengeOpts;
        }
        if (!Object.keys(challenges).length) {
            throw new Error(
                "Could not determine the challenge type for '" +
                    args.challengeOpts.module +
                    "'. Expected a name like @you/acme-xxxx-01-foo. Please name the module with http-01, dns-01, or tls-alpn-01."
            );
        }
        args.challengeOpts.module = args.challenge;
        args.storeOpts.module = args.store;

        console.log('\ngot to the run step');
        require(args.challenge);
        require(args.store);

        var greenlock = require('../').create({
            maintainerEmail: args.maintainerEmail || 'coolaj86@gmail.com',
            manager: './manager.js',
            configFile: '~/.config/greenlock/certs.json',
            challenges: challenges,
            store: args.storeOpts,
            renewOffset: args.renewOffset || '30d',
            renewStagger: '1d'
        });

        // for long-running processes
        if (args.renewEvery) {
            setInterval(function() {
                greenlock.renew({
                    period: args.renewEvery
                });
            }, args.renewEvery);
        }

        // TODO should greenlock.add simply always include greenlock.renew?
        // the concern is conflating error events
        return greenlock
            .add({
                subject: args.subject,
                altnames: args.altnames,
                subscriberEmail: args.subscriberEmail || args.email
            })
            .then(function(changes) {
                console.info(changes);
                // renew should always
                return greenlock
                    .renew({
                        subject: args.subject,
                        force: false
                    })
                    .then(function() {});
            });
    }

    if ('greenlock-store-fs' !== args.store) {
        run();
        return;
    }

    // TODO remove mkdirp and let greenlock-store-fs do this?
    mkdirp(args.storeOpts.configDir, function(err) {
        if (!err) {
            run();
        }

        console.error(
            "Could not create --config-dir '" + args.configDir + "':",
            err.code
        );
        console.error("Try setting --config-dir '/tmp'");
        return;
    });
}, process.argv.slice(3));