forked from root/acme.js
request cleanup
This commit is contained in:
parent
54cda5a888
commit
b39a3763cf
30
README.md
30
README.md
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
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
|
||||
|
||||
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
|
||||
var certinfo = await acme.certificates.create({
|
||||
agreeToTerms: function(tos) {
|
||||
return tos;
|
||||
},
|
||||
account: account,
|
||||
accountKey: accountPrivateJwk,
|
||||
csr: csr,
|
||||
|
|
|
@ -18,33 +18,37 @@ native._canCheck = function(me) {
|
|||
};
|
||||
|
||||
native._dns01 = function(me, ch) {
|
||||
return new me.request({
|
||||
url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
|
||||
}).then(function(resp) {
|
||||
var err;
|
||||
if (!resp.body || !Array.isArray(resp.body.answer)) {
|
||||
err = new Error('failed to get DNS response');
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
if (!resp.body.answer.length) {
|
||||
err = new Error('failed to get DNS answer record in response');
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
answer: resp.body.answer.map(function(ans) {
|
||||
return { data: ans.data, ttl: ans.ttl };
|
||||
})
|
||||
};
|
||||
});
|
||||
return me
|
||||
.request({
|
||||
url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT'
|
||||
})
|
||||
.then(function(resp) {
|
||||
var err;
|
||||
if (!resp.body || !Array.isArray(resp.body.answer)) {
|
||||
err = new Error('failed to get DNS response');
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
if (!resp.body.answer.length) {
|
||||
err = new Error('failed to get DNS answer record in response');
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
answer: resp.body.answer.map(function(ans) {
|
||||
return { data: ans.data, ttl: ans.ttl };
|
||||
})
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
native._http01 = function(me, ch) {
|
||||
var url = encodeURIComponent(ch.challengeUrl);
|
||||
return new me.request({
|
||||
url: me._baseUrl + '/api/http?url=' + url
|
||||
}).then(function(resp) {
|
||||
return resp.body;
|
||||
});
|
||||
return me
|
||||
.request({
|
||||
url: me._baseUrl + '/api/http?url=' + url
|
||||
})
|
||||
.then(function(resp) {
|
||||
return resp.body;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
var UserAgent = module.exports;
|
||||
UserAgent.get = function () {
|
||||
return false;
|
||||
UserAgent.get = function() {
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
var native = module.exports;
|
||||
var promisify = require('util').promisify;
|
||||
var resolveTxt = promisify(require('dns').resolveTxt);
|
||||
var crypto = require('crypto');
|
||||
|
||||
native._canCheck = function(me) {
|
||||
me._canCheck = {};
|
||||
|
@ -31,3 +32,57 @@ native._http01 = function(me, ch) {
|
|||
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';
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var os = require('os');
|
||||
var ver = require('../../package.json');
|
||||
var ver = require('../../package.json').version;
|
||||
|
||||
var UserAgent = module.exports;
|
||||
UserAgent.get = function(me) {
|
||||
|
|
|
@ -5,15 +5,5 @@ var promisify = require('util').promisify;
|
|||
var request = promisify(require('@root/request'));
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
require('dotenv').config();
|
||||
|
||||
var pkg = require('../package.json');
|
||||
var CSR = require('@root/csr');
|
||||
var Enc = require('@root/encoding/base64');
|
||||
var PEM = require('@root/pem');
|
||||
var punycode = require('punycode');
|
||||
var ACME = require('../acme.js');
|
||||
var Keypairs = require('@root/keypairs');
|
||||
var ecJwk = require('../fixtures/account.jwk.json');
|
||||
|
||||
// TODO exec npm install --save-dev CHALLENGE_MODULE
|
||||
if (!process.env.CHALLENGE_OPTIONS) {
|
||||
|
@ -36,6 +38,7 @@ module.exports = function() {
|
|||
var acme = ACME.create({
|
||||
// debug: true
|
||||
maintainerEmail: config.email,
|
||||
packageAgent: 'test-' + pkg.name + '/' + pkg.version,
|
||||
notify: function(ev, params) {
|
||||
console.info(
|
||||
'\t' + ev,
|
||||
|
@ -104,6 +107,10 @@ module.exports = function() {
|
|||
}
|
||||
|
||||
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;
|
||||
if (config.debug) {
|
||||
console.info('Account Key Created');
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
19
utils.js
19
utils.js
|
@ -82,9 +82,14 @@ U._request = function(me, opts) {
|
|||
if (ua && !opts.headers['User-Agent']) {
|
||||
opts.headers['User-Agent'] = ua;
|
||||
}
|
||||
if (opts.json && true !== opts.json) {
|
||||
opts.headers['Content-Type'] = 'application/jose+json';
|
||||
opts.body = JSON.stringify(opts.json);
|
||||
if (opts.json) {
|
||||
opts.headers.Accept = 'application/json';
|
||||
if (true !== opts.json) {
|
||||
opts.body = JSON.stringify(opts.json);
|
||||
}
|
||||
if (/*opts.jose ||*/ opts.json.protected) {
|
||||
opts.headers['Content-Type'] = 'application/jose+json';
|
||||
}
|
||||
}
|
||||
if (!opts.method) {
|
||||
opts.method = 'GET';
|
||||
|
@ -92,16 +97,10 @@ U._request = function(me, opts) {
|
|||
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(opts);
|
||||
return me.request(opts).then(function(resp) {
|
||||
return me.__request(opts).then(function(resp) {
|
||||
if (resp.toJSON) {
|
||||
resp = resp.toJSON();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue