From 25859971389c593c4c13b77e2dc47ce3c29cd099 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 1 Nov 2019 14:04:03 -0600 Subject: [PATCH] mirror greenlock to unscoped npm package --- MIGRATION_GUIDE_V2_V3.md | 517 ---------------- README.md | 4 +- accounts.js | 219 ------- bin/certonly.js | 378 ------------ bin/cli.js | 234 ------- bin/greenlock.js | 13 - certificates.js | 318 ---------- challenges-underlay.js | 96 --- errors.js | 58 -- greenlock.js | 682 +-------------------- logo/beaker-browser-301x112.png | Bin 3481 -> 0 bytes logo/from-not-secure-to-secure-url-bar.png | Bin 34652 -> 0 bytes logo/greenlock-1063x250.png | Bin 18245 -> 0 bytes logo/greenlock-850x200.png | Bin 6425 -> 0 bytes logo/ibm-301x112.png | Bin 1699 -> 0 bytes logo/telebit-301x112.png | Bin 2026 -> 0 bytes manager-underlay.js | 258 -------- order.js | 95 --- plugins.js | 331 ---------- publish.sh | 16 + tests/index.js | 54 -- user-events.js | 7 - utils.js | 281 --------- 23 files changed, 19 insertions(+), 3542 deletions(-) delete mode 100644 MIGRATION_GUIDE_V2_V3.md delete mode 100644 accounts.js delete mode 100755 bin/certonly.js delete mode 100644 bin/cli.js delete mode 100755 bin/greenlock.js delete mode 100644 certificates.js delete mode 100644 challenges-underlay.js delete mode 100644 errors.js delete mode 100755 logo/beaker-browser-301x112.png delete mode 100644 logo/from-not-secure-to-secure-url-bar.png delete mode 100755 logo/greenlock-1063x250.png delete mode 100644 logo/greenlock-850x200.png delete mode 100755 logo/ibm-301x112.png delete mode 100755 logo/telebit-301x112.png delete mode 100644 manager-underlay.js delete mode 100644 order.js delete mode 100644 plugins.js create mode 100644 publish.sh delete mode 100644 tests/index.js delete mode 100644 user-events.js delete mode 100644 utils.js diff --git a/MIGRATION_GUIDE_V2_V3.md b/MIGRATION_GUIDE_V2_V3.md deleted file mode 100644 index f969018..0000000 --- a/MIGRATION_GUIDE_V2_V3.md +++ /dev/null @@ -1,517 +0,0 @@ -# Migrating from Greenlock v2 to v3 - -**Greenlock Express** uses Greenlock directly, the same as before. - -All options described for `Greenlock.create({...})` also apply to the Greenlock Express `init()` callback. - -# Overview of Major Differences - -- Reduced API -- No code in the config - - (config is completely serializable) -- Manager callbacks replace `approveDomains` -- Greenlock Express does more, with less config - - cluster is supported out-of-the-box - - high-performance - - scalable -- ACME challenges are simplified - - init - - zones (dns-01) - - set - - get - - remove -- Store callbacks are simplified - - accounts - - checkKeypairs - - certificates - - checkKeypairs - - check - - set - -# Greenlock JavaScript API greatly reduced - -Whereas before there were many different methods with nuance differences, -now there's just `create`, `get`, `renew`, and sometimes `add` (). - -- Greenlock.create({ maintainerEmail, packageAgent, notify }) -- Greenlock.get({ servername, wildname, duplicate, force }) - - (just a convenience wrapper around renew) -- Greenlock.renew({ subject, altnames, issuedBefore, expiresAfter }) - - (retrieves, issues, renews, all-in-one) -- _optional_ Greenlock.add({ subject, altnames, subscriberEmail }) - - (partially replaces `approveDomains`) - -Also, some disambiguation on terms: - -- `domains` was often ambiguous and confusing, it has been replaced by: - - `subject` refers to the subject of a certificate - the primary domain - - `altnames` refers to the domains in the SAN (Subject Alternative Names) section of the certificate - - `servername` refers to the TLS (SSL) SNI (Server Name Indication) request for a cetificate - - `wildname` refers to the wildcard version of the servername (ex: `www.example.com => *.example.com`) - -When you create an instance of Greenlock, you only supply package and maintainer info. - -All other configuration is A) optional and B) handled by the _Manager_. - -```js -'use strict'; - -var pkg = require('./package.json'); - -var Greenlock = require('greenlock'); -var greenlock = Greenlock.create({ - // used for the ACME client User-Agent string as per RFC 8555 and RFC 7231 - packageAgent: pkg.name + '/' + pkg.version, - - // used as the contact for critical bug and security notices - // should be the same as pkg.author.email - maintainerEmail: 'jon@example.com', - - // used for logging background events and errors - notify: function(ev, args) { - if ('error' === ev || 'warning' === ev) { - console.error(ev, args); - return; - } - console.info(ev, args); - } -}); -``` - -By default **no certificates will be issued**. See the _manager_ section. - -When you want to get a single certificate, you use `get`, which will: - -- will return null if neither the `servername` or its `wildname` (wildcard) variant can be found -- retrieve a non-expired certificate, if possible -- will renew the certificate in the background, if stale -- will wait for the certificate to be issued if new - -```js -greenlock - .get({ servername: 'www.example.com' }) - .then(function(result) { - if (!result) { - // certificate is not on the approved list - return null; - } - - var fullchain = result.pems.cert + '\n' + result.pems.chain + '\n'; - var privkey = result.pems.privkey; - - return { - fullchain: fullchain, - privkey: privkey - }; - }) - .catch(function(e) { - // something went wrong in the renew process - console.error(e); - }); -``` - -By default **no certificates will be issued**. See the _manager_ section. - -When you want to renew certificates, _en masse_, you use `renew`, which will: - -- check all certificates matching the given criteria -- only renew stale certificates by default -- return error objects (will NOT throw exception for failed renewals) - -```js -greenlock - .renew({}) - .then(function(results) { - if (!result.length) { - // no certificates found - return null; - } - - // [{ site, error }] - return results; - }) - .catch(function(e) { - // an unexpected error, not related to renewal - console.error(e); - }); -``` - -Options: - -| Option | Description | -| ------------- | -------------------------------------------------------------------------- | -| `altnames` | only check and renew certs matching these altnames (including wildcards) | -| `renewBefore` | only check and renew certs marked for renewal before the given date, in ms | -| `duplicate` | renew certificates regardless of timing | -| `force` | allow silly things, like tiny `renewOffset`s | - -By default **no certificates will be issued**. See the _manager_ section. - -# Greenlock Express Example - -The options that must be returned from `init()` are the same that are used in `Greenlock.create()`, -with a few extra that are specific to Greenlock Express: - -```js -require('@root/greenlock-express') - .init(function() { - // This object will be passed to Greenlock.create() - - var options = { - // some options, like cluster, are special to Greenlock Express - - cluster: false, - - // The rest are the same as for Greenlock - - packageAgent: pkg.name + '/' + pkg.version, - maintainerEmail: 'jon@example.com', - notify: function(ev, args) { - console.info(ev, args); - } - }; - - return options; - }) - .serve(function(glx) { - // will start servers on port 80 and 443 - - glx.serveApp(function(req, res) { - res.end('Hello, Encrypted World!'); - }); - - // you can get access to the raw server (i.e. for websockets) - - glx.httpsServer(); // returns raw server object - }); -``` - -# _Manager_ replaces `approveDomains` - -`approveDomains` was always a little confusing. Most people didn't need it. - -Instead, now there is a simple config file that will work for most people, -as well as a set of callbacks for easy configurability. - -### Default Manager - -The default manager is `greenlock-manager-fs` and the default `configFile` is `~/.config/greenlock/manager.json`. - -The config file should look something like this: - -`~/.config/greenlock/manager.json`: - -```json -{ - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - } -} -``` - -You can specify a `acme-dns-01-*` or `acme-http-01-*` challenge plugin globally, or per-site. - -```json -{ - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"], - "challenges": { - "dns-01": { - "module": "acme-dns-01-digitalocean", - "token": "apikey-xxxxx" - } - } - } - } -} -``` - -The same is true with `greenlock-store-*` plugins: - -```json -{ - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - }, - "store": { - "module": "greenlock-store-fs", - "basePath": "~/.config/greenlock" - } -} -``` - -### Customer Manager, the lazy way - -At the very least you have to implement `find({ servername })`. - -Since this is a very common use case, it's supported out of the box as part of the default manager plugin: - -```js -var greenlock = Greenlock.create({ - packageAgent: pkg.name + '/' + pkg.version, - maintainerEmail: 'jon@example.com', - notify: notify, - find: find -}); - -// In the simplest case you can ignore all incoming options -// and return a single site config in the same format as the config file - -function find(options) { - var servername = options.servername; // www.example.com - var wildname = options.wildname; // *.example.com - return Promise.resolve([ - { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } - ]); -} - -function notify(ev, args) { - if ('error' === ev || 'warning' === ev) { - console.error(ev, args); - return; - } - console.info(ev, args); -} -``` - -If you want to use wildcards or local domains, you must specify the `dns-01` challenge plugin to use: - -```js -function find(options) { - var subject = options.subject; - // may include wildcard - var altnames = options.altnames; - var wildname = options.wildname; // *.example.com - return Promise.resolve([ - { - subject: 'example.com', - altnames: ['example.com', 'www.example.com'], - challenges: { - 'dns-01': { module: 'acme-dns-01-namedotcom', apikey: 'xxxx' } - } - } - ]); -} -``` - -### Customer Manager, complete - -To use a fully custom manager, you give the npm package name, or absolute path to the file to load - -```js -Greenlock.create({ - // Greenlock Options - maintainerEmail: 'jon@example.com', - packageAgent: 'my-package/v2.1.1', - notify: notify, - - // file path or npm package name - manager: '/path/to/manager.js', - // options that get passed to the manager - myFooOption: 'whatever' -}); -``` - -The manager itself is, again relatively simple: - -- find(options) -- set(siteConfig) -- remove(options) -- defaults(globalOptions) (as setter) - - defaults() => globalOptions (as getter) - -`/path/to/manager.js`: - -```js -'use strict'; - -module.exports.create = function() { - var manager = {}; - - manager.find = async function({ subject, altnames, renewBefore }) { - if (subject) { - return getSiteConfigBySubject(subject); - } - - if (altnames) { - // may include wildcards - return getSiteConfigByAnyAltname(altnames); - } - - if (renewBefore) { - return getSiteConfigsWhereRenewAtIsLessThan(renewBefore); - } - - return []; - }; - - manage.set = function(opts) { - // this is called by greenlock.add({ subject, altnames }) - // it's also called by greenlock._update({ subject, renewAt }) - - return mergSiteConfig(subject, opts); - }; - - manage.remove = function({ subject, altname }) { - if (subject) { - return removeSiteConfig(subject); - } - - return removeFromSiteConfigAndResetRenewAtToZero(altname); - }; - - // set the global config - manage.defaults = function(options) { - if (!options) { - return getGlobalConfig(); - } - return mergeGlobalConfig(options); - }; -}; -``` - -# ACME Challenge Plugins - -The ACME challenge plugins are just a few simple callbacks: - -- `init` -- `zones` (dns-01 only) -- `set` -- `get` -- `remove` - -They are described here: - -- [dns-01 documentation](https://git.rootprojects.org/root/acme-dns-01-test.js) -- [http-01 documentation](https://git.rootprojects.org/root/acme-http-01-test.js) - -# Key and Cert Store Plugins - -Again, these are just a few simple callbacks: - -- `certificates.checkKeypair` -- `certificates.check` -- `certificates.setKeypair` -- `certificates.set` -- `accounts.checkKeypair` -- `accounts.check` (optional) -- `accounts.setKeypair` -- `accounts.set` (optional) - -The name `check` is used instead of `get` because they only need to return something if it exists. They do not need to fail, nor do they need to generate anything. - -They are described here: - -- [greenlock store documentation](https://git.rootprojects.org/root/greenlock-store-test.js) - -If you are just implenting in-house and are not going to publish a module, you can also do some hack things like this: - -### Custome Store, The hacky / lazy way - -`/path/to/project/my-hacky-store.js`: - -```js -'use strict'; - -module.exports.create = function(options) { - // ex: /path/to/account.ecdsa.jwk.json - var accountJwk = require(options.accountJwkPath); - // ex: /path/to/privkey.rsa.pem - var serverPem = fs.readFileSync(options.serverPemPath, 'ascii'); - var accounts = {}; - var certificates = {}; - var store = { accounts, certificates }; - - // bare essential account callbacks - accounts.checkKeypair = function() { - // ignore all options and just return a single, global keypair - - return Promise.resolve({ - privateKeyJwk: accountJwk - }); - }; - accounts.setKeypair = function() { - // this will never get called if checkKeypair always returns - - return Promise.resolve({}); - }; - - // bare essential cert and key callbacks - certificates.checkKeypair = function() { - // ignore all options and just return a global server keypair - - return { - privateKeyPem: serverPem - }; - }; - certificates.setKeypair = function() { - // never gets called if checkKeypair always returns an existing key - - return Promise.resolve(null); - }; - - certificates.check = function(args) { - var subject = args.subject; - // make a database call or whatever to get a certificate - return goGetCertBySubject(subject).then(function() { - return { - pems: { - chain: '', - cert: '' - } - }; - }); - }; - certificates.set = function(args) { - var subject = args.subject; - var cert = args.pems.cert; - var chain = args.pems.chain; - - // make a database call or whatever to get a certificate - return goSaveCert({ - subject, - cert, - chain - }); - }; -}; -``` - -### Using the hacky / lazy store plugin - -That sort of implementation won't pass the test suite, but it'll work just fine a use case where you only have one subscriber email (most of the time), -you only have one server key (not recommended, but works), and you only really want to worry about storing cetificates. - -Then you could assign it as the default for all of your sites: - -```json -{ - "subscriberEmail": "jon@example.com", - "agreeToTerms": true, - "sites": { - "example.com": { - "subject": "example.com", - "altnames": ["example.com", "www.example.com"] - } - }, - "store": { - "module": "/path/to/project/my-hacky-store.js", - "accountJwkPath": "/path/to/account.ecdsa.jwk.json", - "serverPemPath": "/path/to/privkey.rsa.pem" - } -} -``` diff --git a/README.md b/README.md index 039b7db..423a3c7 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,7 @@ Greenlock comes with reasonable defaults but when you install it, you should also install any plugins that you need. ```bash -npm install --save @root/greenlock +npm install --save greenlock npm install --save greenlock-manager-fs npm install --save greenlock-store-fs npm install --save acme-http-01-standalone @@ -420,7 +420,7 @@ TODO ```js 'use strict'; -var Greenlock = require(@root/greenlock-express); +var Greenlock = require(greenlock-express); var greenlock = Greenlock.create({ // for security and critical bug notices diff --git a/accounts.js b/accounts.js deleted file mode 100644 index 2166ab4..0000000 --- a/accounts.js +++ /dev/null @@ -1,219 +0,0 @@ -'use strict'; - -var A = module.exports; -var U = require('./utils.js'); -var E = require('./errors.js'); - -var pending = {}; - -A._getOrCreate = function(gnlck, mconf, db, acme, args) { - var email = args.subscriberEmail || mconf.subscriberEmail; - - if (!email) { - throw E.NO_SUBSCRIBER('get account', args.subject); - } - - // TODO send welcome message with benefit info - return U._validMx(email) - .catch(function() { - throw E.NO_SUBSCRIBER('get account', args.subcriberEmail); - }) - .then(function() { - if (pending[email]) { - return pending[email]; - } - - pending[email] = A._rawGetOrCreate( - gnlck, - mconf, - db, - acme, - args, - email - ) - .catch(function(e) { - delete pending[email]; - throw e; - }) - .then(function(result) { - delete pending[email]; - return result; - }); - - return pending[email]; - }); -}; - -// What we really need out of this is the private key and the ACME "key" id -A._rawGetOrCreate = function(gnlck, mconf, db, acme, args, email) { - var p; - if (db.check) { - p = A._checkStore(gnlck, mconf, db, acme, args, email); - } else { - p = Promise.resolve(null); - } - - return p.then(function(fullAccount) { - if (!fullAccount) { - return A._newAccount(gnlck, mconf, db, acme, args, email, null); - } - - if (fullAccount.keypair && fullAccount.key && fullAccount.key.kid) { - return fullAccount; - } - - return A._newAccount(gnlck, mconf, db, acme, args, email, fullAccount); - }); -}; - -A._newAccount = function(gnlck, mconf, db, acme, args, email, fullAccount) { - var keyType = args.accountKeyType || mconf.accountKeyType; - var query = { - subject: args.subject, - email: email, - subscriberEmail: email, - customerEmail: args.customerEmail, - account: fullAccount || {}, - directoryUrl: - args.directoryUrl || - mconf.directoryUrl || - gnlck._defaults.directoryUrl - }; - - return U._getOrCreateKeypair(db, args.subject, query, keyType).then( - function(kresult) { - var keypair = kresult.keypair; - var accReg = { - subscriberEmail: email, - agreeToTerms: - args.agreeToTerms || - mconf.agreeToTerms || - gnlck._defaults.agreeToTerms, - accountKey: keypair.privateKeyJwk || keypair.private, - debug: args.debug - }; - return acme.accounts.create(accReg).then(function(receipt) { - var reg = { - keypair: keypair, - receipt: receipt, - // shudder... not actually a KeyID... but so it is called anyway... - kid: - receipt && - receipt.key && - (receipt.key.kid || receipt.kid), - email: args.email, - subscriberEmail: email, - customerEmail: args.customerEmail - }; - - var keyP; - if (kresult.exists) { - keyP = Promise.resolve(); - } else { - query.keypair = keypair; - query.receipt = receipt; - /* - query.server = gnlck._defaults.directoryUrl.replace( - /^https?:\/\//i, - '' - ); - */ - keyP = db.setKeypair(query, keypair); - } - - return keyP - .then(function() { - if (!db.set) { - return Promise.resolve({ - keypair: keypair - }); - } - return db.set( - { - // id to be set by Store - email: email, - subscriberEmail: email, - customerEmail: args.customerEmail, - agreeTos: true, - agreeToTerms: true, - directoryUrl: - args.directoryUrl || - mconf.directoryUrl || - gnlck._defaults.directoryUrl - /* - server: gnlck._defaults.directoryUrl.replace( - /^https?:\/\//i, - '' - ) - */ - }, - reg - ); - }) - .then(function(fullAccount) { - if (fullAccount && 'object' !== typeof fullAccount) { - throw new Error( - "accounts.set should either return 'null' or an object with an 'id' string" - ); - } - - if (!fullAccount) { - fullAccount = {}; - } - fullAccount.keypair = keypair; - if (!fullAccount.key) { - fullAccount.key = {}; - } - fullAccount.key.kid = reg.kid; - - return fullAccount; - }); - }); - } - ); -}; - -A._checkStore = function(gnlck, mconf, db, acme, args, email) { - if ((args.domain || args.domains) && !args.subject) { - console.warn("use 'subject' instead of 'domain'"); - args.subject = args.domain; - } - - var account = args.account; - if (!account) { - account = {}; - } - - if (args.accountKey) { - console.warn( - 'rather than passing accountKey, put it directly into your account key store' - ); - // TODO we probably don't need this - return U._importKeypair(args.accountKey); - } - - if (!db.check) { - return Promise.resolve(null); - } - - return db - .check({ - //keypair: undefined, - //receipt: undefined, - email: email, - subscriberEmail: email, - customerEmail: args.customerEmail || mconf.customerEmail, - account: account, - directoryUrl: - args.directoryUrl || - mconf.directoryUrl || - gnlck._defaults.directoryUrl - }) - .then(function(fullAccount) { - if (!fullAccount) { - return null; - } - - return fullAccount; - }); -}; diff --git a/bin/certonly.js b/bin/certonly.js deleted file mode 100755 index cab7126..0000000 --- a/bin/certonly.js +++ /dev/null @@ -1,378 +0,0 @@ -'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)); diff --git a/bin/cli.js b/bin/cli.js deleted file mode 100644 index 49d92f6..0000000 --- a/bin/cli.js +++ /dev/null @@ -1,234 +0,0 @@ -'use strict'; - -var CLI = module.exports; - -var defaultConf; -var defaultOpts; -var bags = []; - -CLI.parse = function(conf) { - var opts = (defaultOpts = {}); - defaultConf = conf; - - Object.keys(conf).forEach(function(k) { - var v = conf[k]; - var aliases = v[5]; - var bag; - var bagName; - - // the name of the argument set is now the 0th argument - v.unshift(k); - // v[0] flagname - // v[1] short flagname - // v[2] description - // v[3] type - // v[4] default value - // v[5] aliases - - if ('bag' === v[3]) { - bag = v[0]; // 'bag-option-xxxx' => '--bag-option-' - bag = '--' + bag.replace(/xxx.*/, ''); - bags.push(bag); - - bagName = toBagName(bag.replace(/^--/, '')); - opts[bagName] = {}; - } - - if ('json' === v[3]) { - bagName = toBagName(v[0].replace(/-json$/, '')); // 'bag-option-json' => 'bagOptionOpts' - opts[bagName] = {}; - } else if ('ignore' !== v[3] && 'undefined' !== typeof v[4]) { - // set the default values (where 'undefined' is not an allowed value) - opts[toCamel(k)] = v[4]; - } - - if (!aliases) { - aliases = []; - } else if ('string' === typeof aliases) { - aliases = aliases.split(','); - } - aliases.forEach(function(alias) { - if (alias in conf) { - throw new Error( - "Cannot alias '" + - alias + - "' from '" + - k + - "': option already exists" - ); - } - conf[alias] = v; - }); - }); -}; - -CLI.main = function(cb, args) { - var leftovers = []; - var conf = defaultConf; - var opts = defaultOpts; - - if (!opts) { - throw new Error("you didn't call `CLI.parse(configuration)`"); - } - - // TODO what's the existing API for this? - if (!args) { - args = process.argv.slice(2); - } - - var flag; - var cnf; - var typ; - - function grab(bag) { - var bagName = toBagName(bag); - if (bag !== flag.slice(0, bag.length)) { - return false; - } - console.log(bagName, toCamel(flag.slice(bag.length))); - opts[bagName][toCamel(flag.slice(bag.length))] = args.shift(); - return true; - } - - while (args.length) { - // take one off the top - flag = args.shift(); - - // mind the gap - if ('--' === flag) { - leftovers = leftovers.concat(args); - break; - } - - // help! - if ( - '--help' === flag || - '-h' === flag || - '/?' === flag || - 'help' === flag - ) { - printHelp(conf); - process.exit(1); - } - - // only long names are actually used - if ('--' !== flag.slice(0, 2)) { - console.error("Unrecognized argument '" + flag + "'"); - process.exit(1); - } - - cnf = conf[flag.slice(2)]; - if (!cnf) { - // look for arbitrary flags - if (bags.some(grab)) { - continue; - } - - // other arbitrary args are not used - console.error("Unrecognized flag '" + flag + "'"); - process.exit(1); - } - - // encourage switching to non-aliased version - if (flag !== '--' + cnf[0]) { - console.warn( - "use of '" + - flag + - "' is deprecated, use '--" + - cnf[0] + - "' instead" - ); - } - - // look for xxx-json flags - if ('json' === cnf[3]) { - try { - var json = JSON.parse(args.shift()); - var bagName = toBagName(cnf[0].replace(/-json$/, '')); - Object.keys(json).forEach(function(k) { - opts[bagName][k] = json[k]; - }); - } catch (e) { - console.error("Could not parse option '" + flag + "' as JSON:"); - console.error(e.message); - process.exit(1); - } - continue; - } - - // set booleans, otherwise grab the next arg in line - typ = cnf[3]; - // TODO --no- to negate - if (Boolean === typ || 'boolean' === typ) { - opts[toCamel(cnf[0])] = true; - continue; - } - opts[toCamel(cnf[0])] = args.shift(); - continue; - } - - cb(leftovers, opts); -}; - -function toCamel(str) { - return str.replace(/-([a-z0-9])/g, function(m) { - return m[1].toUpperCase(); - }); -} - -function toBagName(bag) { - // trim leading and trailing '-' - bag = bag.replace(/^-+/g, '').replace(/-+$/g, ''); - return toCamel(bag) + 'Opts'; // '--bag-option-' => bagOptionOpts -} - -function printHelp(conf) { - var flagLen = 0; - var typeLen = 0; - var defLen = 0; - - Object.keys(conf).forEach(function(k) { - flagLen = Math.max(flagLen, conf[k][0].length); - typeLen = Math.max(typeLen, conf[k][3].length); - if ('undefined' !== typeof conf[k][4]) { - defLen = Math.max( - defLen, - '(Default: )'.length + String(conf[k][4]).length - ); - } - }); - - Object.keys(conf).forEach(function(k) { - var v = conf[k]; - - // skip aliases - if (v[0] !== k) { - return; - } - - var def = v[4]; - if ('undefined' === typeof def) { - def = ''; - } else { - def = '(default: ' + JSON.stringify(def) + ')'; - } - - var msg = - ' --' + - v[0].padEnd(flagLen) + - ' ' + - v[3].padStart(typeLen + 1) + - ' ' + - (v[2] || '') + - ' ' + - def; /*.padStart(defLen)*/ - // v[0] flagname - // v[1] short flagname - // v[2] description - // v[3] type - // v[4] default value - // v[5] aliases - - console.info(msg); - }); -} diff --git a/bin/greenlock.js b/bin/greenlock.js deleted file mode 100755 index 51cc82c..0000000 --- a/bin/greenlock.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -var args = process.argv.slice(2); -//console.log(args); -//['certonly', 'add', 'config', 'defaults', 'remove'] -if ('certonly' === args[0]) { - require('./certonly.js'); - return; -} - -console.error("command not yet implemented"); -process.exit(); diff --git a/certificates.js b/certificates.js deleted file mode 100644 index 8bd44a7..0000000 --- a/certificates.js +++ /dev/null @@ -1,318 +0,0 @@ -'use strict'; - -var C = module.exports; -var U = require('./utils.js'); -var CSR = require('@root/csr'); -var Enc = require('@root/encoding'); -var Keypairs = require('@root/keypairs'); - -var pending = {}; -var rawPending = {}; - -// What the abbreviations mean -// -// gnlkc => greenlock -// mconf => manager config -// db => greenlock store instance -// acme => instance of ACME.js -// chs => instances of challenges -// acc => account -// args => site / extra options - -// Certificates -C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) { - var email = args.subscriberEmail || mconf.subscriberEmail; - - var id = args.altnames - .slice(0) - .sort() - .join(' '); - if (pending[id]) { - return pending[id]; - } - - pending[id] = C._rawGetOrOrder( - gnlck, - mconf, - db, - acme, - chs, - acc, - email, - args - ) - .then(function(pems) { - delete pending[id]; - return pems; - }) - .catch(function(err) { - delete pending[id]; - throw err; - }); - - return pending[id]; -}; - -// Certificates -C._rawGetOrOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { - return C._check(gnlck, mconf, db, args).then(function(pems) { - // Nice and fresh? We're done! - if (pems) { - if (!C._isStale(gnlck, mconf, args, pems)) { - // return existing unexpired (although potentially stale) certificates when available - // there will be an additional .renewing property if the certs are being asynchronously renewed - //pems._type = 'current'; - return pems; - } - } - - // We're either starting fresh or freshening up... - var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args); - var evname = pems ? 'cert_renewal' : 'cert_issue'; - p.then(function(newPems) { - // notify in the background - var renewAt = C._renewWithStagger(gnlck, mconf, args, newPems); - gnlck._notify(evname, { - renewAt: renewAt, - subject: args.subject, - altnames: args.altnames - }); - gnlck._notify('_cert_issue', { - renewAt: renewAt, - subject: args.subject, - altnames: args.altnames, - pems: newPems - }); - }).catch(function(err) { - if (!err.context) { - err.context = evname; - } - err.subject = args.subject; - err.altnames = args.altnames; - gnlck._notify('error', err); - }); - - // No choice but to hang tight and wait for it - if ( - !pems || - pems.renewAt < Date.now() - 24 * 60 * 60 * 1000 || - pems.expiresAt <= Date.now() + 24 * 60 * 60 * 1000 - ) { - return p; - } - - // Wait it out - // TODO should we call this waitForRenewal? - if (args.waitForRenewal) { - return p; - } - - // Let the certs renew in the background - return pems; - }); -}; - -// we have another promise here because it the optional renewal -// may resolve in a different stack than the returned pems -C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) { - var id = args.altnames - .slice(0) - .sort() - .join(' '); - if (rawPending[id]) { - return rawPending[id]; - } - - var keyType = args.serverKeyType || mconf.serverKeyType; - var query = { - subject: args.subject, - certificate: args.certificate || {}, - directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl - }; - rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType) - .then(function(kresult) { - var serverKeypair = kresult.keypair; - var domains = args.altnames.slice(0); - - return CSR.csr({ - jwk: serverKeypair.privateKeyJwk || serverKeypair.private, - domains: domains, - encoding: 'der' - }) - .then(function(csrDer) { - // TODO let CSR support 'urlBase64' ? - return Enc.bufToUrlBase64(csrDer); - }) - .then(function(csr) { - function notify(ev, opts) { - gnlck._notify(ev, opts); - } - var certReq = { - debug: args.debug || gnlck._defaults.debug, - - challenges: chs, - account: acc, // only used if accounts.key.kid exists - accountKey: - acc.keypair.privateKeyJwk || acc.keypair.private, - keypair: acc.keypair, // TODO - csr: csr, - domains: domains, // because ACME.js v3 uses `domains` still, actually - onChallengeStatus: notify, - notify: notify // TODO - - // TODO handle this in acme-v2 - //subject: args.subject, - //altnames: args.altnames.slice(0), - }; - return acme.certificates - .create(certReq) - .then(U._attachCertInfo); - }) - .then(function(pems) { - if (kresult.exists) { - return pems; - } - query.keypair = serverKeypair; - return db.setKeypair(query, serverKeypair).then(function() { - return pems; - }); - }); - }) - .then(function(pems) { - // TODO put this in the docs - // { cert, chain, privkey, subject, altnames, issuedAt, expiresAt } - // Note: the query has been updated - query.pems = pems; - return db.set(query); - }) - .then(function() { - return C._check(gnlck, mconf, db, args); - }) - .then(function(bundle) { - // TODO notify Manager - delete rawPending[id]; - return bundle; - }) - .catch(function(err) { - // Todo notify manager - delete rawPending[id]; - throw err; - }); - - return rawPending[id]; -}; - -// returns pems, if they exist -C._check = function(gnlck, mconf, db, args) { - var query = { - subject: args.subject, - // may contain certificate.id - certificate: args.certificate, - directoryUrl: args.directoryUrl || gnlck._defaults.directoryUrl - }; - return db.check(query).then(function(pems) { - if (!pems) { - return null; - } - - pems = U._attachCertInfo(pems); - - // For eager management - if (args.subject && !U._certHasDomain(pems, args.subject)) { - // TODO report error, but continue the process as with no cert - return null; - } - - // For lazy SNI requests - if (args.domain && !U._certHasDomain(pems, args.domain)) { - // TODO report error, but continue the process as with no cert - return null; - } - - return U._getKeypair(db, args.subject, query) - .then(function(keypair) { - return Keypairs.export({ - jwk: keypair.privateKeyJwk || keypair.private, - encoding: 'pem' - }).then(function(pem) { - pems.privkey = pem; - return pems; - }); - }) - .catch(function() { - // TODO report error, but continue the process as with no cert - return null; - }); - }); -}; - -// Certificates -C._isStale = function(gnlck, mconf, args, pems) { - if (args.duplicate) { - return true; - } - - var renewAt = C._renewableAt(gnlck, mconf, args, pems); - - if (Date.now() >= renewAt) { - return true; - } - - return false; -}; - -C._renewWithStagger = function(gnlck, mconf, args, pems) { - var renewOffset = C._renewOffset(gnlck, mconf, args, pems); - var renewStagger; - try { - renewStagger = U._parseDuration( - args.renewStagger || mconf.renewStagger || 0 - ); - } catch (e) { - renewStagger = U._parseDuration( - args.renewStagger || mconf.renewStagger - ); - } - - // TODO check this beforehand - if (!args.force && renewStagger / renewOffset >= 0.5) { - renewStagger = renewOffset * 0.1; - } - - if (renewOffset > 0) { - // stagger forward, away from issued at - return Math.round( - pems.issuedAt + renewOffset + Math.random() * renewStagger - ); - } - - // stagger backward, toward issued at - return Math.round( - pems.expiresAt + renewOffset - Math.random() * renewStagger - ); -}; -C._renewOffset = function(gnlck, mconf, args /*, pems*/) { - var renewOffset = U._parseDuration( - args.renewOffset || mconf.renewOffset || 0 - ); - var week = 1000 * 60 * 60 * 24 * 6; - if (!args.force && Math.abs(renewOffset) < week) { - throw new Error( - 'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset' - ); - } - return renewOffset; -}; -C._renewableAt = function(gnlck, mconf, args, pems) { - if (args.renewAt) { - return args.renewAt; - } - - var renewOffset = C._renewOffset(gnlck, mconf, args, pems); - - if (renewOffset > 0) { - return pems.issuedAt + renewOffset; - } - - return pems.expiresAt + renewOffset; -}; diff --git a/challenges-underlay.js b/challenges-underlay.js deleted file mode 100644 index 8d4d7f9..0000000 --- a/challenges-underlay.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict'; - -var Greenlock = require('./'); - -module.exports.wrap = function(greenlock) { - greenlock.challenges.get = function(chall) { - // TODO pick one and warn on the others - // (just here due to some backwards compat issues with early v3 plugins) - var servername = - chall.servername || - chall.altname || - (chall.identifier && chall.identifier.value); - - // TODO some sort of caching to prevent database hits? - return greenlock - ._config({ servername: servername }) - .then(function(site) { - if (!site) { - return null; - } - - // Hmm... this _should_ be impossible - if (!site.challenges || !site.challenges['http-01']) { - var copy = JSON.parse(JSON.stringify(site)); - sanitizeCopiedConf(copy); - sanitizeCopiedConf(copy.store); - if (site.challenges) { - sanitizeCopiedConf(copy.challenges['http-01']); - sanitizeCopiedConf(copy.challenges['dns-01']); - sanitizeCopiedConf(copy.challenges['tls-alpn-01']); - } - console.warn('[Bug] Please report this error:'); - console.warn( - '\terror: http-01 challenge requested, but not even a default http-01 config exists' - ); - console.warn('\tservername:', JSON.stringify(servername)); - console.warn('\tsite:', JSON.stringify(copy)); - return null; - } - - return Greenlock._loadChallenge(site.challenges, 'http-01'); - }) - .then(function(plugin) { - if (!plugin) { - return null; - } - return plugin - .get({ - challenge: { - type: chall.type, - //hostname: chall.servername, - altname: chall.servername, - identifier: { value: chall.servername }, - token: chall.token - } - }) - .then(function(result) { - var keyAuth; - var keyAuthDigest; - if (result) { - // backwards compat that shouldn't be dropped - // because new v3 modules had to do this to be - // backwards compatible with Greenlock v2.7 at - // the time. - if (result.challenge) { - result = result.challenge; - } - keyAuth = result.keyAuthorization; - keyAuthDigest = result.keyAuthorizationDigest; - } - if (/dns/.test(chall.type)) { - return { - keyAuthorizationDigest: keyAuthDigest - }; - } - return { - keyAuthorization: keyAuth - }; - }); - }); - }; -}; - -function sanitizeCopiedConf(copy) { - if (!copy) { - return; - } - - Object.keys(copy).forEach(function(k) { - if (/(api|key|token)/i.test(k) && 'string' === typeof copy[k]) { - copy[k] = '**redacted**'; - } - }); - - return copy; -} diff --git a/errors.js b/errors.js deleted file mode 100644 index 5fb0a0d..0000000 --- a/errors.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -var E = module.exports; - -function create(code, msg) { - E[code] = function(ctx, msg2) { - var err = new Error(msg); - err.code = code; - err.context = ctx; - if (msg2) { - err.message += ': ' + msg2; - } - /* - Object.keys(extras).forEach(function(k) { - if ('message' === k) { - err.message += ': ' + extras[k]; - } else { - err[k] = extras[k]; - } - }); - */ - return err; - }; -} - -// TODO open issues and link to them as the error url -create( - 'NO_MAINTAINER', - 'please supply `maintainerEmail` as a contact for security and critical bug notices' -); -create( - 'BAD_ORDER', - 'altnames should be in deterministic order, with subject as the first altname' -); -create('NO_SUBJECT', 'no certificate subject given'); -create( - 'NO_SUBSCRIBER', - 'please supply `subscriberEmail` as a contact for failed renewal and certificate revocation' -); -create( - 'INVALID_SUBSCRIBER', - '`subscriberEmail` is not a valid address, please check for typos' -); -create( - 'INVALID_HOSTNAME', - 'valid hostnames must be restricted to a-z0-9_.- and contain at least one "."' -); -create( - 'INVALID_DOMAIN', - 'one or more domains do not exist on public DNS SOA record' -); -create( - 'NOT_UNIQUE', - 'found duplicate domains, or a subdomain that overlaps a wildcard' -); - -// exported for testing only -E._create = create; diff --git a/greenlock.js b/greenlock.js index 5aa199a..846a0cb 100644 --- a/greenlock.js +++ b/greenlock.js @@ -1,683 +1,3 @@ 'use strict'; -var pkg = require('./package.json'); - -var ACME = require('@root/acme'); -var Greenlock = module.exports; -var request = require('@root/request'); - -var G = Greenlock; -var U = require('./utils.js'); -var E = require('./errors.js'); -var P = require('./plugins.js'); -var A = require('./accounts.js'); -var C = require('./certificates.js'); -var UserEvents = require('./user-events.js'); - -var caches = {}; - -// { maintainerEmail, directoryUrl, subscriberEmail, store, challenges } -G.create = function(gconf) { - var greenlock = {}; - var gdefaults = {}; - if (!gconf) { - gconf = {}; - } - var manager; - - greenlock._create = function() { - if (!gconf.maintainerEmail) { - throw E.NO_MAINTAINER('create'); - } - - // TODO send welcome message with benefit info - U._validMx(gconf.maintainerEmail).catch(function() { - console.error( - 'invalid maintainer contact info:', - gconf.maintainerEmail - ); - - // maybe move this to init and don't exit the process, just in case - process.exit(1); - }); - - if ('function' === typeof gconf.notify) { - gdefaults.notify = gconf.notify; - } else { - gdefaults.notify = _notify; - } - - if (gconf.directoryUrl) { - gdefaults = gconf.directoryUrl; - if (gconf.staging) { - throw new Error( - 'supply `directoryUrl` or `staging`, but not both' - ); - } - } else if (gconf.staging) { - gdefaults.directoryUrl = - 'https://acme-staging-v02.api.letsencrypt.org/directory'; - } else { - gdefaults.directoryUrl = - 'https://acme-v02.api.letsencrypt.org/directory'; - } - console.info('ACME Directory URL:', gdefaults.directoryUrl); - - manager = normalizeManager(gconf); - - // Wraps each of the following with appropriate error checking - // greenlock.manager.defaults - // greenlock.manager.add - // greenlock.manager.update - // greenlock.manager.remove - // greenlock.manager.find - require('./manager-underlay.js').wrap(greenlock, manager, gconf); - - // Exports challenges.get for Greenlock Express HTTP-01, - // and whatever odd use case pops up, I suppose - // greenlock.challenges.get - require('./challenges-underlay.js').wrap(greenlock, manager, gconf); - - greenlock._defaults = gdefaults; - greenlock._defaults.debug = gconf.debug; - - // renew every 90-ish minutes (random for staggering) - // the weak setTimeout (unref) means that when run as a CLI process this - // will still finish as expected, and not wait on the timeout - (function renew() { - setTimeout(function() { - greenlock.renew({}); - renew(); - }, Math.PI * 30 * 60 * 1000).unref(); - })(); - }; - - // The purpose of init is to make MCONF the source of truth - greenlock._init = function() { - var p; - greenlock._init = function() { - return p; - }; - - if (manager.init) { - // TODO punycode? - p = manager.init({ - request: request - //punycode: require('punycode') - }); - } else { - p = Promise.resolve(); - } - p = p - .then(function() { - return manager.defaults().then(function(MCONF) { - mergeDefaults(MCONF, gconf); - if (true === MCONF.agreeToTerms) { - gdefaults.agreeToTerms = function(tos) { - return Promise.resolve(tos); - }; - } - return manager.defaults(MCONF); - }); - }) - .catch(function(err) { - console.error('Fatal error during greenlock init:'); - console.error(err); - process.exit(1); - }); - return p; - }; - - // The goal here is to reduce boilerplate, such as error checking - // and duration parsing, that a manager must implement - greenlock.sites.add = greenlock.add = greenlock.manager.add; - - greenlock.notify = greenlock._notify = function(ev, params) { - var mng = greenlock.manager; - - if ('_' === String(ev)[0]) { - if ('_cert_issue' === ev) { - try { - mng.update({ - subject: params.subject, - renewAt: params.renewAt - }).catch(function(e) { - e.context = '_cert_issue'; - greenlock._notify('error', e); - }); - } catch (e) { - e.context = '_cert_issue'; - greenlock._notify('error', e); - } - } - // trap internal events internally - return; - } - - try { - var p = greenlock._defaults.notify(ev, params); - if (p && p.catch) { - p.catch(function(e) { - console.error("Promise Rejection on event '" + ev + "':"); - console.error(e); - }); - } - } catch (e) { - console.error("Thrown Exception on event '" + ev + "':"); - console.error(e); - console.error(params); - } - - if (-1 !== ['cert_issue', 'cert_renewal'].indexOf(ev)) { - // We will notify all greenlock users of mandatory and security updates - // We'll keep track of versions and os so we can make sure things work well - // { name, version, email, domains, action, communityMember, telemetry } - // TODO look at the other one - UserEvents.notify({ - /* - // maintainer should be only on pre-publish, or maybe install, I think - maintainerEmail: greenlock._defaults._maintainerEmail, - name: greenlock._defaults._packageAgent, - version: greenlock._defaults._maintainerPackageVersion, - //action: params.pems._type, - domains: params.altnames, - subscriberEmail: greenlock._defaults._subscriberEmail, - // TODO enable for Greenlock Pro - //customerEmail: args.customerEmail - telemetry: greenlock._defaults.telemetry - */ - }); - } - }; - - // certs.get - greenlock.get = function(args) { - return greenlock - ._single(args) - .then(function() { - args._includePems = true; - return greenlock.renew(args); - }) - .then(function(results) { - if (!results || !results.length) { - // TODO throw an error here? - return null; - } - - // just get the first one - var result = results[0]; - - // (there should be only one, ideally) - if (results.length > 1) { - var err = new Error( - "a search for '" + - args.servername + - "' returned multiple certificates" - ); - err.context = 'duplicate_certs'; - err.servername = args.servername; - err.subjects = results.map(function(r) { - return (r.site || {}).subject || 'N/A'; - }); - - greenlock._notify('warning', err); - } - - if (result.error) { - return Promise.reject(result.error); - } - - // site for plugin options, such as http-01 challenge - // pems for the obvious reasons - return result; - }); - }; - - greenlock._single = function(args) { - if ('string' !== typeof args.servername) { - return Promise.reject(new Error('no `servername` given')); - } - // www.example.com => *.example.com - args.wildname = - '*.' + - args.servername - .split('.') - .slice(1) - .join('.'); - if ( - args.servernames || - args.subject || - args.renewBefore || - args.issueBefore || - args.expiresBefore - ) { - return Promise.reject( - new Error( - 'bad arguments, did you mean to call greenlock.renew()?' - ) - ); - } - // duplicate, force, and others still allowed - return Promise.resolve(args); - }; - - greenlock._config = function(args) { - return greenlock - ._single(args) - .then(function() { - return greenlock._find(args); - }) - .then(function(sites) { - if (!sites || !sites.length) { - return null; - } - var site = sites[0]; - site = JSON.parse(JSON.stringify(site)); - if (site.store && site.challenges) { - return site; - } - return manager.defaults().then(function(mconf) { - if (!site.store) { - site.store = mconf.store; - } - if (!site.challenges) { - site.challenges = mconf.challenges; - } - return site; - }); - }); - }; - - // needs to get info about the renewal, such as which store and challenge(s) to use - greenlock.renew = function(args) { - return greenlock._init().then(function() { - return manager.defaults().then(function(mconf) { - return greenlock._renew(mconf, args); - }); - }); - }; - greenlock._renew = function(mconf, args) { - if (!args) { - args = {}; - } - - var renewedOrFailed = []; - //console.log('greenlock._renew find', args); - return greenlock._find(args).then(function(sites) { - // Note: the manager must guaranteed that these are mutable copies - //console.log('greenlock._renew found', sites);; - - function next() { - var site = sites.shift(); - if (!site) { - return Promise.resolve(null); - } - - var order = { site: site }; - renewedOrFailed.push(order); - // TODO merge args + result? - return greenlock - ._order(mconf, site) - .then(function(pems) { - if (args._includePems) { - order.pems = pems; - } - }) - .catch(function(err) { - order.error = err; - - // For greenlock express serialization - err.toJSON = errorToJSON; - err.context = err.context || 'cert_order'; - err.subject = site.subject; - if (args.servername) { - err.servername = args.servername; - } - // for debugging, but not to be relied on - err._site = site; - // TODO err.context = err.context || 'renew_certificate' - greenlock._notify('error', err); - }) - .then(function() { - return next(); - }); - } - - return next().then(function() { - return renewedOrFailed; - }); - }); - }; - - greenlock._acme = function(args) { - var packageAgent = gconf.packageAgent || ''; - // because Greenlock_Express/v3.x Greenlock/v3 is redundant - if (!/greenlock/i.test(packageAgent)) { - packageAgent = (packageAgent + ' Greenlock/' + pkg.version).trim(); - } - var acme = ACME.create({ - maintainerEmail: gconf.maintainerEmail, - packageAgent: packageAgent, - notify: greenlock._notify, - debug: greenlock._defaults.debug || args.debug - }); - var dirUrl = args.directoryUrl || greenlock._defaults.directoryUrl; - - var dir = caches[dirUrl]; - - // don't cache more than an hour - if (dir && Date.now() - dir.ts < 1 * 60 * 60 * 1000) { - return dir.promise; - } - - return acme - .init(dirUrl) - .then(function(/*meta*/) { - caches[dirUrl] = { - promise: Promise.resolve(acme), - ts: Date.now() - }; - return acme; - }) - .catch(function(err) { - // TODO - // let's encrypt is possibly down for maintenaince... - // this is a special kind of failure mode - throw err; - }); - }; - - greenlock.order = function(args) { - return greenlock._init().then(function() { - return manager.defaults().then(function(mconf) { - return greenlock._order(mconf, args); - }); - }); - }; - greenlock._order = function(mconf, args) { - // packageAgent, maintainerEmail - return greenlock._acme(args).then(function(acme) { - var storeConf = args.store || mconf.store; - return P._loadStore(storeConf).then(function(store) { - return A._getOrCreate( - greenlock, - mconf, - store.accounts, - acme, - args - ).then(function(account) { - var challengeConfs = args.challenges || mconf.challenges; - return Promise.all( - Object.keys(challengeConfs).map(function(typ01) { - return P._loadChallenge(challengeConfs, typ01); - }) - ).then(function(arr) { - var challenges = {}; - arr.forEach(function(el) { - challenges[el._type] = el; - }); - return C._getOrOrder( - greenlock, - mconf, - store.certificates, - acme, - challenges, - account, - args - ).then(function(pems) { - if (!pems) { - throw new Error('no order result'); - } - if (!pems.privkey) { - throw new Error( - 'missing private key, which is kinda important' - ); - } - return pems; - }); - }); - }); - }); - }); - }; - - greenlock._create(); - - return greenlock; -}; - -G._loadChallenge = P._loadChallenge; - -function errorToJSON(e) { - var error = {}; - Object.getOwnPropertyNames(e).forEach(function(k) { - error[k] = e[k]; - }); - return error; -} - -function normalizeManager(gconf) { - var m; - // 1. Get the manager - // 2. Figure out if we need to wrap it - - if (!gconf.manager) { - gconf.manager = 'greenlock-manager-fs'; - if (gconf.find) { - // { manager: 'greenlock-manager-fs', find: function () { } } - warpFind(gconf); - } - } - - if ('string' === typeof gconf.manager) { - try { - // wrap this to be safe for greenlock-manager-fs - m = require(gconf.manager).create(gconf); - } catch (e) { - console.error(e.code); - console.error(e.message); - } - } else { - m = gconf.manager; - } - - if (!m) { - console.error(); - console.error( - 'Failed to load manager plugin ', - JSON.stringify(gconf.manager) - ); - console.error(); - process.exit(1); - } - - if ( - ['set', 'remove', 'find', 'defaults'].every(function(k) { - return 'function' === typeof m[k]; - }) - ) { - return m; - } - - // { manager: { find: function () { } } } - if (m.find) { - warpFind(m); - } - // m.configFile could also be set - m = require('greenlock-manager-fs').create(m); - - if ('function' !== typeof m.find) { - console.error(); - console.error( - JSON.stringify(gconf.manager), - 'must implement `find()` and should implement `set()`, `remove()`, `defaults()`, and `init()`' - ); - console.error(); - process.exit(1); - } - - return m; -} - -function warpFind(gconf) { - gconf.__gl_find = gconf.find; - gconf.find = function(args) { - // the incoming args will be normalized by greenlock - return gconf.__gl_find(args).then(function(sites) { - // we also need to error check the incoming sites, - // as if they were being passed through `add()` or `set()` - // (effectively they are) because the manager assumes that - // they're not bad - sites.forEach(function(s) { - if (!s || 'string' !== typeof s.subject) { - throw new Error('missing subject'); - } - if ( - !Array.isArray(s.altnames) || - !s.altnames.length || - !s.altnames[0] || - s.altnames[0] !== s.subject - ) { - throw new Error('missing or malformed altnames'); - } - ['renewAt', 'issuedAt', 'expiresAt'].forEach(function(k) { - if (s[k]) { - throw new Error( - '`' + - k + - '` should be updated by `set()`, not by `find()`' - ); - } - }); - }); - }); - }; -} - -function mergeDefaults(MCONF, gconf) { - if ( - gconf.agreeToTerms === true || - MCONF.agreeToTerms === true || - // TODO deprecate - gconf.agreeTos === true || - MCONF.agreeTos === true - ) { - MCONF.agreeToTerms = true; - } - - if (!MCONF.subscriberEmail && gconf.subscriberEmail) { - MCONF.subscriberEmail = gconf.subscriberEmail; - } - - var homedir; - // Load the default store module - if (!MCONF.store) { - if (gconf.store) { - MCONF.store = gconf.store; - } else { - homedir = require('os').homedir(); - MCONF.store = { - module: 'greenlock-store-fs', - basePath: homedir + '/.config/greenlock/' - }; - } - } - // just to test that it loads - P._loadSync(MCONF.store.module); - - // Load the default challenge modules - var challenges = MCONF.challenges || gconf.challenges; - if (!challenges) { - challenges = {}; - } - if (!challenges['http-01'] && !challenges['dns-01']) { - challenges['http-01'] = { module: 'acme-http-01-standalone' }; - } - if (challenges['http-01']) { - if ('string' !== typeof challenges['http-01'].module) { - throw new Error( - 'bad challenge http-01 module config:' + - JSON.stringify(challenges['http-01']) - ); - } - P._loadSync(challenges['http-01'].module); - } - if (challenges['dns-01']) { - if ('string' !== typeof challenges['dns-01'].module) { - throw new Error( - 'bad challenge dns-01 module config' + - JSON.stringify(challenges['dns-01']) - ); - } - P._loadSync(challenges['dns-01'].module); - } - MCONF.challenges = challenges; - - if (!MCONF.renewOffset) { - MCONF.renewOffset = gconf.renewOffset || '-45d'; - } - if (!MCONF.renewStagger) { - MCONF.renewStagger = gconf.renewStagger || '3d'; - } - - if (!MCONF.accountKeyType) { - MCONF.accountKeyType = gconf.accountKeyType || 'EC-P256'; - } - if (!MCONF.serverKeyType) { - MCONF.serverKeyType = gconf.serverKeyType || 'RSA-2048'; - } -} - -function _notify(ev, args) { - if (!args) { - args = ev; - ev = args.event; - delete args.event; - } - - // TODO define message types - if (!_notify._notice) { - console.info( - 'set greenlockOptions.notify to override the default logger' - ); - _notify._notice = true; - } - var prefix = 'Warning'; - switch (ev) { - case 'error': - prefix = 'Error'; - /* falls through */ - case 'warning': - console.error( - prefix + '%s:', - (' ' + (args.context || '')).trimRight() - ); - console.error(args.message); - if (args.description) { - console.error(args.description); - } - if (args.code) { - console.error('code:', args.code); - } - if (args.stack) { - console.error(args.stack); - } - break; - default: - if (/status/.test(ev)) { - console.info( - ev, - args.altname || args.subject || '', - args.status || '' - ); - if (!args.status) { - console.info(args); - } - break; - } - console.info( - ev, - '(more info available: ' + Object.keys(args).join(' ') + ')' - ); - } -} +module.exports = require('@root/greenlock'); diff --git a/logo/beaker-browser-301x112.png b/logo/beaker-browser-301x112.png deleted file mode 100755 index 386b24f61463dcb22ae3e952000d1642a86306c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3481 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 PGbw;B)(BIBc6#t%lwdoH diff --git a/logo/from-not-secure-to-secure-url-bar.png b/logo/from-not-secure-to-secure-url-bar.png deleted file mode 100644 index 91517c136b32134ae5208ff03b4ebdf3908c5587..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34652 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

qA-Q(A4;Zw1+ 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

!b`UudCfDBX1} z4Ay4ucbgp$@Z4{X`tWMW=P1$-ntkaW(GnK3QT~iC-EPlb-FAMN8j}X?I~#ErAQm{y zjO2sgxxV(zTu{%T35F`UXC*I|*r^OP8!V@LRMQ=4G14|ym&fqD?Ib3m$u0L1y@9>o z(g1i3nOk~S2W>vwelXuHg5^g3n)FQWy?d{7TNB^#kE00>|2Jz?tv(f+iGQ?f02aNS z*SZC6rI>0bwh^Pg-8(U#EV<_=oUmFb-!`_Kn{#EdWL-=M8M$88^S(ji+Z+cHK#7N$_5CV;6_`HG6ZW}{WbmC(p6LMCw^ zL;rX|`>k-YzC}2!AYp3Dn|RS7-doObeb%b|GiIR5!Y%W~^9?Y8emuT-+$Y>R`3ygT z!hU&ZQ$^m!1YlwZ5f&o&j`OsT-KzJ-q8Eu?*Gh`hk}Tc~PpcDe=pcZhZrfww_3Su^w7nvooB# z3jc4l6u5@AjbDq6WBh$XWNRQj<+}arQua;zw9TE%I!+BntF6VH#e)`gr}gP^hH|}@ zigHb?vn@u(OnH8Jg1C8-v~Q3tK3pk@1>hgtK9yX7Bw7w1iPg1VH|I9!&`2PiA z1D^aF3Z~eXeF)L?9_O1)mzkW_11FTU|V>G6IyS zv_xnLb=anXFDpu|B&a~Z=K0*RLZR3|=q;vDC=?3CvkPrs0oO|J{q?X;_GH<%`fK}h zONAg9U8e*EB?a6*e}N@oNx@}7Ky;!0_;O5OB&OxO@&`jGd!c-R;E08&s3_*>Qf74_ z6?$@gU}ct%LpfG)KOv-4aeCbGU-Y6mS$v1=+LN&!O4l&wyD_+XDfEMyL|!=ONkB`!>A^Zw-r~XEr(H)&)Pa*M+~P#( zlcPj3#8rUO`Z?BIO{j;(S2@3;CXpfY@-&4)ahstrClm^WLZR5asIo0k_sHWC73FRj zO=6*H)M&O5?KYxTl79V6)r*)PwzW_K>i7k<5)D*QE5Ltwbbly5PFbnTs|N^0xaPY4 z%Q`D@%5&F`$Hyk<#4q{WpPD4`(oJN> zeQ{3wd2;=8vb)p``P@JIrC-D|2}-&=PaSdY!fG*2er3E7OeF} z=$GqHkp$^<8m=2%CV6-sY+t&uJjH&?RWXul#OHN8VyvMr3>V|Yk#mMjnCRNM>4?}7 z&Sisol5$RxBPQ(mV*=4iaim2REFydbOE{3U)RLq_oj!poPS1&nD(ghOMi(Oqk)~CJ n`tcd(dz{HrQbM7)1Ni>|Z^9>dbczJp00000NkvXXu0mjf%5}y} diff --git a/logo/greenlock-1063x250.png b/logo/greenlock-1063x250.png deleted file mode 100755 index fdad0d7e7f0a3cebf5a469a183e8b3650a15ff29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18245 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 (<$kJ?r zY)Z97xp~ddR1F!mDXmx;W`MTV<_o=uK}k5o%Yyql(RKj_c-R9;^Mh|ku)(MX-xX(R z(X|*`*jz0-hh7GQCM*)`h3eA<A!Q*4Z!VxZwi!*TQ5x*@*r^%ny zXUvzAr;m;id6dG?oKV|X#au7&{mt#!ZMxOrkcef77@{#=Aqq#R&L^8+H<-ORhMY+Y~XDrk)&*Z&E9*WOxdf{#{*h)RPOFIvem zcq<1Ws3S)$)u?~81gqK|G$QKd4K-=fms+KRV`CeDi>^-5MX% z9o0Y#>YT638-604q;v`Y7beW4wSUU~YyR)n*zQVa2}~qKS}yOlCy3Q!(S09giK_#Q zdO<2a-st zYqA_9JGtg1=H%k4`qHSuN#9kPY{QB(bQ-2SSIK5Haxe8EA^njjV`T?XLOd7&jlAk| zQVU+sWL!pSxH;E3DrGHW&}y@gg0$Q36j)-cR>2y^7%ROBM91>Kjn3V9RXtA^#V$u! z@;cZ94Qo%KWurR44q-{XowAE6wfsigDW#0qRGePwve}9j`HXz=O}2AhUvvOSt?VR| zA30OsxR^Bd*KilQpH;KI90Pd|(ueF;%_Fj*YD0i0U{*k*0GUe8siy^djwGJ(2SBQ6 z+O_LocOPMt2_Fa}a)veLTUW6jb)31=eOm5QtyS$NIkXfi4zBBDNM6D?oudT5Psofe z-YeS(D-cti-!3pcqu*9m>S5hW*>hC-~ith;itqFUVl5 z^A`ccfDre8ZTSC%4RZdUbFzNjXktqxRKq2yncK@0p_mC5Q(neq3kho^MG2QE8b;|j7HQRrNcMNl z``lE76WP|vhFRg}{jD&|LU)~W&pqdUx}W;+yq@R7?>sNRC*wHINdu|}1pq+9#Tn}f z01%YYk5W@rYS&bgXr);nNl~G# zrAeyz#)-m$;_AwLn;jDW^jVpd>qCIMUv~k?Myc9>uNn#f;Y5IB zSPj%;D3GW$!I}XmYu$@281-c2&nxJ74k0j^%(Tt5wYANIgM(9}a=CnOfg6U}+1VL% z_ADyv*rPuh8g%h^Jn?yST%0aKU*Exu`E6kVz3^pjPK(Fqx4>~YoSQs*5KbJMo}Z7_ z1{8;wxR%AmOFa@v=bS5vnGhUo6k~y<42=qGUj-r%h)`61U0q$yHJ+H(teLc;SaGsD z_#`RlJ@*OGlA87jdc(vWWk9|So>TLYSJPSd-S0CHLqEF1N>F>TMRn*vBbN)^U0zYa zS8aAA8)#(Rwj<*;c)n;&nL?p=9gVmdMs7Xm;o&j3v}Cp6b$sLonxLb6cHsOr+1w;6 zZ^9X{o(bO)lkNDT&~`Q zDy?MR-N0)9n{|!oD^tGJM$v^AxUUSNmS8HS$52Eg#OENe*l<-%&BGTN;B+5Wy_e z_=wLTtb}yqMY%le~Lxb?gh?n$i)L@!n}$=Fu=-X~7$`9U8ln*80% z1e};}eK$&`Bh2_$8F+$e954Wt{y=MMhLx?pI4ClA-=u*nN%Fc0@;SK_W*ZBAjcQvv zV;K72S^Nzm{fzCd4m-*gOiGBZji|>rf--!WFy7uyJCjv*YKM(@1!;!{Lzb3(7nac{ z49L8U{Q_I87WHKjNnSEA-Y7|%00*|B*qGAcoM6TtaU7PdvyNVa@4P^ku?u}Tf|8N) z57dcx+_KGohM7X?)MEC4(wxysgsaqHQqcmob51&htTrj`ZaPXA>`8t6EjpsEeBC?I zG(&Q=lPl%4-RsldjZ`I8gH zumL33gU24-8jtSdsIKly@JlCEz`b@h?6fmu-!`upp<3UQT|6D|jMq6QeJoBU;0-@R zD>?rjLn!P(A+E%wEq37!IN?})xQXWl?dtQHgm>?t6TbO9=f}D87aM2&0uD;B^428( zxQ)buCvFC(eJihL1swVODWoK~joW3~chsw1vb+xH>8m2rjmfB(ZEgX4qMk3C;W3+wB z5bzyCtcgmS2?`Yr@xT#v1C(R#Zi{Rq0^PaL4{G}~EMh5QDmoLj=op)d4~?HZU)I|~ zJu#beE_~*hbqlZima-?lEiPdiIx1_mmt<$qw2jndB2oKj+gi&ptq=E zZ!-i&{n_rj-t~lTT%OQfD9dUOo+#1YTtx#&L5s?>{CWKOzZBhZ1>+V{wx;I9@5-M7 MxH#glASUqY-}#m`fpq=Pc;i7Ju#>t^)7-DF=ZbC8)u5GjCQ6Ok27e_ORQu3Gy#%M@{cxbRiO-hQ z!#*PR0u}`7sMOveXn;Tvs=qfj^d@*^9)om@HWa?idL3?IY4yJ_fKv1hVeJe@pQ@v@ z>fV7qMNRbU@CDY({(eUGv$fi-PwW6hg0lJ)(W7hQTz25c-Fx8M2Gj(QT(e%|i4jiw z_K{*^P=uxvs7)UL#aM7by*Ls$jW)SQ4*i=5Tj;$uvkC<+2l*6)Qt}cmeAbu16mz8+Isa zUc2_pHb3X+(W6c62Ee=*$@)fq{I<9wBR#xgXVw-4XO^Iia#eso=1B0L1+uM@~ zg!-P}2f7u=Sw`QC;*Gm0O(fGf6w1mt=l4o~MO{H^=0I)dq{@RD z4Q~KvtT2+nY`QD&dpCh@3ui4`hSwT^d>rKS`J?hY%i?g$j@y&ewMY_+Aki{GLCNjR zJMjx$8nuA)RMt&7!k0xuLtZI| zXD#_92u77wu&2?5#*rC2lqn3UR?na`blqqL&0B6*bv2p~3&qA5=dniCT-35aL}U@# zgb(m3?d6e+5uj;Ib7=K22}^ba^{riXS04pSB0($p(_Z2zZYSF=oJRW&`emoHX+6qZ zX`U4$g#*>qdM>jVQ_WT_@lW7aGMP+=Sw5-il}s0fmz23M4_H$?~{$ro^l5h1xjDf`4yvS)@`{9b% zF(SBu*1Vo}VqFJD#Eq6o{ zcLbp(YM0p{FQulY(iWr#X%6>es6$=XSvCZq$5kqeE8m_fVSZ>u2KbPgyhx`Sgp86q zGp&4XGOl9t{ri*ekzu?j$#|EXjc+y0ji(Ers^)C`w(-5;T9#;q8|*6;&$Bj}i3_yD z!9xUDL?qWYwq^F;e$yt9npn|t-L5hu=5^z{%H>X5>!kinc$;pWA~l2fBE-iS9gwS{ z)BWRKi1y`js|$ze_A{+InP%IMfGZ=31y$4x;QnX_Czrj*!OK)JsLR0Go4O 0 && label.length < 64; - }) - ); -}; - -U._validMx = function(email) { - var host = email.split('@').slice(1)[0]; - // try twice, just because DNS hiccups sometimes - // Note: we don't care if the domain exists, just that it *can* exist - return resolveMx(host).catch(function() { - return U._timeout(1000).then(function() { - return resolveMx(host); - }); - }); -}; - -// should be called after _validName -U._validDomain = function(str) { - // TODO use @root/dns (currently dns-suite) - // because node's dns can't read Authority records - return Promise.resolve(str); - /* - // try twice, just because DNS hiccups sometimes - // Note: we don't care if the domain exists, just that it *can* exist - return resolveSoa(str).catch(function() { - return U._timeout(1000).then(function() { - return resolveSoa(str); - }); - }); - */ -}; - -// foo.example.com and *.example.com overlap -// should be called after _validName -// (which enforces *. or no *) -U._uniqueNames = function(altnames) { - var dups = {}; - var wilds = {}; - if ( - altnames.some(function(w) { - if ('*.' !== w.slice(0, 2)) { - return; - } - if (wilds[w]) { - return true; - } - wilds[w] = true; - }) - ) { - return false; - } - - return altnames.every(function(name) { - var w; - if ('*.' !== name.slice(0, 2)) { - w = - '*.' + - name - .split('.') - .slice(1) - .join('.'); - } else { - return true; - } - - if (!dups[name] && !dups[w]) { - dups[name] = true; - return true; - } - }); -}; - -U._timeout = function(d) { - return new Promise(function(resolve) { - setTimeout(resolve, d); - }); -}; - -U._genKeypair = function(keyType) { - var keyopts; - var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10); - if (/RSA/.test(keyType)) { - keyopts = { - kty: 'RSA', - modulusLength: len || 2048 - }; - } else if (/^(EC|P\-?\d)/i.test(keyType)) { - keyopts = { - kty: 'EC', - namedCurve: 'P-' + (len || 256) - }; - } else { - // TODO put in ./errors.js - throw new Error('invalid key type: ' + keyType); - } - - return Keypairs.generate(keyopts).then(function(pair) { - return U._jwkToSet(pair.private); - }); -}; - -// TODO use ACME._importKeypair ?? -U._importKeypair = function(keypair) { - // this should import all formats equally well: - // 'object' (JWK), 'string' (private key pem), kp.privateKeyPem, kp.privateKeyJwk - if (keypair.private || keypair.d) { - return U._jwkToSet(keypair.private || keypair); - } - if (keypair.privateKeyJwk) { - return U._jwkToSet(keypair.privateKeyJwk); - } - - if ('string' !== typeof keypair && !keypair.privateKeyPem) { - // TODO put in errors - throw new Error('missing private key'); - } - - return Keypairs.import({ pem: keypair.privateKeyPem || keypair }).then( - function(priv) { - if (!priv.d) { - throw new Error('missing private key'); - } - return U._jwkToSet(priv); - } - ); -}; - -U._jwkToSet = function(jwk) { - var keypair = { - privateKeyJwk: jwk - }; - return Promise.all([ - Keypairs.export({ - jwk: jwk, - encoding: 'pem' - }).then(function(pem) { - keypair.privateKeyPem = pem; - }), - Keypairs.export({ - jwk: jwk, - encoding: 'pem', - public: true - }).then(function(pem) { - keypair.publicKeyPem = pem; - }), - Keypairs.publish({ - jwk: jwk - }).then(function(pub) { - keypair.publicKeyJwk = pub; - }) - ]).then(function() { - return keypair; - }); -}; - -U._attachCertInfo = function(results) { - var certInfo = certParser.info(results.cert); - - // subject, altnames, issuedAt, expiresAt - Object.keys(certInfo).forEach(function(key) { - results[key] = certInfo[key]; - }); - - return results; -}; - -U._certHasDomain = function(certInfo, _domain) { - var names = (certInfo.altnames || []).slice(0); - return names.some(function(name) { - var domain = _domain.toLowerCase(); - name = name.toLowerCase(); - if ('*.' === name.substr(0, 2)) { - name = name.substr(2); - domain = domain - .split('.') - .slice(1) - .join('.'); - } - return name === domain; - }); -}; - -// a bit heavy to be labeled 'utils'... perhaps 'common' would be better? -U._getOrCreateKeypair = function(db, subject, query, keyType, mustExist) { - var exists = false; - return db - .checkKeypair(query) - .then(function(kp) { - if (kp) { - exists = true; - return U._importKeypair(kp); - } - - if (mustExist) { - // TODO put in errors - throw new Error( - 'required keypair not found: ' + - (subject || '') + - ' ' + - JSON.stringify(query) - ); - } - - return U._genKeypair(keyType); - }) - .then(function(keypair) { - return { exists: exists, keypair: keypair }; - }); -}; - -U._getKeypair = function(db, subject, query) { - return U._getOrCreateKeypair(db, subject, query, '', true).then(function( - result - ) { - return result.keypair; - }); -};