v3.0.0: update for greenlock v2.7+

This commit is contained in:
AJ ONeal 2019-04-05 23:11:00 -06:00
parent 63a9deea59
commit bda3a62e28
4 changed files with 324 additions and 57 deletions

View File

@ -10,7 +10,7 @@ le-challenge-manual
A [Root](https://rootprojects.org) Project A [Root](https://rootprojects.org) Project
A manual cli-based strategy for node-letsencrypt. A manual cli-based strategy for [Greenlock](https://git.coolaj86.com/coolaj86/greenlock-express.js) v2.7+ (and v3).
Prints the ACME challenge Token and Key and then waits for you to hit enter before continuing. Prints the ACME challenge Token and Key and then waits for you to hit enter before continuing.
@ -18,37 +18,102 @@ Install
------- -------
```bash ```bash
npm install --save le-challenge-manual@2.x npm install --save le-challenge-manual@3.x
``` ```
Usage Usage
----- -----
```bash ```bash
var leChallenge = require('le-challenge-manual').create({ var Greenlock = require('greenlock');
, debug: false
});
var LE = require('letsencrypt'); Greenlock.create({
...
LE.create({ , challenges: { 'http-01': require('le-challenge-manual')
server: LE.stagingServerUrl , 'dns-01': require('le-challenge-manual')
, challenge: leChallenge , 'tls-alpn-01': require('le-challenge-manual')
}
...
}); });
``` ```
NOTE: If you request a certificate with 6 domains listed, Note: If you request a certificate with 6 domains listed,
it will require 6 individual challenges. it will require 6 individual challenges.
Exposed Methods Exposed (Promise) Methods
--------------- ---------------
For ACME Challenge: For ACME Challenge:
* `set(opts, domain, key, val, done)` * `set(opts)`
* `get(defaults, domain, key, done)` * `remove(opts)`
* `remove(defaults, domain, key, done)`
For node-letsencrypt internals: The options will look like this for normal domains:
* `getOptions()` returns the internal defaults merged with the user-supplied options ```js
{ challenge: {
type: 'http-01'
, identifier: { type: 'dns', value: 'example.com' }
, wildcard: false
, expires: '2012-01-01T12:00:00.000Z'
, token: 'abc123'
, thumbprint: '<<account key thumbprint>>'
, keyAuthorization: 'abc123.xxxx'
, dnsHost: '_acme-challenge.example.com'
, dnsAuthorization: 'yyyy'
, altname: 'example.com'
}
}
```
And they'll look like this for wildcard domains:
```js
{ challenge: {
type: 'http-01'
, identifier: { type: 'dns', value: 'example.com' }
, wildcard: true
, expires: '2012-01-01T12:00:00.000Z'
, token: 'abc123'
, thumbprint: '<<account key thumbprint>>'
, keyAuthorization: 'abc123.xxxx'
, dnsHost: '_acme-challenge.example.com'
, dnsAuthorization: 'yyyy'
, altname: '*.example.com'
}
}
```
The only difference is that `altname` will have the `*.` prefix (which you would expect
but, of course, can't work as a specific a DNS record) and the `wildcard` property is `true`.
Optional
* `get(limitedOpts)`
Because the get method is apart from the main flow (such as a DNS query),
it's not always implemented and the options are much more limited in scope:
```js
{ challenge: {
type: 'http-01'
, identifier: { type: 'dns', value: 'example.com' }
, wildcard: false
, token: 'abc123'
, altname: 'example.com'
}
}
```
If there were an implementation of Greenlock integrated directly into
a NameServer (which currently there is not), it would probably look like this:
```js
{ challenge: {
type: 'dns-01'
, identifier: { type: 'dns', value: 'example.com' }
, token: 'abc123'
, dnsHost: '_acme-challenge.example.com'
}
}
```

222
index.js
View File

@ -1,35 +1,132 @@
'use strict'; 'use strict';
/*global Promise*/
var Challenge = module.exports; var Challenge = module.exports;
Challenge.create = function (defaults) { // IMPORTANT
return { //
getOptions: function () { // These are all PROMISIFIED by Greenlock in such a way that
return defaults; // it doesn't matter whether you return synchronously, asynchronously,
// or even node-style callback thunk.
//
// Typically you should be using a promise or async function,
// but choose whichever makes sense for you.
Challenge.create = function (config) {
// If your implementation needs config options, set them. Otherwise, don't bother (duh).
var challenger = {};
// Note: normally you'd implement these right here, but for the sake of
// documentation I've abstracted them out "Table of Contents"-style.
// call out to set the challenge, wherever
challenger.set = function (opts, cb) {
var ch = opts.challenge;
if ('http-01' === ch.type) {
return Challenge._setHttp(opts, cb);
} else if ('dns-01' === ch.type) {
return Challenge._setDns(opts, cb);
} else {
return Challenge._setAny(opts, cb);
} }
, set: Challenge.set
, get: Challenge.get
, remove: Challenge.remove
}; };
// call out to remove the challenge, wherever
challenger.remove = function (opts) {
var ch = opts.challenge;
if ('http-01' === ch.type) {
return Challenge._removeHttp(opts);
} else if ('dns-01' === ch.type) {
return Challenge._removeDns(opts);
} else {
return Challenge._removeAny(opts);
}
};
// only really useful for http,
// but probably not so much in this context...
// (though you can test it and it'll work)
challenger.get = function (opts) {
var ch = opts.challenge;
if ('http-01' === ch.type) {
return Challenge._getHttp(opts);
} else if ('dns-01' === ch.type) {
return Challenge._getDns(opts);
} else {
return Challenge._getAny(opts);
}
};
// Whatever you set to 'options' will be merged into 'opts' just before each call
// (for convenience, so you don't have to merge it yourself).
challenger.options = { debug: config.debug };
return challenger;
}; };
// Show the user the token and key and wait for them to be ready to continue // Show the user the token and key and wait for them to be ready to continue
Challenge.set = function (args, domain, token, secret, cb) { Challenge._setHttp = function (args, cb) {
// Using a node-style callback "thunk" in this example, because that makes
var ch = args.challenge;
console.info("[ACME http-01 '" + ch.altname + "' CHALLENGE]");
console.info("Your mission (since you chose to accept it):");
console.info("First, you must create a file with the following name and contents.");
console.info("Then, by any means necessary, you cause that file to appear at the specified URL.");
console.info(""); console.info("");
console.info("Challenge for '" + domain + "'"); console.info("\tFilename: " + ch.token);
console.info("\tContents: " + ch.keyAuthorization);
// TODO let acme-v2 handle generating this url
console.info('\tURL: http://' + ch.altname + '/.well-known/acme-challenge/' + ch.token);
console.info(""); console.info("");
console.info("We now present (for you copy-and-paste pleasure) your ACME Challenge"); console.info("And, if you need additional information for debugging:");
console.info("public Token and secret Key, in that order, respectively:");
console.info(token);
console.info(secret);
console.info(""); console.info("");
console.info(JSON.stringify({ console.info(JSON.stringify(httpChallengeToJson(ch), null, 2).replace(/^/gm, '\t'));
domain: domain
, token: token
, key: secret
}, null, ' ').replace(/^/gm, '\t'));
console.info(""); console.info("");
console.info("hit enter to continue..."); console.info("This message won't self-destruct, but you may press hit the any as soon as you're ready to continue...");
console.info("");
console.info("[Press the ANY key to continue...]");
process.stdin.resume();
process.stdin.once('data', function () {
process.stdin.pause();
cb(null);
});
};
Challenge._setDns = function (args, cb) {
// Using a node-style callback "thunk" in this example, because that makes
var ch = args.challenge;
console.info("[ACME dns-01 '" + ch.altname + "' CHALLENGE]");
console.info("Your mission (since you chose to accept it):");
console.info("First, you must create a DNS record with the following parameters:");
console.info("");
console.info(ch.dnsHost + "\tTXT\t" + ch.dnsKeyAuthorization + "\tTTL 60");
console.info("");
console.info("Next, wait, no... there is no next. That's it - but here's some stuff anyway:");
console.info("");
console.info(JSON.stringify(dnsChallengeToJson(ch), null, 2).replace(/^/gm, '\t'));
console.info("");
console.info("[Press the ANY key to continue...]");
process.stdin.resume();
process.stdin.once('data', function () {
process.stdin.pause();
cb(null);
});
};
Challenge._setAny = function (args, cb) {
var ch = args.challenge;
console.info("[ACME " + ch.type + " '" + ch.altname + "' CHALLENGE]");
console.info("There's no quippy pre-programmed response for this type of challenge.");
console.info("I have no idea what you intend to do, but I'll tell you everything I know:");
console.info("");
console.info(JSON.stringify(ch, null, 2).replace(/^/gm, '\t'));
console.info("");
console.info("[Press the ANY key to continue...]");
process.stdin.resume(); process.stdin.resume();
process.stdin.on('data', function () { process.stdin.on('data', function () {
process.stdin.pause(); process.stdin.pause();
@ -37,14 +134,89 @@ Challenge.set = function (args, domain, token, secret, cb) {
}); });
}; };
// nothing to do here, that's why it's manual // might as well tell the user that whatever they were setting up has been checked
Challenge.get = function (args, domain, token, cb) { Challenge._removeHttp = function (args) {
cb(null); var ch = args.challenge;
console.info("");
console.info("Challenge for '" + ch.altname + "' complete. You can delete this file now:");
console.info('\thttp://' + ch.altname + '/.well-known/acme-challenge/' + ch.token);
console.info("");
// this can return null or a Promise null
// (or callback null, just like the set() above)
return null;
};
Challenge._removeDns = function (args) {
var ch = args.challenge;
console.info("");
console.info("Challenge for '" + ch.altname + "' complete. You can remove this record now:");
console.info("\t" + ch.dnsHost + "\tTXT\t" + ch.dnsKeyAuthorization + "\tTTL 60");
console.info("");
// this can return null or a Promise null
// (or callback null, just like the set() above)
return null;
};
Challenge._removeAny = function (args) {
var ch = args.challenge;
console.info("");
console.info("Challenge for '" + ch.altname + "' complete. You can now undo what you did.");
console.info("");
// this can return null or a Promise null
// (or callback null, just like set() above)
return null;
}; };
// might as well tell the user that whatever they were setting up has been checked // nothing to do here, that's why it's manual
Challenge.remove = function (args, domain, token, cb) { Challenge._get = function (args, cb) {
console.info("Challenge for '" + domain + "' complete.");
console.info(""); console.info("");
console.info("Woah! Hey, guess what!? That's right you guessed it:");
console.info("It's time to painstakingly type out the ACME challenge response with your bear hands. Yes. Your bear hands.");
process.stdout.write("> ");
// Using a promise here just to show that Promises are support
// (in fact, they're the default)
return new Promise(function (resolve, reject) {
process.stdin.resume();
process.stdin.on('error', reject);
process.stdin.on('data', function (chunk) {
process.stdin.pause();
var result = chunk.toString();
try {
result = JSON.parse(result);
} catch(e) {
args.keyAuthorization = result;
}
cb(null); cb(null);
});
});
}; };
function httpChallengeToJson(ch) {
return {
type: ch.type
, altname: ch.altname
, identifier: ch.identifier
, wildcard: false
, expires: ch.expires
, token: ch.token
, thumbprint: ch.thumbprint
, keyAuthorization: ch.keyAuthorization
};
}
function dnsChallengeToJson(ch) {
return {
type: ch.type
, altname: '*.example.com'
, identifier: ch.identifier
, wildcard: ch.wildcard
, expires: ch.expires
, token: ch.token
, thumbprint: ch.thumbprint
, keyAuthorization: ch.keyAuthorization
, dnsHost: ch.dnsHost
, dnsAuthorization: ch.dnsAuthorization
};
}

View File

@ -1,6 +1,6 @@
{ {
"name": "le-challenge-manual", "name": "le-challenge-manual",
"version": "2.1.1", "version": "3.0.0",
"description": "A cli-based strategy for node-letsencrypt. Prints the ACME challenge Token and Key and then waits for you to hit enter before continuing.", "description": "A cli-based strategy for node-letsencrypt. Prints the ACME challenge Token and Key and then waits for you to hit enter before continuing.",
"main": "index.js", "main": "index.js",
"homepage": "https://git.coolaj86.com/coolaj86/le-challenge-manual.js", "homepage": "https://git.coolaj86.com/coolaj86/le-challenge-manual.js",

52
test.js
View File

@ -1,20 +1,50 @@
'use strict'; 'use strict';
/*global Promise*/
var challenge = require('./').create({}); var challenge = require('./').create({});
var opts = challenge.getOptions(); var opts = challenge.getOptions && challenge.getOptions() || challenge.options;
var domain = 'example.com';
var token = 'token-id';
var key = 'secret-key';
function run() {
// this will cause the prompt to appear // this will cause the prompt to appear
challenge.set(opts, domain, token, key, function (err) { return new Promise(function (resolve, reject) {
// if there's an error, there's a problem challenge.set(opts, function () {
if (err) { // this will cause the final completion message to appear
throw err; return Promise.resolve(challenge.remove(opts)).then(resolve).catch(reject);
});
});
} }
// this will cause the final completion message to appear opts.challenge = {
challenge.remove(opts, domain, token, function () { type: 'http-01'
}); , identifier: { type: 'dns', value: 'example.com' }
, wildcard: false
, expires: '2012-01-01T12:00:00.000Z'
, token: 'abc123'
, thumbprint: '<<account key thumbprint>>'
, keyAuthorization: 'abc123.xxxx'
, dnsHost: '_acme-challenge.example.com'
, dnsAuthorization: 'yyyy'
, altname: 'example.com'
};
run(opts).then(function () {
opts.challenge = {
type: 'dns-01'
, identifier: { type: 'dns', value: 'example.com' }
, wildcard: true
, expires: '2012-01-01T12:00:00.000Z'
, token: 'abc123'
, thumbprint: '<<account key thumbprint>>'
, keyAuthorization: 'abc123.xxxx'
, dnsHost: '_acme-challenge.example.com'
, dnsAuthorization: 'yyyy'
, altname: '*.example.com'
};
return run(opts);
}).then(function () {
console.info("PASS");
}).catch(function (err) {
console.error("FAIL");
console.error(err);
process.exit(17);
}); });