request cleanup

This commit is contained in:
AJ ONeal 2019-10-25 04:54:40 -06:00
parent 54cda5a888
commit b39a3763cf
9 changed files with 203 additions and 52 deletions

View File

@ -1,4 +1,4 @@
# [ACME.js](https://git.rootprojects.org/root/acme.js) v3 # [ACME.js](https://git.rootprojects.org/root/acme.js) (RFC 8555 / November 2019)
| Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains) | Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains)
@ -52,6 +52,31 @@ If they don't, please open an issue to let us know why.
We'd much rather improve the app than have a hundred different versions running in the wild. We'd much rather improve the app than have a hundred different versions running in the wild.
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify. However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
# API Overview
```js
ACME.create({ maintainerEmail, packageAgent });
acme.init(directoryUrl);
acme.accounts.create({ subscriberEmail, agreeToTerms, accountKey });
acme.certificates.create({
customerEmail, // do not use
account,
accountKey,
serverKey,
csr,
domains,
challenges
});
```
```js
ACME.computeChallenge({
accountKey: jwk,
hostname: 'example.com',
challenge: { type: 'dns-01', token: 'xxxx' }
});
```
# Install # Install
To make it easy to generate, encode, and decode keys and certificates, To make it easy to generate, encode, and decode keys and certificates,
@ -234,9 +259,6 @@ is a required part of the process, which requires `set` and `remove` callbacks/p
```js ```js
var certinfo = await acme.certificates.create({ var certinfo = await acme.certificates.create({
agreeToTerms: function(tos) {
return tos;
},
account: account, account: account,
accountKey: accountPrivateJwk, accountKey: accountPrivateJwk,
csr: csr, csr: csr,

View File

@ -18,9 +18,11 @@ native._canCheck = function(me) {
}; };
native._dns01 = function(me, ch) { native._dns01 = function(me, ch) {
return new me.request({ return me
.request({
url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT' url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
}).then(function(resp) { })
.then(function(resp) {
var err; var err;
if (!resp.body || !Array.isArray(resp.body.answer)) { if (!resp.body || !Array.isArray(resp.body.answer)) {
err = new Error('failed to get DNS response'); err = new Error('failed to get DNS response');
@ -42,9 +44,11 @@ native._dns01 = function(me, ch) {
native._http01 = function(me, ch) { native._http01 = function(me, ch) {
var url = encodeURIComponent(ch.challengeUrl); var url = encodeURIComponent(ch.challengeUrl);
return new me.request({ return me
.request({
url: me._baseUrl + '/api/http?url=' + url url: me._baseUrl + '/api/http?url=' + url
}).then(function(resp) { })
.then(function(resp) {
return resp.body; return resp.body;
}); });
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
var UserAgent = module.exports; var UserAgent = module.exports;
UserAgent.get = function () { UserAgent.get = function() {
return false; return false;
}; };

View File

@ -3,6 +3,7 @@
var native = module.exports; var native = module.exports;
var promisify = require('util').promisify; var promisify = require('util').promisify;
var resolveTxt = promisify(require('dns').resolveTxt); var resolveTxt = promisify(require('dns').resolveTxt);
var crypto = require('crypto');
native._canCheck = function(me) { native._canCheck = function(me) {
me._canCheck = {}; me._canCheck = {};
@ -31,3 +32,57 @@ native._http01 = function(me, ch) {
return resp.body; return resp.body;
}); });
}; };
// the hashcash here is for browser parity only
// basically we ask the client to find a needle in a haystack
// (very similar to CloudFlare's api protection)
native._hashcash = function(ch) {
if (!ch || !ch.nonce) {
ch = { nonce: 'xxx' };
}
return Promise.resolve()
.then(function() {
// only get easy answers
var len = ch.needle.length;
var start = ch.start || 0;
var end = ch.end || Math.ceil(len / 2);
var window = parseInt(end - start, 10) || 0;
var maxLen = 6;
var maxTries = Math.pow(2, maxLen * 8);
if (
len > maxLen ||
window < Math.ceil(len / 2) ||
ch.needle.toLowerCase() !== ch.needle ||
ch.alg !== 'SHA-256'
) {
// bail unless the server is issuing very easy challenges
throw new Error('possible and easy answers only, please');
}
var haystack;
var i;
var answer;
var needle = Buffer.from(ch.needle, 'hex');
for (i = 0; i < maxTries; i += 1) {
answer = i.toString(16);
if (answer.length % 2) {
answer = '0' + answer;
}
haystack = crypto
.createHash('sha256')
.update(Buffer.from(ch.nonce + answer, 'hex'))
.digest()
.slice(ch.start, ch.end);
if (-1 !== haystack.indexOf(needle)) {
return ch.nonce + ':' + answer;
}
}
return ch.nonce + ':xxx';
})
.catch(function() {
//console.log('[debug]', err);
// ignore any error
return ch.nonce + ':xxx';
});
};

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
var os = require('os'); var os = require('os');
var ver = require('../../package.json'); var ver = require('../../package.json').version;
var UserAgent = module.exports; var UserAgent = module.exports;
UserAgent.get = function(me) { UserAgent.get = function(me) {

View File

@ -5,15 +5,5 @@ var promisify = require('util').promisify;
var request = promisify(require('@root/request')); var request = promisify(require('@root/request'));
http.request = function(opts) { http.request = function(opts) {
if (!opts.headers) {
opts.headers = {};
}
if (
!Object.keys(opts.headers).some(function(key) {
return 'user-agent' === key.toLowerCase();
})
) {
// TODO opts.headers['User-Agent'] = 'TODO';
}
return request(opts); return request(opts);
}; };

View File

@ -2,12 +2,14 @@
require('dotenv').config(); require('dotenv').config();
var pkg = require('../package.json');
var CSR = require('@root/csr'); var CSR = require('@root/csr');
var Enc = require('@root/encoding/base64'); var Enc = require('@root/encoding/base64');
var PEM = require('@root/pem'); var PEM = require('@root/pem');
var punycode = require('punycode'); var punycode = require('punycode');
var ACME = require('../acme.js'); var ACME = require('../acme.js');
var Keypairs = require('@root/keypairs'); var Keypairs = require('@root/keypairs');
var ecJwk = require('../fixtures/account.jwk.json');
// TODO exec npm install --save-dev CHALLENGE_MODULE // TODO exec npm install --save-dev CHALLENGE_MODULE
if (!process.env.CHALLENGE_OPTIONS) { if (!process.env.CHALLENGE_OPTIONS) {
@ -36,6 +38,7 @@ module.exports = function() {
var acme = ACME.create({ var acme = ACME.create({
// debug: true // debug: true
maintainerEmail: config.email, maintainerEmail: config.email,
packageAgent: 'test-' + pkg.name + '/' + pkg.version,
notify: function(ev, params) { notify: function(ev, params) {
console.info( console.info(
'\t' + ev, '\t' + ev,
@ -104,6 +107,10 @@ module.exports = function() {
} }
var accountKeypair = await Keypairs.generate({ kty: accKty }); var accountKeypair = await Keypairs.generate({ kty: accKty });
if (/EC/i.test(accKty)) {
// to test that an existing account gets back data
accountKeypair = ecJwk;
}
var accountKey = accountKeypair.private; var accountKey = accountKeypair.private;
if (config.debug) { if (config.debug) {
console.info('Account Key Created'); console.info('Account Key Created');

74
tests/maintainer.js Normal file
View File

@ -0,0 +1,74 @@
'use strict';
var native = require('../lib/native.js');
var crypto = require('crypto');
native
._hashcash({
alg: 'SHA-256',
nonce: '00',
needle: '0000',
start: 0,
end: 2
})
.then(function(hashcash) {
if ('00:76de' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: known hash solves correctly');
return native
._hashcash({
alg: 'SHA-256',
nonce: '10',
needle: '',
start: 0,
end: 2
})
.then(function(hashcash) {
if ('10:00' !== hashcash) {
throw new Error('hashcash algorthim changed');
}
console.info('PASS: empty hash solves correctly');
var now = Date.now();
var nonce = '20';
var needle = crypto
.randomBytes(3)
.toString('hex')
.slice(0, 5);
native
._hashcash({
alg: 'SHA-256',
nonce: nonce,
needle: needle,
start: 0,
end: Math.ceil(needle.length / 2)
})
.then(function(hashcash) {
var later = Date.now();
var parts = hashcash.split(':');
var answer = parts[1];
if (parts[0] !== nonce) {
throw new Error('incorrect nonce');
}
var haystack = crypto
.createHash('sha256')
.update(Buffer.from(nonce + answer, 'hex'))
.digest()
.slice(0, Math.ceil(needle.length / 2));
if (
-1 === haystack.indexOf(Buffer.from(needle, 'hex'))
) {
throw new Error('incorrect solution');
}
if (later - now > 2000) {
throw new Error('took too long to solve');
}
console.info(
'PASS: rando hash solves correctly (and in good time - %dms)',
later - now
);
});
});
});

View File

@ -82,26 +82,25 @@ U._request = function(me, opts) {
if (ua && !opts.headers['User-Agent']) { if (ua && !opts.headers['User-Agent']) {
opts.headers['User-Agent'] = ua; opts.headers['User-Agent'] = ua;
} }
if (opts.json && true !== opts.json) { if (opts.json) {
opts.headers['Content-Type'] = 'application/jose+json'; opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json); opts.body = JSON.stringify(opts.json);
} }
if (/*opts.jose ||*/ opts.json.protected) {
opts.headers['Content-Type'] = 'application/jose+json';
}
}
if (!opts.method) { if (!opts.method) {
opts.method = 'GET'; opts.method = 'GET';
if (opts.body) { if (opts.body) {
opts.method = 'POST'; opts.method = 'POST';
} }
} }
if (opts.json) {
opts.headers.Accept = 'application/json';
if (true !== opts.json) {
opts.body = JSON.stringify(opts.json);
}
}
//console.log('\n[debug] REQUEST'); //console.log('\n[debug] REQUEST');
//console.log(opts); //console.log(opts);
return me.request(opts).then(function(resp) { return me.__request(opts).then(function(resp) {
if (resp.toJSON) { if (resp.toJSON) {
resp = resp.toJSON(); resp = resp.toJSON();
} }