From f2b6772f5c9f0a5d7210864e557433b464d58d69 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 13 Jun 2019 01:55:25 -0600 Subject: [PATCH] add .prettierrc, and make prettier --- .prettierrc | 8 + README.md | 118 +-- compat.js | 123 ++- examples/cli.js | 139 ++- examples/genkeypair.js | 24 +- examples/http-server.js | 8 +- examples/https-server.js | 21 +- node.js | 1958 +++++++++++++++++++++--------------- package-lock.json | 76 +- package.json | 60 +- tests/cb.js | 175 ++-- tests/compat.js | 150 +-- tests/fullchain-formats.js | 76 +- tests/promise.js | 185 ++-- 14 files changed, 1839 insertions(+), 1282 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..420e082 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 80, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": true +} diff --git a/README.md b/README.md index a242145..99e8924 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,21 @@ # [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) -A lightweight, **Low Dependency*** framework for building +A lightweight, **Low Dependency**\* framework for building Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). -* although `node-forge` and `ursa` are included as `optionalDependencies` +\* although `node-forge` and `ursa` are included as `optionalDependencies` for backwards compatibility with older versions of node, there are no other dependencies except those that I wrote for this (and related) projects. ## Looking for Quick 'n' Easy™? -If you're looking to *build a webserver*, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). -If you're looking for an *ACME-enabled webserver*, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). +If you're looking to _build a webserver_, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). +If you're looking for an _ACME-enabled webserver_, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). -* [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) -* [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) +- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) +- [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) ## How to build ACME clients @@ -77,20 +77,20 @@ https://acme-staging-v02.api.letsencrypt.org/directory ## Two API versions, Two Implementations -This library (acme-v2.js) supports ACME [*draft 11*](https://tools.ietf.org/html/draft-ietf-acme-acme-11), +This library (acme-v2.js) supports ACME [_draft 11_](https://tools.ietf.org/html/draft-ietf-acme-acme-11), otherwise known as Let's Encrypt v2 (or v02). - * ACME draft 11 - * Let's Encrypt v2 - * Let's Encrypt v02 +- ACME draft 11 +- Let's Encrypt v2 +- Let's Encrypt v02 The predecessor (le-acme-core) supports Let's Encrypt v1 (or v01), which was a [hodge-podge of various drafts](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) of the ACME spec early on. - * ACME early draft - * Let's Encrypt v1 - * Let's Encrypt v01 +- ACME early draft +- Let's Encrypt v1 +- Let's Encrypt v01 This library maintains compatibility with le-acme-core so that it can be used as a **drop-in replacement** and requires **no changes to existing code**, @@ -102,7 +102,7 @@ Status: Stable, Locked, Bugfix-only See Full Documentation at -``` +```js var RSA = require('rsa-compat').RSA; var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); @@ -118,7 +118,7 @@ Status: Almost stable, but **not semver locked** This API is a simple evolution of le-acme-core, but tries to provide a better mapping to the new draft 11 APIs. -``` +```js // Create Instance (Dependency Injection) var ACME = require('acme-v2').ACME.create({ RSA: require('rsa-compat').RSA @@ -187,54 +187,54 @@ Helpers & Stuff ```javascript // Constants -ACME.challengePrefixes['http-01'] // '/.well-known/acme-challenge' -ACME.challengePrefixes['dns-01'] // '_acme-challenge' +ACME.challengePrefixes['http-01']; // '/.well-known/acme-challenge' +ACME.challengePrefixes['dns-01']; // '_acme-challenge' ``` # Changelog -* v1.5 - * perform full test challenge first (even before nonce) -* v1.3 - * Use node RSA keygen by default - * No non-optional external deps! -* v1.2 - * fix some API out-of-specness - * doc some magic numbers (status) - * updated deps -* v1.1.0 - * reduce dependencies (use lightweight @coolaj86/request instead of request) -* v1.0.5 - cleanup logging -* v1.0.4 - v6- compat use `promisify` from node's util or bluebird -* v1.0.3 - documentation cleanup -* v1.0.2 - * use `options.contact` to provide raw contact array - * made `options.email` optional - * file cleanup -* v1.0.1 - * Compat API is ready for use - * Eliminate debug logging -* Apr 10, 2018 - tested backwards-compatibility using greenlock.js -* Apr 5, 2018 - export http and dns challenge tests -* Apr 5, 2018 - test http and dns challenges (success and failure) -* Apr 5, 2018 - test subdomains and its wildcard -* Apr 5, 2018 - test two subdomains -* Apr 5, 2018 - test wildcard -* Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) -* Mar 21, 2018 - *mostly* matches le-acme-core.js API -* Mar 21, 2018 - can now accept values (not hard coded) -* Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) -* Mar 20, 2018 - download certificate -* Mar 20, 2018 - poll for status -* Mar 20, 2018 - finalize order (submit csr) -* Mar 20, 2018 - generate domain keypair -* Mar 20, 2018 - respond to challenges -* Mar 16, 2018 - get challenges -* Mar 16, 2018 - new order -* Mar 15, 2018 - create account -* Mar 15, 2018 - generate account keypair -* Mar 15, 2018 - get nonce -* Mar 15, 2018 - get directory +- v1.5 + - perform full test challenge first (even before nonce) +- v1.3 + - Use node RSA keygen by default + - No non-optional external deps! +- v1.2 + - fix some API out-of-specness + - doc some magic numbers (status) + - updated deps +- v1.1.0 + - reduce dependencies (use lightweight @coolaj86/request instead of request) +- v1.0.5 - cleanup logging +- v1.0.4 - v6- compat use `promisify` from node's util or bluebird +- v1.0.3 - documentation cleanup +- v1.0.2 + - use `options.contact` to provide raw contact array + - made `options.email` optional + - file cleanup +- v1.0.1 + - Compat API is ready for use + - Eliminate debug logging +- Apr 10, 2018 - tested backwards-compatibility using greenlock.js +- Apr 5, 2018 - export http and dns challenge tests +- Apr 5, 2018 - test http and dns challenges (success and failure) +- Apr 5, 2018 - test subdomains and its wildcard +- Apr 5, 2018 - test two subdomains +- Apr 5, 2018 - test wildcard +- Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) +- Mar 21, 2018 - _mostly_ matches le-acme-core.js API +- Mar 21, 2018 - can now accept values (not hard coded) +- Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) +- Mar 20, 2018 - download certificate +- Mar 20, 2018 - poll for status +- Mar 20, 2018 - finalize order (submit csr) +- Mar 20, 2018 - generate domain keypair +- Mar 20, 2018 - respond to challenges +- Mar 16, 2018 - get challenges +- Mar 16, 2018 - new order +- Mar 15, 2018 - create account +- Mar 15, 2018 - generate account keypair +- Mar 15, 2018 - get nonce +- Mar 15, 2018 - get directory # Legal diff --git a/compat.js b/compat.js index 73f2961..9654475 100644 --- a/compat.js +++ b/compat.js @@ -8,74 +8,87 @@ var ACME2 = require('./').ACME; function resolveFn(cb) { - return function (val) { - // nextTick to get out of Promise chain - process.nextTick(function () { cb(null, val); }); - }; + return function(val) { + // nextTick to get out of Promise chain + process.nextTick(function() { + cb(null, val); + }); + }; } function rejectFn(cb) { - return function (err) { - console.error('[acme-v2] handled(?) rejection as errback:'); - console.error(err.stack); + return function(err) { + console.error('[acme-v2] handled(?) rejection as errback:'); + console.error(err.stack); - // nextTick to get out of Promise chain - process.nextTick(function () { cb(err); }); + // nextTick to get out of Promise chain + process.nextTick(function() { + cb(err); + }); - // do not resolve promise further - return new Promise(function () {}); - }; + // do not resolve promise further + return new Promise(function() {}); + }; } function create(deps) { - deps.LeCore = {}; - var acme2 = ACME2.create(deps); - acme2.registerNewAccount = function (options, cb) { - acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); - }; - acme2.getCertificate = function (options, cb) { - options.agreeToTerms = options.agreeToTerms || function (tos) { - return Promise.resolve(tos); - }; - acme2.certificates.create(options).then(function (certs) { - var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); - certs.privkey = privkeyPem; - resolveFn(cb)(certs); - }, rejectFn(cb)); - }; - acme2.getAcmeUrls = function (options, cb) { - acme2.init(options).then(resolveFn(cb), rejectFn(cb)); - }; - acme2.getOptions = function () { - var defs = {}; + deps.LeCore = {}; + var acme2 = ACME2.create(deps); + acme2.registerNewAccount = function(options, cb) { + acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getCertificate = function(options, cb) { + options.agreeToTerms = + options.agreeToTerms || + function(tos) { + return Promise.resolve(tos); + }; + acme2.certificates.create(options).then(function(certs) { + var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); + certs.privkey = privkeyPem; + resolveFn(cb)(certs); + }, rejectFn(cb)); + }; + acme2.getAcmeUrls = function(options, cb) { + acme2.init(options).then(resolveFn(cb), rejectFn(cb)); + }; + acme2.getOptions = function() { + var defs = {}; - Object.keys(module.exports.defaults).forEach(function (key) { - defs[key] = defs[deps] || module.exports.defaults[key]; - }); + Object.keys(module.exports.defaults).forEach(function(key) { + defs[key] = defs[deps] || module.exports.defaults[key]; + }); - return defs; - }; - acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; - acme2.productionServerUrl = module.exports.defaults.productionServerUrl; - acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; - return acme2; + return defs; + }; + acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; + acme2.productionServerUrl = module.exports.defaults.productionServerUrl; + acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; + return acme2; } -module.exports.ACME = { }; +module.exports.ACME = {}; module.exports.defaults = { - productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' -, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' -, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] -, challengeTypes: [ 'http-01', 'dns-01' ] -, challengeType: 'http-01' -//, keyType: 'rsa' // ecdsa -//, keySize: 2048 // 256 -, rsaKeySize: 2048 // 256 -, acmeChallengePrefix: '/.well-known/acme-challenge/' + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory', + stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory', + knownEndpoints: [ + 'keyChange', + 'meta', + 'newAccount', + 'newNonce', + 'newOrder', + 'revokeCert' + ], + challengeTypes: ['http-01', 'dns-01'], + challengeType: 'http-01', + //, keyType: 'rsa' // ecdsa + //, keySize: 2048 // 256 + rsaKeySize: 2048, // 256 + acmeChallengePrefix: '/.well-known/acme-challenge/' }; -Object.keys(module.exports.defaults).forEach(function (key) { - module.exports.ACME[key] = module.exports.defaults[key]; +Object.keys(module.exports.defaults).forEach(function(key) { + module.exports.ACME[key] = module.exports.defaults[key]; }); -Object.keys(ACME2).forEach(function (key) { - module.exports.ACME[key] = ACME2[key]; +Object.keys(ACME2).forEach(function(key) { + module.exports.ACME[key] = ACME2[key]; }); module.exports.ACME.create = create; diff --git a/examples/cli.js b/examples/cli.js index 21ff620..e453439 100644 --- a/examples/cli.js +++ b/examples/cli.js @@ -7,66 +7,125 @@ var RSA = require('rsa-compat').RSA; var readline = require('readline'); var rl = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, + output: process.stdout }); require('./genkeypair.js'); function getWeb() { - rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { - web = (web||'').trim().split(/,/g); - if (!web[0]) { getWeb(); return; } + rl.question( + 'What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', + function(web) { + web = (web || '').trim().split(/,/g); + if (!web[0]) { + getWeb(); + return; + } - if (web.some(function (w) { return '*' === w[0]; })) { - console.log('Wildcard domains must use dns-01'); - getEmail(web, 'dns-01'); - } else { - getChallengeType(web); - } - }); + if ( + web.some(function(w) { + return '*' === w[0]; + }) + ) { + console.log('Wildcard domains must use dns-01'); + getEmail(web, 'dns-01'); + } else { + getChallengeType(web); + } + } + ); } function getChallengeType(web) { - rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { - chType = (chType||'').trim(); - if (!chType) { chType = 'http-01'; } + rl.question( + 'What challenge will you be testing today? http-01 or dns-01? [http-01] ', + function(chType) { + chType = (chType || '').trim(); + if (!chType) { + chType = 'http-01'; + } - getEmail(web, chType); - }); + getEmail(web, chType); + } + ); } function getEmail(web, chType) { - rl.question('What email should we use? (optional) ', function (email) { - email = (email||'').trim(); - if (!email) { email = null; } + rl.question('What email should we use? (optional) ', function(email) { + email = (email || '').trim(); + if (!email) { + email = null; + } - getApiStyle(web, chType, email); - }); + getApiStyle(web, chType, email); + }); } function getApiStyle(web, chType, email) { - var defaultStyle = 'compat'; - rl.question('What API style would you like to test? v1-compat or promise? [v1-compat] ', function (apiStyle) { - apiStyle = (apiStyle||'').trim(); - if (!apiStyle) { apiStyle = 'v1-compat'; } + var defaultStyle = 'compat'; + rl.question( + 'What API style would you like to test? v1-compat or promise? [v1-compat] ', + function(apiStyle) { + apiStyle = (apiStyle || '').trim(); + if (!apiStyle) { + apiStyle = 'v1-compat'; + } - rl.close(); + rl.close(); - var RSA = require('rsa-compat').RSA; - var accountKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/account.privkey.pem') }); - var domainKeypair = RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/../tests/privkey.pem') }); - var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; + var RSA = require('rsa-compat').RSA; + var accountKeypair = RSA.import({ + privateKeyPem: require('fs').readFileSync( + __dirname + '/../tests/account.privkey.pem' + ) + }); + var domainKeypair = RSA.import({ + privateKeyPem: require('fs').readFileSync( + __dirname + '/../tests/privkey.pem' + ) + }); + var directoryUrl = + 'https://acme-staging-v02.api.letsencrypt.org/directory'; - if ('promise' === apiStyle) { - require('../tests/promise.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); - } else if ('cb' === apiStyle) { - require('../tests/cb.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); - } else { - if ('v1-compat' !== apiStyle) { console.warn("Didn't understand '" + apiStyle + "', using 'v1-compat' instead..."); } - require('../tests/compat.js').run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair); - } - }); + if ('promise' === apiStyle) { + require('../tests/promise.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } else if ('cb' === apiStyle) { + require('../tests/cb.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } else { + if ('v1-compat' !== apiStyle) { + console.warn( + "Didn't understand '" + apiStyle + "', using 'v1-compat' instead..." + ); + } + require('../tests/compat.js').run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair + ); + } + } + ); } getWeb(); diff --git a/examples/genkeypair.js b/examples/genkeypair.js index 4a5bad6..c40e187 100644 --- a/examples/genkeypair.js +++ b/examples/genkeypair.js @@ -6,21 +6,21 @@ var RSA = require('rsa-compat').RSA; var fs = require('fs'); if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { - RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); + RSA.generateKeypair(2048, 65537, {}, function(err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair); + console.log(privkeyPem); - fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); - }); + fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); + }); } if (!fs.existsSync(__dirname + '/../tests/privkey.pem')) { - RSA.generateKeypair(2048, 65537, {}, function (err, keypair) { - console.log(keypair); - var privkeyPem = RSA.exportPrivatePem(keypair) - console.log(privkeyPem); + RSA.generateKeypair(2048, 65537, {}, function(err, keypair) { + console.log(keypair); + var privkeyPem = RSA.exportPrivatePem(keypair); + console.log(privkeyPem); - fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); - }); + fs.writeFileSync(__dirname + '/../tests/privkey.pem', privkeyPem); + }); } diff --git a/examples/http-server.js b/examples/http-server.js index f923472..26b6cab 100644 --- a/examples/http-server.js +++ b/examples/http-server.js @@ -6,6 +6,8 @@ var http = require('http'); var express = require('express'); -var server = http.createServer(express.static('../tests')).listen(80, function () { - console.log('Listening on', this.address()); -}); +var server = http + .createServer(express.static('../tests')) + .listen(80, function() { + console.log('Listening on', this.address()); + }); diff --git a/examples/https-server.js b/examples/https-server.js index 5520041..4369ebb 100644 --- a/examples/https-server.js +++ b/examples/https-server.js @@ -5,11 +5,16 @@ 'use strict'; var https = require('https'); -var server = https.createServer({ - key: require('fs').readFileSync('../tests/privkey.pem') -, cert: require('fs').readFileSync('../tests/fullchain.pem') -}, function (req, res) { - res.end("Hello, World!"); -}).listen(443, function () { - console.log('Listening on', this.address()); -}); +var server = https + .createServer( + { + key: require('fs').readFileSync('../tests/privkey.pem'), + cert: require('fs').readFileSync('../tests/fullchain.pem') + }, + function(req, res) { + res.end('Hello, World!'); + } + ) + .listen(443, function() { + console.log('Listening on', this.address()); + }); diff --git a/node.js b/node.js index 8582269..67eb88d 100644 --- a/node.js +++ b/node.js @@ -5,99 +5,141 @@ 'use strict'; /* globals Promise */ -var ACME = module.exports.ACME = {}; +var ACME = (module.exports.ACME = {}); ACME.formatPemChain = function formatPemChain(str) { - return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; + return ( + str + .trim() + .replace(/[\r\n]+/g, '\n') + .replace(/\-\n\-/g, '-\n\n-') + '\n' + ); }; ACME.splitPemChain = function splitPemChain(str) { - return str.trim().split(/[\r\n]{2,}/g).map(function (str) { - return str + '\n'; - }); + return str + .trim() + .split(/[\r\n]{2,}/g) + .map(function(str) { + return str + '\n'; + }); }; - // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" ACME.challengePrefixes = { - 'http-01': '/.well-known/acme-challenge' -, 'dns-01': '_acme-challenge' + 'http-01': '/.well-known/acme-challenge', + 'dns-01': '_acme-challenge' }; ACME.challengeTests = { - 'http-01': function (me, auth) { - var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; - return me._request({ url: url }).then(function (resp) { - var err; - - // TODO limit the number of bytes that are allowed to be downloaded - if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { - return true; - } - - err = new Error( - "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" - + "curl '" + url + "'\n" - + "Expected: '" + auth.keyAuthorization + "'\n" - + "Got: '" + resp.body + "'\n" - + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - return Promise.reject(err); - }); - } -, 'dns-01': function (me, auth) { - // remove leading *. on wildcard domains - return me._dig({ - type: 'TXT' - , name: auth.dnsHost - }).then(function (ans) { - var err; - - if (ans.answer.some(function (txt) { - return auth.dnsAuthorization === txt.data[0]; - })) { - return true; - } - - err = new Error( - "Error: Failed DNS-01 Pre-Flight Dry Run.\n" - + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" - + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - return Promise.reject(err); - }); - } + 'http-01': function(me, auth) { + var url = + 'http://' + + auth.hostname + + ACME.challengePrefixes['http-01'] + + '/' + + auth.token; + return me._request({ url: url }).then(function(resp) { + var err; + + // TODO limit the number of bytes that are allowed to be downloaded + if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { + return true; + } + + err = new Error( + 'Error: Failed HTTP-01 Pre-Flight / Dry Run.\n' + + "curl '" + + url + + "'\n" + + "Expected: '" + + auth.keyAuthorization + + "'\n" + + "Got: '" + + resp.body + + "'\n" + + 'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4' + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + }, + 'dns-01': function(me, auth) { + // remove leading *. on wildcard domains + return me + ._dig({ + type: 'TXT', + name: auth.dnsHost + }) + .then(function(ans) { + var err; + + if ( + ans.answer.some(function(txt) { + return auth.dnsAuthorization === txt.data[0]; + }) + ) { + return true; + } + + err = new Error( + 'Error: Failed DNS-01 Pre-Flight Dry Run.\n' + + "dig TXT '" + + auth.dnsHost + + "' does not return '" + + auth.dnsAuthorization + + "'\n" + + 'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4' + ); + err.code = 'E_FAIL_DRY_CHALLENGE'; + return Promise.reject(err); + }); + } }; -ACME._getUserAgentString = function (deps) { - var uaDefaults = { - pkg: "Greenlock/" + deps.pkg.version - , os: "(" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" - , node: "Node.js/" + deps.process.version - , user: '' - }; - - var userAgent = []; - - //Object.keys(currentUAProps) - Object.keys(uaDefaults).forEach(function (key) { - if (uaDefaults[key]) { - userAgent.push(uaDefaults[key]); - } - }); - - return userAgent.join(' ').trim(); +ACME._getUserAgentString = function(deps) { + var uaDefaults = { + pkg: 'Greenlock/' + deps.pkg.version, + os: + '(' + + deps.os.type() + + '; ' + + deps.process.arch + + ' ' + + deps.os.platform() + + ' ' + + deps.os.release() + + ')', + node: 'Node.js/' + deps.process.version, + user: '' + }; + + var userAgent = []; + + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function(key) { + if (uaDefaults[key]) { + userAgent.push(uaDefaults[key]); + } + }); + + return userAgent.join(' ').trim(); }; -ACME._directory = function (me) { - return me._request({ url: me.directoryUrl, json: true }); +ACME._directory = function(me) { + return me._request({ url: me.directoryUrl, json: true }); }; -ACME._getNonce = function (me) { - if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } - return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - return me._nonce; - }); +ACME._getNonce = function(me) { + if (me._nonce) { + return new Promise(function(resolve) { + resolve(me._nonce); + return; + }); + } + return me + ._request({ method: 'HEAD', url: me._directoryUrls.newNonce }) + .then(function(resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); }; // ACME RFC Section 7.3 Account Creation /* @@ -119,114 +161,141 @@ ACME._getNonce = function (me) { "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" } */ -ACME._registerAccount = function (me, options) { - if (me.debug) { console.debug('[acme-v2] accounts.create'); } - - return ACME._getNonce(me).then(function () { - return new Promise(function (resolve, reject) { - - function agree(tosUrl) { - var err; - if (me._tos !== tosUrl) { - err = new Error("You must agree to the ToS at '" + me._tos + "'"); - err.code = "E_AGREE_TOS"; - reject(err); - return; - } - - var jwk = me.RSA.exportPublicJwk(options.accountKeypair); - var contact; - if (options.contact) { - contact = options.contact.slice(0); - } else if (options.email) { - contact = [ 'mailto:' + options.email ]; - } - var body = { - termsOfServiceAgreed: tosUrl === me._tos - , onlyReturnExisting: false - , contact: contact - }; - if (options.externalAccount) { - // TODO is this really done by HMAC or is it arbitrary? - body.externalAccountBinding = me.RSA.signJws( - options.externalAccount.secret - , undefined - , { alg: "HS256" - , kid: options.externalAccount.id - , url: me._directoryUrls.newAccount - } - , Buffer.from(JSON.stringify(jwk)) - ); - } - var payload = JSON.stringify(body); - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce - , alg: (me._alg || 'RS256') - , url: me._directoryUrls.newAccount - , jwk: jwk - } - , Buffer.from(payload) - ); - - delete jws.header; - if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } - if (me.debug) { console.debug(jws); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._directoryUrls.newAccount - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - var account = resp.body; - - if (2 !== Math.floor(resp.statusCode / 100)) { - throw new Error('account error: ' + JSON.stringify(body)); - } - - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - // the account id url - me._kid = location; - if (me.debug) { console.debug('[DEBUG] new account location:'); } - if (me.debug) { console.debug(location); } - if (me.debug) { console.debug(resp.toJSON()); } - - /* +ACME._registerAccount = function(me, options) { + if (me.debug) { + console.debug('[acme-v2] accounts.create'); + } + + return ACME._getNonce(me).then(function() { + return new Promise(function(resolve, reject) { + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = 'E_AGREE_TOS'; + reject(err); + return; + } + + var jwk = me.RSA.exportPublicJwk(options.accountKeypair); + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = ['mailto:' + options.email]; + } + var body = { + termsOfServiceAgreed: tosUrl === me._tos, + onlyReturnExisting: false, + contact: contact + }; + if (options.externalAccount) { + // TODO is this really done by HMAC or is it arbitrary? + body.externalAccountBinding = me.RSA.signJws( + options.externalAccount.secret, + undefined, + { + alg: 'HS256', + kid: options.externalAccount.id, + url: me._directoryUrls.newAccount + }, + Buffer.from(JSON.stringify(jwk)) + ); + } + var payload = JSON.stringify(body); + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: me._directoryUrls.newAccount, + jwk: jwk + }, + Buffer.from(payload) + ); + + delete jws.header; + if (me.debug) { + console.debug('[acme-v2] accounts.create JSON body:'); + } + if (me.debug) { + console.debug(jws); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._directoryUrls.newAccount, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + var account = resp.body; + + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error('account error: ' + JSON.stringify(body)); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + // the account id url + me._kid = location; + if (me.debug) { + console.debug('[DEBUG] new account location:'); + } + if (me.debug) { + console.debug(location); + } + if (me.debug) { + console.debug(resp.toJSON()); + } + + /* { contact: ["mailto:jon@example.com"], orders: "https://some-url", status: 'valid' } */ - if (!account) { account = { _emptyResponse: true, key: {} }; } - // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 - if (!account.key) { account.key = {}; } - account.key.kid = me._kid; - return account; - }).then(resolve, reject); - } - - if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } - if (1 === options.agreeToTerms.length) { - // newer promise API - return options.agreeToTerms(me._tos).then(agree, reject); - } - else if (2 === options.agreeToTerms.length) { - // backwards compat cb API - return options.agreeToTerms(me._tos, function (err, tosUrl) { - if (!err) { agree(tosUrl); return; } - reject(err); - }); - } - else { - reject(new Error('agreeToTerms has incorrect function signature.' - + ' Should be fn(tos) { return Promise; }')); - } - }); - }); + if (!account) { + account = { _emptyResponse: true, key: {} }; + } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { + account.key = {}; + } + account.key.kid = me._kid; + return account; + }) + .then(resolve, reject); + } + + if (me.debug) { + console.debug('[acme-v2] agreeToTerms'); + } + if (1 === options.agreeToTerms.length) { + // newer promise API + return options.agreeToTerms(me._tos).then(agree, reject); + } else if (2 === options.agreeToTerms.length) { + // backwards compat cb API + return options.agreeToTerms(me._tos, function(err, tosUrl) { + if (!err) { + agree(tosUrl); + return; + } + reject(err); + }); + } else { + reject( + new Error( + 'agreeToTerms has incorrect function signature.' + + ' Should be fn(tos) { return Promise; }' + ) + ); + } + }); + }); }; /* POST /acme/new-order HTTP/1.1 @@ -248,173 +317,222 @@ ACME._registerAccount = function (me, options) { "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" } */ -ACME._getChallenges = function (me, options, auth) { - if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } - return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { - return resp.body; - }); +ACME._getChallenges = function(me, options, auth) { + if (me.debug) { + console.debug('\n[DEBUG] getChallenges\n'); + } + return me + ._request({ method: 'GET', url: auth, json: true }) + .then(function(resp) { + return resp.body; + }); }; ACME._wait = function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); + return new Promise(function(resolve) { + setTimeout(resolve, ms || 1100); + }); }; -ACME._testChallengeOptions = function () { - var chToken = require('crypto').randomBytes(16).toString('hex'); - return [ - { - "type": "http-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/0", - "token": "test-" + chToken + "-0" - } - , { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/1", - "token": "test-" + chToken + "-1", - "_wildcard": true - } - , { - "type": "tls-sni-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/2", - "token": "test-" + chToken + "-2" - } - , { - "type": "tls-alpn-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/3", - "token": "test-" + chToken + "-3" - } - ]; +ACME._testChallengeOptions = function() { + var chToken = require('crypto') + .randomBytes(16) + .toString('hex'); + return [ + { + type: 'http-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/0', + token: 'test-' + chToken + '-0' + }, + { + type: 'dns-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/1', + token: 'test-' + chToken + '-1', + _wildcard: true + }, + { + type: 'tls-sni-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/2', + token: 'test-' + chToken + '-2' + }, + { + type: 'tls-alpn-01', + status: 'pending', + url: 'https://acme-staging-v02.example.com/3', + token: 'test-' + chToken + '-3' + } + ]; }; -ACME._testChallenges = function (me, options) { - if (me.skipChallengeTest) { - return Promise.resolve(); - } - - var CHECK_DELAY = 0; - return Promise.all(options.domains.map(function (identifierValue) { - // TODO we really only need one to pass, not all to pass - var challenges = ACME._testChallengeOptions(); - if (identifierValue.includes("*")) { - challenges = challenges.filter(function (ch) { return ch._wildcard; }); - } - - var challenge = ACME._chooseChallenge(options, { challenges: challenges }); - if (!challenge) { - // For example, wildcards require dns-01 and, if we don't have that, we have to bail - var enabled = options.challengeTypes.join(', ') || 'none'; - var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; - return Promise.reject(new Error( - "None of the challenge types that you've enabled ( " + enabled + " )" - + " are suitable for validating the domain you've selected (" + identifierValue + ")." - + " You must enable one of ( " + suitable + " )." - )); - } - if ('dns-01' === challenge.type) { - // Give the nameservers a moment to propagate - CHECK_DELAY = 1.5 * 1000; - } - - return Promise.resolve().then(function () { - var results = { - identifier: { - type: "dns" - , value: identifierValue.replace(/^\*\./, '') - } - , challenges: [ challenge ] - , expires: new Date(Date.now() + (60 * 1000)).toISOString() - , wildcard: identifierValue.includes('*.') || undefined - }; - var dryrun = true; - var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); - return ACME._setChallenge(me, options, auth).then(function () { - return auth; - }); - }); - })).then(function (auths) { - return ACME._wait(CHECK_DELAY).then(function () { - return Promise.all(auths.map(function (auth) { - return ACME.challengeTests[auth.type](me, auth); - })); - }); - }); +ACME._testChallenges = function(me, options) { + if (me.skipChallengeTest) { + return Promise.resolve(); + } + + var CHECK_DELAY = 0; + return Promise.all( + options.domains.map(function(identifierValue) { + // TODO we really only need one to pass, not all to pass + var challenges = ACME._testChallengeOptions(); + if (identifierValue.includes('*')) { + challenges = challenges.filter(function(ch) { + return ch._wildcard; + }); + } + + var challenge = ACME._chooseChallenge(options, { + challenges: challenges + }); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + var enabled = options.challengeTypes.join(', ') || 'none'; + var suitable = + challenges + .map(function(r) { + return r.type; + }) + .join(', ') || 'none'; + return Promise.reject( + new Error( + "None of the challenge types that you've enabled ( " + + enabled + + ' )' + + " are suitable for validating the domain you've selected (" + + identifierValue + + ').' + + ' You must enable one of ( ' + + suitable + + ' ).' + ) + ); + } + if ('dns-01' === challenge.type) { + // Give the nameservers a moment to propagate + CHECK_DELAY = 1.5 * 1000; + } + + return Promise.resolve().then(function() { + var results = { + identifier: { + type: 'dns', + value: identifierValue.replace(/^\*\./, '') + }, + challenges: [challenge], + expires: new Date(Date.now() + 60 * 1000).toISOString(), + wildcard: identifierValue.includes('*.') || undefined + }; + var dryrun = true; + var auth = ACME._challengeToAuth( + me, + options, + results, + challenge, + dryrun + ); + return ACME._setChallenge(me, options, auth).then(function() { + return auth; + }); + }); + }) + ).then(function(auths) { + return ACME._wait(CHECK_DELAY).then(function() { + return Promise.all( + auths.map(function(auth) { + return ACME.challengeTests[auth.type](me, auth); + }) + ); + }); + }); }; ACME._chooseChallenge = function(options, results) { - // For each of the challenge types that we support - var challenge; - options.challengeTypes.some(function (chType) { - // And for each of the challenge types that are allowed - return results.challenges.some(function (ch) { - // Check to see if there are any matches - if (ch.type === chType) { - challenge = ch; - return true; - } - }); - }); - - return challenge; + // For each of the challenge types that we support + var challenge; + options.challengeTypes.some(function(chType) { + // And for each of the challenge types that are allowed + return results.challenges.some(function(ch) { + // Check to see if there are any matches + if (ch.type === chType) { + challenge = ch; + return true; + } + }); + }); + + return challenge; }; -ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { - // we don't poison the dns cache with our dummy request - var dnsPrefix = ACME.challengePrefixes['dns-01']; - if (dryrun) { - dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + Math.random().toString().slice(2,6)); - } - - var auth = {}; - - // straight copy from the new order response - // { identifier, status, expires, challenges, wildcard } - Object.keys(request).forEach(function (key) { - auth[key] = request[key]; - }); - - // copy from the challenge we've chosen - // { type, status, url, token } - // (note the duplicate status overwrites the one above, but they should be the same) - Object.keys(challenge).forEach(function (key) { - // don't confused devs with the id url - auth[key] = challenge[key]; - }); - - // batteries-included helpers - auth.hostname = auth.identifier.value; - // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases - auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); - auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; - // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead - auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; - auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); - auth.dnsAuthorization = ACME._toWebsafeBase64( - require('crypto').createHash('sha256').update(auth.keyAuthorization).digest('base64') - ); - - return auth; +ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes['dns-01']; + if (dryrun) { + dnsPrefix = dnsPrefix.replace( + 'acme-challenge', + 'greenlock-dryrun-' + + Math.random() + .toString() + .slice(2, 6) + ); + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function(key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function(key) { + // don't confused devs with the id url + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); + auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; + // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead + auth.challengeUrl = + 'http://' + + auth.identifier.value + + ACME.challengePrefixes['http-01'] + + '/' + + auth.token; + auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); + auth.dnsAuthorization = ACME._toWebsafeBase64( + require('crypto') + .createHash('sha256') + .update(auth.keyAuthorization) + .digest('base64') + ); + + return auth; }; -ACME._untame = function (name, wild) { - if (wild) { name = '*.' + name.replace('*.', ''); } - return name; +ACME._untame = function(name, wild) { + if (wild) { + name = '*.' + name.replace('*.', ''); + } + return name; }; // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 -ACME._postChallenge = function (me, options, auth) { - var RETRY_INTERVAL = me.retryInterval || 1000; - var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; - var MAX_POLL = me.retryPoll || 8; - var MAX_PEND = me.retryPending || 4; - var count = 0; +ACME._postChallenge = function(me, options, auth) { + var RETRY_INTERVAL = me.retryInterval || 1000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; + var count = 0; - var altname = ACME._untame(auth.identifier.value, auth.wildcard); + var altname = ACME._untame(auth.identifier.value, auth.wildcard); - /* + /* POST /acme/authz/1234 HTTP/1.1 Host: example.com Content-Type: application/jose+json @@ -432,480 +550,720 @@ ACME._postChallenge = function (me, options, auth) { "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" } */ - function deactivate() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } - , Buffer.from(JSON.stringify({ "status": "deactivated" })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: auth.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } - - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('deactivate challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(DEAUTH_INTERVAL); - }); - } - - function pollStatus() { - if (count >= MAX_POLL) { - return Promise.reject(new Error( - "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" - )); - } - - count += 1; - - if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } - return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { - if ('processing' === resp.body.status) { - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - } - - // This state should never occur - if ('pending' === resp.body.status) { - if (count >= MAX_PEND) { - return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); - } - if (me.debug) { console.debug('poll: again'); } - return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); - } - - if ('valid' === resp.body.status) { - if (me.debug) { console.debug('poll: valid'); } - - try { - if (1 === options.removeChallenge.length) { - options.removeChallenge(auth).then(function () {}, function () {}); - } else if (2 === options.removeChallenge.length) { - options.removeChallenge(auth, function (err) { return err; }); - } else { - if (!ACME._removeChallengeWarn) { - console.warn("Please update to acme-v2 removeChallenge(options) or removeChallenge(options, cb)."); - console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); - ACME._removeChallengeWarn = true; - } - options.removeChallenge(auth.request.identifier, auth.token, function () {}); - } - } catch(e) {} - return resp.body; - } - - var errmsg; - if (!resp.body.status) { - errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; - } - else if ('invalid' === resp.body.status) { - errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; - } - else { - errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; - } - - return Promise.reject(new Error(errmsg)); - }); - } - - function respondToChallenge() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } - , Buffer.from(JSON.stringify({ })) - ); - me._nonce = null; - return me._request({ - method: 'POST' - , url: auth.url - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } - if (me.debug) { console.debug(resp.headers); } - if (me.debug) { console.debug(resp.body); } - if (me.debug) { console.debug(); } - - me._nonce = resp.toJSON().headers['replay-nonce']; - if (me.debug) { console.debug('respond to challenge: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - }); - } - - return respondToChallenge(); + function deactivate() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: auth.url, + kid: me._kid + }, + Buffer.from(JSON.stringify({ status: 'deactivated' })) + ); + me._nonce = null; + return me + ._request({ + method: 'POST', + url: auth.url, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + if (me.debug) { + console.debug('[acme-v2.js] deactivate:'); + } + if (me.debug) { + console.debug(resp.headers); + } + if (me.debug) { + console.debug(resp.body); + } + if (me.debug) { + console.debug(); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { + console.debug('deactivate challenge: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + return Promise.reject( + new Error( + "[acme-v2] stuck in bad pending/processing state for '" + + altname + + "'" + ) + ); + } + + count += 1; + + if (me.debug) { + console.debug('\n[DEBUG] statusChallenge\n'); + } + return me + ._request({ method: 'GET', url: auth.url, json: true }) + .then(function(resp) { + if ('processing' === resp.body.status) { + if (me.debug) { + console.debug('poll: again'); + } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + } + + // This state should never occur + if ('pending' === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL) + .then(deactivate) + .then(respondToChallenge); + } + if (me.debug) { + console.debug('poll: again'); + } + return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); + } + + if ('valid' === resp.body.status) { + if (me.debug) { + console.debug('poll: valid'); + } + + try { + if (1 === options.removeChallenge.length) { + options.removeChallenge(auth).then(function() {}, function() {}); + } else if (2 === options.removeChallenge.length) { + options.removeChallenge(auth, function(err) { + return err; + }); + } else { + if (!ACME._removeChallengeWarn) { + console.warn( + 'Please update to acme-v2 removeChallenge(options) or removeChallenge(options, cb).' + ); + console.warn( + "The API has been changed for compatibility with all ACME / Let's Encrypt challenge types." + ); + ACME._removeChallengeWarn = true; + } + options.removeChallenge( + auth.request.identifier, + auth.token, + function() {} + ); + } + } catch (e) {} + return resp.body; + } + + var errmsg; + if (!resp.body.status) { + errmsg = + "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + + altname + + "':"; + } else if ('invalid' === resp.body.status) { + errmsg = + "[acme-v2] (E_STATE_INVALID) challenge state for '" + + altname + + "': '" + + resp.body.status + + "'"; + } else { + errmsg = + "[acme-v2] (E_STATE_UKN) challenge state for '" + + altname + + "': '" + + resp.body.status + + "'"; + } + + return Promise.reject(new Error(errmsg)); + }); + } + + function respondToChallenge() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: auth.url, + kid: me._kid + }, + Buffer.from(JSON.stringify({})) + ); + me._nonce = null; + return me + ._request({ + method: 'POST', + url: auth.url, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + if (me.debug) { + console.debug('[acme-v2.js] challenge accepted!'); + } + if (me.debug) { + console.debug(resp.headers); + } + if (me.debug) { + console.debug(resp.body); + } + if (me.debug) { + console.debug(); + } + + me._nonce = resp.toJSON().headers['replay-nonce']; + if (me.debug) { + console.debug('respond to challenge: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + }); + } + + return respondToChallenge(); }; -ACME._setChallenge = function (me, options, auth) { - return new Promise(function (resolve, reject) { - try { - if (1 === options.setChallenge.length) { - options.setChallenge(auth).then(resolve).catch(reject); - } else if (2 === options.setChallenge.length) { - options.setChallenge(auth, function (err) { - if(err) { reject(err); } else { resolve(); } - }); - } else { - var challengeCb = function(err) { - if(err) { reject(err); } else { resolve(); } - }; - // for backwards compat adding extra keys without changing params length - Object.keys(auth).forEach(function (key) { - challengeCb[key] = auth[key]; - }); - if (!ACME._setChallengeWarn) { - console.warn("Please update to acme-v2 setChallenge(options) or setChallenge(options, cb)."); - console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); - ACME._setChallengeWarn = true; - } - options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); - } - } catch(e) { - reject(e); - } - }).then(function () { - // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? - var DELAY = me.setChallengeWait || 500; - if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); } - return ACME._wait(DELAY); - }); +ACME._setChallenge = function(me, options, auth) { + return new Promise(function(resolve, reject) { + try { + if (1 === options.setChallenge.length) { + options + .setChallenge(auth) + .then(resolve) + .catch(reject); + } else if (2 === options.setChallenge.length) { + options.setChallenge(auth, function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + var challengeCb = function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }; + // for backwards compat adding extra keys without changing params length + Object.keys(auth).forEach(function(key) { + challengeCb[key] = auth[key]; + }); + if (!ACME._setChallengeWarn) { + console.warn( + 'Please update to acme-v2 setChallenge(options) or setChallenge(options, cb).' + ); + console.warn( + "The API has been changed for compatibility with all ACME / Let's Encrypt challenge types." + ); + ACME._setChallengeWarn = true; + } + options.setChallenge( + auth.identifier.value, + auth.token, + auth.keyAuthorization, + challengeCb + ); + } + } catch (e) { + reject(e); + } + }).then(function() { + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = me.setChallengeWait || 500; + if (me.debug) { + console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); + } + return ACME._wait(DELAY); + }); }; -ACME._finalizeOrder = function (me, options, validatedDomains) { - if (me.debug) { console.debug('finalizeOrder:'); } - var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); - var body = { csr: csr }; - var payload = JSON.stringify(body); - - function pollCert() { - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } - , Buffer.from(payload) - ); - - if (me.debug) { console.debug('finalize:', me._finalize); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._finalize - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 - // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" - me._nonce = resp.toJSON().headers['replay-nonce']; - - if (me.debug) { console.debug('order finalized: resp.body:'); } - if (me.debug) { console.debug(resp.body); } - - if ('valid' === resp.body.status) { - me._expires = resp.body.expires; - me._certificate = resp.body.certificate; - - return resp.body; // return order - } - - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - - if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } - - if ('pending' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'pending'." - + " Best guess: You have not accepted at least one challenge for each domain:\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) - )); - } - - if ('invalid' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'invalid'." - + " Best guess: One or more of the domain challenges could not be verified" - + " (or the order was canceled).\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) - )); - } - - if ('ready' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'ready'." - + " Hmmm... this state shouldn't be possible here. That was the last state." - + " This one should at least be 'processing'.\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) + "\n\n" - + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" - )); - } - - return Promise.reject(new Error( - "Didn't finalize order: Unhandled status '" + resp.body.status + "'." - + " This is not one of the known statuses...\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) + "\n\n" - + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" - )); - }); - } - - return pollCert(); +ACME._finalizeOrder = function(me, options, validatedDomains) { + if (me.debug) { + console.debug('finalizeOrder:'); + } + var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg || 'RS256', + url: me._finalize, + kid: me._kid + }, + Buffer.from(payload) + ); + + if (me.debug) { + console.debug('finalize:', me._finalize); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._finalize, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" + me._nonce = resp.toJSON().headers['replay-nonce']; + + if (me.debug) { + console.debug('order finalized: resp.body:'); + } + if (me.debug) { + console.debug(resp.body); + } + + if ('valid' === resp.body.status) { + me._expires = resp.body.expires; + me._certificate = resp.body.certificate; + + return resp.body; // return order + } + + if ('processing' === resp.body.status) { + return ACME._wait().then(pollCert); + } + + if (me.debug) { + console.debug( + 'Error: bad status:\n' + JSON.stringify(resp.body, null, 2) + ); + } + + if ('pending' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'pending'." + + ' Best guess: You have not accepted at least one challenge for each domain:\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ('invalid' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'invalid'." + + ' Best guess: One or more of the domain challenges could not be verified' + + ' (or the order was canceled).\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ('ready' === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'.\n" + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + + '\n\n' + + 'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js' + ) + ); + } + + return Promise.reject( + new Error( + "Didn't finalize order: Unhandled status '" + + resp.body.status + + "'." + + ' This is not one of the known statuses...\n' + + "Requested: '" + + options.domains.join(', ') + + "'\n" + + "Validated: '" + + validatedDomains.join(', ') + + "'\n" + + JSON.stringify(resp.body, null, 2) + + '\n\n' + + 'Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js' + ) + ); + }); + } + + return pollCert(); }; -ACME._getCertificate = function (me, options) { - if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } - - // Lot's of error checking to inform the user of mistakes - if (!(options.challengeTypes||[]).length) { - options.challengeTypes = Object.keys(options.challenges||{}); - } - if (!options.challengeTypes.length) { - options.challengeTypes = [ options.challengeType ].filter(Boolean); - } - if (options.challengeType) { - options.challengeTypes.sort(function (a, b) { - if (a === options.challengeType) { return -1; } - if (b === options.challengeType) { return 1; } - return 0; - }); - if (options.challengeType !== options.challengeTypes[0]) { - return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," - + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); - } - } - // TODO check that all challengeTypes are represented in challenges - if (!options.challengeTypes.length) { - return Promise.reject(new Error("options.challengeTypes (string array) must be specified" - + " (and in order of preferential priority).")); - } - if (!(options.domains && options.domains.length)) { - return Promise.reject(new Error("options.domains must be a list of string domain names," - + " with the first being the subject of the domain (or options.subject must specified).")); - } - - // It's just fine if there's no account, we'll go get the key id we need via the public key - if (!me._kid) { - if (options.accountKid || options.account && options.account.kid) { - me._kid = options.accountKid || options.account.kid; - } else { - //return Promise.reject(new Error("must include KeyID")); - // This is an idempotent request. It'll return the same account for the same public key. - return ACME._registerAccount(me, options).then(function () { - // start back from the top - return ACME._getCertificate(me, options); - }); - } - } - - // Do a little dry-run / self-test - return ACME._testChallenges(me, options).then(function () { - if (me.debug) { console.debug('[acme-v2] certificates.create'); } - return ACME._getNonce(me).then(function () { - var body = { - // raw wildcard syntax MUST be used here - identifiers: options.domains.sort(function (a, b) { - // the first in the list will be the subject of the certificate, I believe (and hope) - if (!options.subject) { return 0; } - if (options.subject === a) { return -1; } - if (options.subject === b) { return 1; } - return 0; - }).map(function (hostname) { - return { type: "dns", value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; - - var payload = JSON.stringify(body); - // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? - me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); - me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) - var jws = me.RSA.signJws( - options.accountKeypair - , undefined - , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } - , Buffer.from(payload, 'utf8') - ); - - if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } - me._nonce = null; - return me._request({ - method: 'POST' - , url: me._directoryUrls.newOrder - , headers: { 'Content-Type': 'application/jose+json' } - , json: jws - }).then(function (resp) { - me._nonce = resp.toJSON().headers['replay-nonce']; - var location = resp.toJSON().headers.location; - var setAuths; - var auths = []; - if (me.debug) { console.debug(location); } // the account id url - if (me.debug) { console.debug(resp.toJSON()); } - me._authorizations = resp.body.authorizations; - me._order = location; - me._finalize = resp.body.finalize; - //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; - - if (!me._authorizations) { - return Promise.reject(new Error( - "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" - + JSON.stringify(resp.body) - )); - } - if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } - setAuths = me._authorizations.slice(0); - - function setNext() { - var authUrl = setAuths.shift(); - if (!authUrl) { return; } - - return ACME._getChallenges(me, options, authUrl).then(function (results) { - // var domain = options.domains[i]; // results.identifier.value - - // If it's already valid, we're golden it regardless - if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { - return setNext(); - } - - var challenge = ACME._chooseChallenge(options, results); - if (!challenge) { - // For example, wildcards require dns-01 and, if we don't have that, we have to bail - return Promise.reject(new Error( - "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." - )); - } - - var auth = ACME._challengeToAuth(me, options, results, challenge); - auths.push(auth); - return ACME._setChallenge(me, options, auth).then(setNext); - }); - } - - function challengeNext() { - var auth = auths.shift(); - if (!auth) { return; } - return ACME._postChallenge(me, options, auth).then(challengeNext); - } - - // First we set every challenge - // Then we ask for each challenge to be checked - // Doing otherwise would potentially cause us to poison our own DNS cache with misses - return setNext().then(challengeNext).then(function () { - if (me.debug) { console.debug("[getCertificate] next.then"); } - var validatedDomains = body.identifiers.map(function (ident) { - return ident.value; - }); - - return ACME._finalizeOrder(me, options, validatedDomains); - }).then(function (order) { - if (me.debug) { console.debug('acme-v2: order was finalized'); } - return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { - if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } - // https://github.com/certbot/certbot/issues/5721 - var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); - // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ - var certs = { - expires: order.expires - , identifiers: order.identifiers - //, authorizations: order.authorizations - , cert: certsarr.shift() - //, privkey: privkeyPem - , chain: certsarr.join('\n') - }; - if (me.debug) { console.debug(certs); } - return certs; - }); - }); - }); - }); - }); +ACME._getCertificate = function(me, options) { + if (me.debug) { + console.debug('[acme-v2] DEBUG get cert 1'); + } + + // Lot's of error checking to inform the user of mistakes + if (!(options.challengeTypes || []).length) { + options.challengeTypes = Object.keys(options.challenges || {}); + } + if (!options.challengeTypes.length) { + options.challengeTypes = [options.challengeType].filter(Boolean); + } + if (options.challengeType) { + options.challengeTypes.sort(function(a, b) { + if (a === options.challengeType) { + return -1; + } + if (b === options.challengeType) { + return 1; + } + return 0; + }); + if (options.challengeType !== options.challengeTypes[0]) { + return Promise.reject( + new Error( + "options.challengeType is '" + + options.challengeType + + "'," + + " which does not exist in the supplied types '" + + options.challengeTypes.join(',') + + "'" + ) + ); + } + } + // TODO check that all challengeTypes are represented in challenges + if (!options.challengeTypes.length) { + return Promise.reject( + new Error( + 'options.challengeTypes (string array) must be specified' + + ' (and in order of preferential priority).' + ) + ); + } + if (!(options.domains && options.domains.length)) { + return Promise.reject( + new Error( + 'options.domains must be a list of string domain names,' + + ' with the first being the subject of the domain (or options.subject must specified).' + ) + ); + } + + // It's just fine if there's no account, we'll go get the key id we need via the public key + if (!me._kid) { + if (options.accountKid || (options.account && options.account.kid)) { + me._kid = options.accountKid || options.account.kid; + } else { + //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. + return ACME._registerAccount(me, options).then(function() { + // start back from the top + return ACME._getCertificate(me, options); + }); + } + } + + // Do a little dry-run / self-test + return ACME._testChallenges(me, options).then(function() { + if (me.debug) { + console.debug('[acme-v2] certificates.create'); + } + return ACME._getNonce(me).then(function() { + var body = { + // raw wildcard syntax MUST be used here + identifiers: options.domains + .sort(function(a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { + return 0; + } + if (options.subject === a) { + return -1; + } + if (options.subject === b) { + return 1; + } + return 0; + }) + .map(function(hostname) { + return { type: 'dns', value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer? + me._kty = + (options.accountKeypair.privateKeyJwk && + options.accountKeypair.privateKeyJwk.kty) || + 'RSA'; + me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled) + var jws = me.RSA.signJws( + options.accountKeypair, + undefined, + { + nonce: me._nonce, + alg: me._alg, + url: me._directoryUrls.newOrder, + kid: me._kid + }, + Buffer.from(payload, 'utf8') + ); + + if (me.debug) { + console.debug('\n[DEBUG] newOrder\n'); + } + me._nonce = null; + return me + ._request({ + method: 'POST', + url: me._directoryUrls.newOrder, + headers: { 'Content-Type': 'application/jose+json' }, + json: jws + }) + .then(function(resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + var location = resp.toJSON().headers.location; + var setAuths; + var auths = []; + if (me.debug) { + console.debug(location); + } // the account id url + if (me.debug) { + console.debug(resp.toJSON()); + } + me._authorizations = resp.body.authorizations; + me._order = location; + me._finalize = resp.body.finalize; + //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return; + + if (!me._authorizations) { + return Promise.reject( + new Error( + "[acme-v2.js] authorizations were not fetched for '" + + options.domains.join() + + "':\n" + + JSON.stringify(resp.body) + ) + ); + } + if (me.debug) { + console.debug('[acme-v2] POST newOrder has authorizations'); + } + setAuths = me._authorizations.slice(0); + + function setNext() { + var authUrl = setAuths.shift(); + if (!authUrl) { + return; + } + + return ACME._getChallenges(me, options, authUrl).then(function( + results + ) { + // var domain = options.domains[i]; // results.identifier.value + + // If it's already valid, we're golden it regardless + if ( + results.challenges.some(function(ch) { + return 'valid' === ch.status; + }) + ) { + return setNext(); + } + + var challenge = ACME._chooseChallenge(options, results); + if (!challenge) { + // For example, wildcards require dns-01 and, if we don't have that, we have to bail + return Promise.reject( + new Error( + "Server didn't offer any challenge we can handle for '" + + options.domains.join() + + "'." + ) + ); + } + + var auth = ACME._challengeToAuth(me, options, results, challenge); + auths.push(auth); + return ACME._setChallenge(me, options, auth).then(setNext); + }); + } + + function challengeNext() { + var auth = auths.shift(); + if (!auth) { + return; + } + return ACME._postChallenge(me, options, auth).then(challengeNext); + } + + // First we set every challenge + // Then we ask for each challenge to be checked + // Doing otherwise would potentially cause us to poison our own DNS cache with misses + return setNext() + .then(challengeNext) + .then(function() { + if (me.debug) { + console.debug('[getCertificate] next.then'); + } + var validatedDomains = body.identifiers.map(function(ident) { + return ident.value; + }); + + return ACME._finalizeOrder(me, options, validatedDomains); + }) + .then(function(order) { + if (me.debug) { + console.debug('acme-v2: order was finalized'); + } + return me + ._request({ method: 'GET', url: me._certificate, json: true }) + .then(function(resp) { + if (me.debug) { + console.debug('acme-v2: csr submitted and cert received:'); + } + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain( + ACME.formatPemChain(resp.body || '') + ); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + var certs = { + expires: order.expires, + identifiers: order.identifiers, + //, authorizations: order.authorizations + cert: certsarr.shift(), + //, privkey: privkeyPem + chain: certsarr.join('\n') + }; + if (me.debug) { + console.debug(certs); + } + return certs; + }); + }); + }); + }); + }); }; ACME.create = function create(me) { - if (!me) { me = {}; } - // me.debug = true; - me.challengePrefixes = ACME.challengePrefixes; - me.RSA = me.RSA || require('rsa-compat').RSA; - //me.Keypairs = me.Keypairs || require('keypairs'); - me.request = me.request || require('@coolaj86/urequest'); - me._dig = function (query) { - // TODO use digd.js - return new Promise(function (resolve, reject) { - var dns = require('dns'); - dns.resolveTxt(query.name, function (err, records) { - if (err) { reject(err); return; } - - resolve({ - answer: records.map(function (rr) { - return { - data: rr - }; - }) - }); - }); - }); - }; - me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; - - - if ('function' !== typeof me.getUserAgentString) { - me.pkg = me.pkg || require('./package.json'); - me.os = me.os || require('os'); - me.process = me.process || require('process'); - me.userAgent = ACME._getUserAgentString(me); - } - - function getRequest(opts) { - if (!opts) { opts = {}; } - - return me.request.defaults({ - headers: { - 'User-Agent': opts.userAgent || me.userAgent || me.getUserAgentString(me) - } - }); - } - - if ('function' !== typeof me._request) { - me._request = me.promisify(getRequest({})); - } - - me.init = function (_directoryUrl) { - me.directoryUrl = me.directoryUrl || _directoryUrl; - return ACME._directory(me).then(function (resp) { - me._directoryUrls = resp.body; - me._tos = me._directoryUrls.meta.termsOfService; - return me._directoryUrls; - }); - }; - me.accounts = { - create: function (options) { - return ACME._registerAccount(me, options); - } - }; - me.certificates = { - create: function (options) { - return ACME._getCertificate(me, options); - } - }; - return me; + if (!me) { + me = {}; + } + // me.debug = true; + me.challengePrefixes = ACME.challengePrefixes; + me.RSA = me.RSA || require('rsa-compat').RSA; + //me.Keypairs = me.Keypairs || require('keypairs'); + me.request = me.request || require('@coolaj86/urequest'); + me._dig = function(query) { + // TODO use digd.js + return new Promise(function(resolve, reject) { + var dns = require('dns'); + dns.resolveTxt(query.name, function(err, records) { + if (err) { + reject(err); + return; + } + + resolve({ + answer: records.map(function(rr) { + return { + data: rr + }; + }) + }); + }); + }); + }; + me.promisify = + me.promisify || + require('util').promisify /*node v8+*/ || + require('bluebird').promisify /*node v6*/; + + if ('function' !== typeof me.getUserAgentString) { + me.pkg = me.pkg || require('./package.json'); + me.os = me.os || require('os'); + me.process = me.process || require('process'); + me.userAgent = ACME._getUserAgentString(me); + } + + function getRequest(opts) { + if (!opts) { + opts = {}; + } + + return me.request.defaults({ + headers: { + 'User-Agent': + opts.userAgent || me.userAgent || me.getUserAgentString(me) + } + }); + } + + if ('function' !== typeof me._request) { + me._request = me.promisify(getRequest({})); + } + + me.init = function(_directoryUrl) { + me.directoryUrl = me.directoryUrl || _directoryUrl; + return ACME._directory(me).then(function(resp) { + me._directoryUrls = resp.body; + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + }; + me.accounts = { + create: function(options) { + return ACME._registerAccount(me, options); + } + }; + me.certificates = { + create: function(options) { + return ACME._getCertificate(me, options); + } + }; + return me; }; -ACME._toWebsafeBase64 = function (b64) { - return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); +ACME._toWebsafeBase64 = function(b64) { + return b64 + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); }; diff --git a/package-lock.json b/package-lock.json index c763d17..07f4556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,40 +1,40 @@ { - "name": "acme-v2", - "version": "1.7.6", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@coolaj86/urequest": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", - "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" - }, - "eckles": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", - "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" - }, - "keypairs": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", - "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", - "requires": { - "eckles": "^1.4.1", - "rasha": "^1.2.4" - } - }, - "rasha": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", - "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" - }, - "rsa-compat": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", - "integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", - "requires": { - "keypairs": "^1.2.14" - } - } - } + "name": "acme-v2", + "version": "1.7.6", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@coolaj86/urequest": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", + "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" + }, + "eckles": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", + "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" + }, + "keypairs": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", + "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", + "requires": { + "eckles": "^1.4.1", + "rasha": "^1.2.4" + } + }, + "rasha": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", + "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" + }, + "rsa-compat": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", + "integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", + "requires": { + "keypairs": "^1.2.14" + } + } + } } diff --git a/package.json b/package.json index 3da21c7..0c225a2 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,32 @@ { - "name": "acme-v2", - "version": "1.7.7", - "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", - "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", - "main": "node.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" - }, - "keywords": [ - "Let's Encrypt", - "ACME", - "v02", - "v2", - "draft-11", - "draft-12", - "free ssl", - "tls", - "automated https", - "letsencrypt" - ], - "author": "AJ ONeal (https://coolaj86.com/)", - "license": "MPL-2.0", - "dependencies": { - "@coolaj86/urequest": "^1.3.6", - "rsa-compat": "^2.0.6" - } + "name": "acme-v2", + "version": "1.7.7", + "description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", + "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", + "main": "node.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" + }, + "keywords": [ + "Let's Encrypt", + "ACME", + "v02", + "v2", + "draft-11", + "draft-12", + "free ssl", + "tls", + "automated https", + "letsencrypt" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "dependencies": { + "@coolaj86/urequest": "^1.3.6", + "rsa-compat": "^2.0.6" + } } diff --git a/tests/cb.js b/tests/cb.js index c5676ff..68b343b 100644 --- a/tests/cb.js +++ b/tests/cb.js @@ -4,80 +4,115 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; -module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { - // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - var acme2 = require('../').ACME.create({ RSA: RSA }); - acme2.init(directoryUrl).then(function () { - var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); - } - , setChallenge: function (opts, cb) { - var pathname; +module.exports.run = function run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair +) { + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + var acme2 = require('../').ACME.create({ RSA: RSA }); + acme2.init(directoryUrl).then(function() { + var options = { + agreeToTerms: function(tosUrl, agree) { + agree(null, tosUrl); + }, + setChallenge: function(opts, cb) { + var pathname; - console.log(""); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(""); + console.log(''); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(''); - if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); - console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); - } else if ('dns-01' === opts.type) { - pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); - console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); - console.log("ddig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); - } else { - cb(new Error("[acme-v2] unrecognized challenge type")); - return; - } - console.log("\nThen hit the 'any' key to continue..."); + if ('http-01' === opts.type) { + pathname = + opts.hostname + + acme2.challengePrefixes['http-01'] + + '/' + + opts.token; + console.log( + "Put the string '" + + opts.keyAuthorization + + "' into a file at '" + + pathname + + "'" + ); + console.log( + "echo '" + opts.keyAuthorization + "' > '" + pathname + "'" + ); + } else if ('dns-01' === opts.type) { + pathname = + acme2.challengePrefixes['dns-01'] + + '.' + + opts.hostname.replace(/^\*\./, ''); + console.log( + "Put the string '" + + opts.dnsAuthorization + + "' into the TXT record '" + + pathname + + "'" + ); + console.log( + 'ddig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" + ); + } else { + cb(new Error('[acme-v2] unrecognized challenge type')); + return; + } + console.log("\nThen hit the 'any' key to continue..."); - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - cb(); - } + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - } - , removeChallenge: function (opts, cb) { - // hostname, key - console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); - setTimeout(cb, 1 * 1000); - } - , challengeType: chType - , email: email - , accountKeypair: accountKeypair - , domainKeypair: domainKeypair - , domains: web - }; + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + }, + removeChallenge: function(opts, cb) { + // hostname, key + console.log( + '[acme-v2] remove challenge', + opts.hostname, + opts.keyAuthorization + ); + setTimeout(cb, 1 * 1000); + }, + challengeType: chType, + email: email, + accountKeypair: accountKeypair, + domainKeypair: domainKeypair, + domains: web + }; - acme2.accounts.create(options).then(function (account) { - console.log('[acme-v2] account:'); - console.log(account); + acme2.accounts.create(options).then(function(account) { + console.log('[acme-v2] account:'); + console.log(account); - acme2.certificates.create(options).then(function (fullchainPem) { - console.log('[acme-v2] fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); + acme2.certificates.create(options).then(function(fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); }; diff --git a/tests/compat.js b/tests/compat.js index a993fd5..363dfba 100644 --- a/tests/compat.js +++ b/tests/compat.js @@ -4,69 +4,103 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; -module.exports.run = function (directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { - console.log('[DEBUG] run', web, chType, email); +module.exports.run = function( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair +) { + console.log('[DEBUG] run', web, chType, email); - var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); - acme2.getAcmeUrls(acme2.stagingServerUrl, function (err/*, directoryUrls*/) { - if (err) { console.log('err 1'); throw err; } + var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); + acme2.getAcmeUrls(acme2.stagingServerUrl, function(err /*, directoryUrls*/) { + if (err) { + console.log('err 1'); + throw err; + } - var options = { - agreeToTerms: function (tosUrl, agree) { - agree(null, tosUrl); - } - , setChallenge: function (hostname, token, val, cb) { - var pathname; + var options = { + agreeToTerms: function(tosUrl, agree) { + agree(null, tosUrl); + }, + setChallenge: function(hostname, token, val, cb) { + var pathname; - if ('http-01' === cb.type) { - pathname = hostname + acme2.acmeChallengePrefix + token; - console.log("Put the string '" + val /*keyAuthorization*/ + "' into a file at '" + pathname + "'"); - console.log("echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'"); - console.log("\nThen hit the 'any' key to continue..."); - } else if ('dns-01' === cb.type) { - // forwards-backwards compat - pathname = acme2.challengePrefixes['dns-01'] + "." + hostname.replace(/^\*\./, ''); - console.log("Put the string '" + cb.dnsAuthorization + "' into the TXT record '" + pathname + "'"); - console.log("dig TXT " + pathname + " '" + cb.dnsAuthorization + "'"); - console.log("\nThen hit the 'any' key to continue..."); - } else { - cb(new Error("[acme-v2] unrecognized challenge type: " + cb.type)); - return; - } + if ('http-01' === cb.type) { + pathname = hostname + acme2.acmeChallengePrefix + token; + console.log( + "Put the string '" + + val /*keyAuthorization*/ + + "' into a file at '" + + pathname + + "'" + ); + console.log( + "echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'" + ); + console.log("\nThen hit the 'any' key to continue..."); + } else if ('dns-01' === cb.type) { + // forwards-backwards compat + pathname = + acme2.challengePrefixes['dns-01'] + + '.' + + hostname.replace(/^\*\./, ''); + console.log( + "Put the string '" + + cb.dnsAuthorization + + "' into the TXT record '" + + pathname + + "'" + ); + console.log('dig TXT ' + pathname + " '" + cb.dnsAuthorization + "'"); + console.log("\nThen hit the 'any' key to continue..."); + } else { + cb(new Error('[acme-v2] unrecognized challenge type: ' + cb.type)); + return; + } - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - cb(); - } + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + cb(); + } - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - } - , removeChallenge: function (hostname, key, cb) { - console.log('[DEBUG] remove challenge', hostname, key); - setTimeout(cb, 1 * 1000); - } - , challengeType: chType - , email: email - , accountKeypair: accountKeypair - , domainKeypair: domainKeypair - , domains: web - }; + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + }, + removeChallenge: function(hostname, key, cb) { + console.log('[DEBUG] remove challenge', hostname, key); + setTimeout(cb, 1 * 1000); + }, + challengeType: chType, + email: email, + accountKeypair: accountKeypair, + domainKeypair: domainKeypair, + domains: web + }; - acme2.registerNewAccount(options, function (err, account) { - if (err) { console.log('err 2'); throw err; } - if (options.debug) console.debug('account:'); - if (options.debug) console.log(account); + acme2.registerNewAccount(options, function(err, account) { + if (err) { + console.log('err 2'); + throw err; + } + if (options.debug) console.debug('account:'); + if (options.debug) console.log(account); - acme2.getCertificate(options, function (err, fullchainPem) { - if (err) { console.log('err 3'); throw err; } - console.log('[acme-v2] A fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); + acme2.getCertificate(options, function(err, fullchainPem) { + if (err) { + console.log('err 3'); + throw err; + } + console.log('[acme-v2] A fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); }; diff --git a/tests/fullchain-formats.js b/tests/fullchain-formats.js index f1ac284..b265b11 100644 --- a/tests/fullchain-formats.js +++ b/tests/fullchain-formats.js @@ -23,59 +23,67 @@ Rules */ // https://github.com/certbot/certbot/issues/5721#issuecomment-402362709 -var expected = "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n"; +var expected = '----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n'; var tests = [ - "----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" -, "----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n" -, "----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----" -, "----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----" -, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----" -, "----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n" -, "----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n" -, "----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n" + '----\r\nxxxx\r\nyyyy\r\n----\r\n\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n', + '----\r\nxxxx\r\nyyyy\r\n----\r\n----\r\nxxxx\r\nyyyy\r\n----\r\n', + '----\nxxxx\nyyyy\n----\n\n----\r\nxxxx\r\nyyyy\r\n----', + '----\nxxxx\nyyyy\n----\n----\r\nxxxx\r\nyyyy\r\n----', + '----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----', + '----\nxxxx\nyyyy\n----\n----\nxxxx\nyyyy\n----\n', + '----\nxxxx\nyyyy\n----\n\n----\nxxxx\nyyyy\n----\n', + '----\nxxxx\nyyyy\n----\r\n----\nxxxx\ryyyy\n----\n' ]; function formatPemChain(str) { - return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; + return ( + str + .trim() + .replace(/[\r\n]+/g, '\n') + .replace(/\-\n\-/g, '-\n\n-') + '\n' + ); } function splitPemChain(str) { - return str.trim().split(/[\r\n]{2,}/g).map(function (str) { - return str + '\n'; - }); + return str + .trim() + .split(/[\r\n]{2,}/g) + .map(function(str) { + return str + '\n'; + }); } -tests.forEach(function (str) { - var actual = formatPemChain(str); - if (expected !== actual) { - console.error('input: ', JSON.stringify(str)); - console.error('expected:', JSON.stringify(expected)); - console.error('actual: ', JSON.stringify(actual)); - throw new Error("did not pass"); - } +tests.forEach(function(str) { + var actual = formatPemChain(str); + if (expected !== actual) { + console.error('input: ', JSON.stringify(str)); + console.error('expected:', JSON.stringify(expected)); + console.error('actual: ', JSON.stringify(actual)); + throw new Error('did not pass'); + } }); if ( - "----\nxxxx\nyyyy\n----\n" - !== - formatPemChain("\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n") + '----\nxxxx\nyyyy\n----\n' !== + formatPemChain('\n\n----\r\nxxxx\r\nyyyy\r\n----\n\n') ) { - throw new Error("Not proper for single cert in chain"); + throw new Error('Not proper for single cert in chain'); } if ( - "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" - !== - formatPemChain("\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n") + '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' !== + formatPemChain( + '\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n--B--\nxxxx\nyyyy\n--E--\n\n\n' + ) ) { - throw new Error("Not proper for three certs in chain"); + throw new Error('Not proper for three certs in chain'); } splitPemChain( - "--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n" -).forEach(function (str) { - if ("--B--\nxxxx\nyyyy\n--E--\n" !== str) { - throw new Error("bad thingy"); - } + '--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n\n--B--\nxxxx\nyyyy\n--E--\n' +).forEach(function(str) { + if ('--B--\nxxxx\nyyyy\n--E--\n' !== str) { + throw new Error('bad thingy'); + } }); console.info('PASS'); diff --git a/tests/promise.js b/tests/promise.js index 1d7a266..90107a5 100644 --- a/tests/promise.js +++ b/tests/promise.js @@ -5,85 +5,120 @@ 'use strict'; /* global Promise */ -module.exports.run = function run(directoryUrl, RSA, web, chType, email, accountKeypair, domainKeypair) { - var acme2 = require('../').ACME.create({ RSA: RSA }); - // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' - acme2.init(directoryUrl).then(function () { - var options = { - agreeToTerms: function (tosUrl) { - return Promise.resolve(tosUrl); - } - , setChallenge: function (opts) { - return new Promise(function (resolve, reject) { - var pathname; +module.exports.run = function run( + directoryUrl, + RSA, + web, + chType, + email, + accountKeypair, + domainKeypair +) { + var acme2 = require('../').ACME.create({ RSA: RSA }); + // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01' + acme2.init(directoryUrl).then(function() { + var options = { + agreeToTerms: function(tosUrl) { + return Promise.resolve(tosUrl); + }, + setChallenge: function(opts) { + return new Promise(function(resolve, reject) { + var pathname; - console.log(""); - console.log('identifier:'); - console.log(opts.identifier); - console.log('hostname:'); - console.log(opts.hostname); - console.log('type:'); - console.log(opts.type); - console.log('token:'); - console.log(opts.token); - console.log('thumbprint:'); - console.log(opts.thumbprint); - console.log('keyAuthorization:'); - console.log(opts.keyAuthorization); - console.log('dnsAuthorization:'); - console.log(opts.dnsAuthorization); - console.log(""); + console.log(''); + console.log('identifier:'); + console.log(opts.identifier); + console.log('hostname:'); + console.log(opts.hostname); + console.log('type:'); + console.log(opts.type); + console.log('token:'); + console.log(opts.token); + console.log('thumbprint:'); + console.log(opts.thumbprint); + console.log('keyAuthorization:'); + console.log(opts.keyAuthorization); + console.log('dnsAuthorization:'); + console.log(opts.dnsAuthorization); + console.log(''); - if ('http-01' === opts.type) { - pathname = opts.hostname + acme2.challengePrefixes['http-01'] + "/" + opts.token; - console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + pathname + "'"); - console.log("echo '" + opts.keyAuthorization + "' > '" + pathname + "'"); - } else if ('dns-01' === opts.type) { - pathname = acme2.challengePrefixes['dns-01'] + "." + opts.hostname.replace(/^\*\./, ''); - console.log("Put the string '" + opts.dnsAuthorization + "' into the TXT record '" + pathname + "'"); - console.log("dig TXT " + pathname + " '" + opts.dnsAuthorization + "'"); - } else { - reject(new Error("[acme-v2] unrecognized challenge type")); - return; - } - console.log("\nThen hit the 'any' key to continue..."); + if ('http-01' === opts.type) { + pathname = + opts.hostname + + acme2.challengePrefixes['http-01'] + + '/' + + opts.token; + console.log( + "Put the string '" + + opts.keyAuthorization + + "' into a file at '" + + pathname + + "'" + ); + console.log( + "echo '" + opts.keyAuthorization + "' > '" + pathname + "'" + ); + } else if ('dns-01' === opts.type) { + pathname = + acme2.challengePrefixes['dns-01'] + + '.' + + opts.hostname.replace(/^\*\./, ''); + console.log( + "Put the string '" + + opts.dnsAuthorization + + "' into the TXT record '" + + pathname + + "'" + ); + console.log( + 'dig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" + ); + } else { + reject(new Error('[acme-v2] unrecognized challenge type')); + return; + } + console.log("\nThen hit the 'any' key to continue..."); - function onAny() { - console.log("'any' key was hit"); - process.stdin.pause(); - process.stdin.removeListener('data', onAny); - process.stdin.setRawMode(false); - resolve(); - return; - } + function onAny() { + console.log("'any' key was hit"); + process.stdin.pause(); + process.stdin.removeListener('data', onAny); + process.stdin.setRawMode(false); + resolve(); + return; + } - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.on('data', onAny); - }); - } - , removeChallenge: function (opts) { - console.log('[acme-v2] remove challenge', opts.hostname, opts.keyAuthorization); - return new Promise(function (resolve) { - // hostname, key - setTimeout(resolve, 1 * 1000); - }); - } - , challengeType: chType - , email: email - , accountKeypair: accountKeypair - , domainKeypair: domainKeypair - , domains: web - }; + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onAny); + }); + }, + removeChallenge: function(opts) { + console.log( + '[acme-v2] remove challenge', + opts.hostname, + opts.keyAuthorization + ); + return new Promise(function(resolve) { + // hostname, key + setTimeout(resolve, 1 * 1000); + }); + }, + challengeType: chType, + email: email, + accountKeypair: accountKeypair, + domainKeypair: domainKeypair, + domains: web + }; - acme2.accounts.create(options).then(function (account) { - console.log('[acme-v2] account:'); - console.log(account); + acme2.accounts.create(options).then(function(account) { + console.log('[acme-v2] account:'); + console.log(account); - acme2.certificates.create(options).then(function (fullchainPem) { - console.log('[acme-v2] fullchain.pem:'); - console.log(fullchainPem); - }); - }); - }); + acme2.certificates.create(options).then(function(fullchainPem) { + console.log('[acme-v2] fullchain.pem:'); + console.log(fullchainPem); + }); + }); + }); };