diff --git a/CHANGES b/CHANGES index de41f29a6..ed994761d 100644 --- a/CHANGES +++ b/CHANGES @@ -1745,4 +1745,5 @@ * import: add option to import app from arbitrary backup config * Show download progress for rsync backups * Fix various repair workflows +* acme2: Implement post-as-get diff --git a/src/cert/acme2.js b/src/cert/acme2.js index 5910f0b8f..aec3f0c44 100644 --- a/src/cert/acme2.js +++ b/src/cert/acme2.js @@ -9,8 +9,8 @@ var assert = require('assert'), fs = require('fs'), path = require('path'), paths = require('../paths.js'), + request = require('request'), safe = require('safetydance'), - superagent = require('superagent'), util = require('util'), _ = require('underscore'); @@ -41,15 +41,6 @@ function Acme2(options) { this.wildcard = !!options.wildcard; } -Acme2.prototype.getNonce = function (callback) { - superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) { - if (error && !error.response) return callback(error); - if (response.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching nonce : ' + response.statusCode)); - - return callback(null, response.headers['Replay-Nonce'.toLowerCase()]); - }); -}; - // urlsafe base64 encoding (jose) function urlBase64Encode(string) { return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); @@ -96,8 +87,12 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) { var payload64 = b64(payload); - this.getNonce(function (error, nonce) { - if (error) return callback(error); + request.get(this.directory.newNonce, { json: true, timeout: 30000 }, function (error, response) { + if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`)); + if (response.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching nonce : ' + response.statusCode)); + + const nonce = response.headers['Replay-Nonce'.toLowerCase()]; + if (!nonce) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response')); debug('sendSignedRequest: using nonce %s for url %s', nonce, url); @@ -113,14 +108,23 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) { signature: signature64 }; - superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) { - if (error && !error.response) return callback(error); // network errors + request.post(url, { headers: { 'Content-Type': 'application/jose+json', 'User-Agent': 'acme-cloudron' }, body: JSON.stringify(data), timeout: 30000 }, function (error, response) { + if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`)); // network error - callback(null, res); + // we don't set json: true in request because it ends up mangling the content-type + // we don't set json: true in request because it ends up mangling the content-type + if (response.headers['content-type'] === 'application/json') response.body = safe.JSON.parse(response.body); + + callback(null, response); }); }); }; +// https://tools.ietf.org/html/rfc8555#section-6.3 +Acme2.prototype.postAsGet = function (url, callback) { + this.sendSignedRequest(url, '', callback); +}; + Acme2.prototype.updateContact = function (registrationUri, callback) { assert.strictEqual(typeof registrationUri, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -134,7 +138,7 @@ Acme2.prototype.updateContact = function (registrationUri, callback) { const that = this; this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) { - if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when updating contact: ${error.message}`)); + if (error) return callback(error); if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text))); debug(`updateContact: contact of user updated to ${that.email}`); @@ -154,7 +158,7 @@ Acme2.prototype.registerUser = function (callback) { var that = this; this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) { - if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when registering user: ${error.message}`)); + if (error) return callback(error); // 200 if already exists. 201 for new accounts if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text))); @@ -180,7 +184,7 @@ Acme2.prototype.newOrder = function (domain, callback) { debug('newOrder: %s', domain); this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) { - if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when creating new order: ${error.message}`)); + if (error) return callback(error); if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`)); if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text))); @@ -201,14 +205,15 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) { assert.strictEqual(typeof callback, 'function'); debug(`waitForOrder: ${orderUrl}`); + const that = this; async.retry({ times: 15, interval: 20000 }, function (retryCallback) { debug('waitForOrder: getting status'); - superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) { + that.postAsGet(orderUrl, function (error, result) { + if (error) { debug('waitForOrder: network error getting uri %s', orderUrl); - return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for order: ${error.message}`)); // network error + return retryCallback(error); } if (result.statusCode !== 200) { debug('waitForOrder: invalid response code getting uri %s', result.statusCode); @@ -253,7 +258,7 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) { }; this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) { - if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when notifying challenge: ${error.message}`)); + if (error) return callback(error); if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text))); callback(); @@ -265,14 +270,15 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) { assert.strictEqual(typeof callback, 'function'); debug('waitingForChallenge: %j', challenge); + const that = this; async.retry({ times: 15, interval: 20000 }, function (retryCallback) { debug('waitingForChallenge: getting status'); - superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) { + that.postAsGet(challenge.url, function (error, result) { + if (error) { debug('waitForChallenge: network error getting uri %s', challenge.url); - return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for challenge: ${error.message}`)); + return retryCallback(error); } if (result.statusCode !== 200) { debug('waitForChallenge: invalid response code getting uri %s', result.statusCode); @@ -305,7 +311,7 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal debug('signCertificate: sending sign request'); this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) { - if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when signing certificate: ${error.message}`)); + if (error) return callback(error); // 429 means we reached the cert limit for this domain if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text))); @@ -348,20 +354,17 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) { assert.strictEqual(typeof callback, 'function'); var outdir = paths.APP_CERTS_DIR; + const that = this; async.retry({ times: 5, interval: 20000 }, function (retryCallback) { debug('downloadCertificate: downloading certificate'); - superagent.get(certUrl).buffer().parse(function (res, done) { - var data = [ ]; - res.on('data', function(chunk) { data.push(chunk); }); - res.on('end', function () { res.text = Buffer.concat(data); done(); }); - }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`)); - if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry')); + that.postAsGet(certUrl, function (error, result) { + if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`)); + if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate')); if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text))); - const fullChainPem = result.text; + const fullChainPem = result.body; // buffer const certName = hostname.replace('*.', '_.'); var certificateFile = path.join(outdir, `${certName}.cert`); @@ -488,8 +491,8 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, debug(`prepareChallenge: http: ${this.performHttpAuthorization}`); const that = this; - superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when preparing challenge: ${error.message}`)); + this.postAsGet(authorizationUrl, function (error, response) { + if (error) return callback(error); if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode)); const authorization = response.body; @@ -569,8 +572,8 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) { Acme2.prototype.getDirectory = function (callback) { const that = this; - superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`)); + request.get(this.caDirectory, { json: true, timeout: 30000 }, function (error, response) { + if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching directory : ' + response.statusCode)); if (typeof response.body.newNonce !== 'string' ||