replace debug() with our custom logger
mostly we want trace() and log(). trace() can be enabled whenever we want by flipping a flag and restarting box
This commit is contained in:
+42
-42
@@ -2,7 +2,7 @@ import assert from 'node:assert';
|
||||
import blobs from './blobs.js';
|
||||
import BoxError from './boxerror.js';
|
||||
import crypto from 'node:crypto';
|
||||
import debugModule from 'debug';
|
||||
import logger from './logger.js';
|
||||
import dns from './dns.js';
|
||||
import openssl from './openssl.js';
|
||||
import path from 'node:path';
|
||||
@@ -12,7 +12,7 @@ import safe from 'safetydance';
|
||||
import superagent from '@cloudron/superagent';
|
||||
import users from './users.js';
|
||||
|
||||
const debug = debugModule('box:cert/acme2');
|
||||
const { log, trace } = logger('cert/acme2');
|
||||
|
||||
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
@@ -53,7 +53,7 @@ function Acme2(fqdn, domainObject, email, key, options) {
|
||||
|
||||
this.profile = options.profile || ''; // https://letsencrypt.org/docs/profiles/ . is validated against the directory
|
||||
|
||||
debug(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.forceHttpAuthorization}`);
|
||||
log(`Acme2: will get cert for fqdn: ${this.fqdn} cn: ${this.cn} certName: ${this.certName} wildcard: ${this.wildcard} http: ${this.forceHttpAuthorization}`);
|
||||
}
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
@@ -140,7 +140,7 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) {
|
||||
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
|
||||
if (!nonce) throw new BoxError(BoxError.ACME_ERROR, 'No nonce in response');
|
||||
|
||||
debug(`sendSignedRequest: using nonce ${nonce} for url ${url}`);
|
||||
log(`sendSignedRequest: using nonce ${nonce} for url ${url}`);
|
||||
|
||||
const protected64 = b64(JSON.stringify(Object.assign({}, header, { nonce: nonce })));
|
||||
|
||||
@@ -168,7 +168,7 @@ Acme2.prototype.postAsGet = async function (url) {
|
||||
Acme2.prototype.updateContact = async function (registrationUri) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
|
||||
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
|
||||
log(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
|
||||
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
const payload = {
|
||||
@@ -178,7 +178,7 @@ Acme2.prototype.updateContact = async function (registrationUri) {
|
||||
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
|
||||
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${result.text}`);
|
||||
|
||||
debug(`updateContact: contact of user updated to ${this.email}`);
|
||||
log(`updateContact: contact of user updated to ${this.email}`);
|
||||
};
|
||||
|
||||
Acme2.prototype.ensureAccount = async function () {
|
||||
@@ -186,18 +186,18 @@ Acme2.prototype.ensureAccount = async function () {
|
||||
termsOfServiceAgreed: true
|
||||
};
|
||||
|
||||
debug('ensureAccount: registering user');
|
||||
log('ensureAccount: registering user');
|
||||
|
||||
this.accountKey = await blobs.getString(blobs.ACME_ACCOUNT_KEY);
|
||||
if (!this.accountKey) {
|
||||
debug('ensureAccount: generating new account keys');
|
||||
log('ensureAccount: generating new account keys');
|
||||
this.accountKey = await openssl.generateKey('rsa4096');
|
||||
await blobs.setString(blobs.ACME_ACCOUNT_KEY, this.accountKey);
|
||||
}
|
||||
|
||||
let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
|
||||
if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') {
|
||||
debug(`ensureAccount: key was revoked. ${result.status} ${result.text}. generating new account key`);
|
||||
log(`ensureAccount: key was revoked. ${result.status} ${result.text}. generating new account key`);
|
||||
this.accountKey = await openssl.generateKey('rsa4096');
|
||||
await blobs.setString(blobs.ACME_ACCOUNT_KEY, this.accountKey);
|
||||
result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
|
||||
@@ -206,7 +206,7 @@ Acme2.prototype.ensureAccount = async function () {
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.status !== 200 && result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${result.text}`);
|
||||
|
||||
debug(`ensureAccount: user registered keyid: ${result.headers.location}`);
|
||||
log(`ensureAccount: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
this.accountKeyId = result.headers.location;
|
||||
|
||||
@@ -223,14 +223,14 @@ Acme2.prototype.newOrder = async function () {
|
||||
});
|
||||
});
|
||||
|
||||
debug(`newOrder: ${JSON.stringify(this.altNames)}`);
|
||||
log(`newOrder: ${JSON.stringify(this.altNames)}`);
|
||||
|
||||
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
|
||||
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
|
||||
if (result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to send new order. Expecting 201, got ${result.status} ${result.text}`);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
debug(`newOrder: created order ${this.cn} order: ${result.text} orderUrl: ${orderUrl}`);
|
||||
log(`newOrder: created order ${this.cn} order: ${result.text} orderUrl: ${orderUrl}`);
|
||||
|
||||
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.ACME_ERROR, 'invalid authorizations in order');
|
||||
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid finalize in order');
|
||||
@@ -242,18 +242,18 @@ Acme2.prototype.newOrder = async function () {
|
||||
Acme2.prototype.waitForOrder = async function (orderUrl) {
|
||||
assert.strictEqual(typeof orderUrl, 'string');
|
||||
|
||||
debug(`waitForOrder: ${orderUrl}`);
|
||||
log(`waitForOrder: ${orderUrl}`);
|
||||
|
||||
return await promiseRetry({ times: 15, interval: 20000, debug }, async () => {
|
||||
debug('waitForOrder: getting status');
|
||||
return await promiseRetry({ times: 15, interval: 20000, debug: log }, async () => {
|
||||
log('waitForOrder: getting status');
|
||||
|
||||
const result = await this.postAsGet(orderUrl);
|
||||
if (result.status !== 200) {
|
||||
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
|
||||
log(`waitForOrder: invalid response code getting uri ${result.status}`);
|
||||
throw new BoxError(BoxError.ACME_ERROR, `Bad response when waiting for order. code: ${result.status}`);
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
log('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.ACME_ERROR, `Request is in ${result.body.status} state`);
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
|
||||
@@ -279,7 +279,7 @@ Acme2.prototype.getKeyAuthorization = async function (token) {
|
||||
Acme2.prototype.notifyChallengeReady = async function (challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
|
||||
|
||||
debug(`notifyChallengeReady: ${challenge.url} was met`);
|
||||
log(`notifyChallengeReady: ${challenge.url} was met`);
|
||||
|
||||
const keyAuthorization = await this.getKeyAuthorization(challenge.token);
|
||||
|
||||
@@ -295,18 +295,18 @@ Acme2.prototype.notifyChallengeReady = async function (challenge) {
|
||||
Acme2.prototype.waitForChallenge = async function (challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`waitingForChallenge: ${JSON.stringify(challenge)}`);
|
||||
log(`waitingForChallenge: ${JSON.stringify(challenge)}`);
|
||||
|
||||
await promiseRetry({ times: 15, interval: 20000, debug }, async () => {
|
||||
debug('waitingForChallenge: getting status');
|
||||
await promiseRetry({ times: 15, interval: 20000, debug: log }, async () => {
|
||||
log('waitingForChallenge: getting status');
|
||||
|
||||
const result = await this.postAsGet(challenge.url);
|
||||
if (result.status !== 200) {
|
||||
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
|
||||
log(`waitForChallenge: invalid response code getting uri ${result.status}`);
|
||||
throw new BoxError(BoxError.ACME_ERROR, `Bad response code when waiting for challenge : ${result.status}`);
|
||||
}
|
||||
|
||||
debug(`waitForChallenge: status is "${result.body.status}" "${result.text}"`);
|
||||
log(`waitForChallenge: status is "${result.body.status}" "${result.text}"`);
|
||||
|
||||
if (result.body.status === 'pending') throw new BoxError(BoxError.ACME_ERROR, 'Challenge is in pending state');
|
||||
else if (result.body.status === 'valid') return;
|
||||
@@ -325,7 +325,7 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) {
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug(`signCertificate: sending sign request to ${finalizationUrl}`);
|
||||
log(`signCertificate: sending sign request to ${finalizationUrl}`);
|
||||
|
||||
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
@@ -335,8 +335,8 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) {
|
||||
Acme2.prototype.downloadCertificate = async function (certUrl) {
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
|
||||
return await promiseRetry({ times: 5, interval: 20000, debug }, async () => {
|
||||
debug(`downloadCertificate: downloading certificate of ${this.cn}`);
|
||||
return await promiseRetry({ times: 5, interval: 20000, debug: log }, async () => {
|
||||
log(`downloadCertificate: downloading certificate of ${this.cn}`);
|
||||
|
||||
const result = await this.postAsGet(certUrl);
|
||||
if (result.status === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate');
|
||||
@@ -350,12 +350,12 @@ Acme2.prototype.downloadCertificate = async function (certUrl) {
|
||||
Acme2.prototype.prepareHttpChallenge = async function (challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`prepareHttpChallenge: preparing for challenge ${JSON.stringify(challenge)}`);
|
||||
log(`prepareHttpChallenge: preparing for challenge ${JSON.stringify(challenge)}`);
|
||||
|
||||
const keyAuthorization = await this.getKeyAuthorization(challenge.token);
|
||||
|
||||
const challengeFilePath = path.join(paths.ACME_CHALLENGES_DIR, challenge.token);
|
||||
debug(`prepareHttpChallenge: writing ${keyAuthorization} to ${challengeFilePath}`);
|
||||
log(`prepareHttpChallenge: writing ${keyAuthorization} to ${challengeFilePath}`);
|
||||
|
||||
if (!safe.fs.writeFileSync(challengeFilePath, keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`);
|
||||
};
|
||||
@@ -364,7 +364,7 @@ Acme2.prototype.cleanupHttpChallenge = async function (challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
const challengeFilePath = path.join(paths.ACME_CHALLENGES_DIR, challenge.token);
|
||||
debug(`cleanupHttpChallenge: unlinking ${challengeFilePath}`);
|
||||
log(`cleanupHttpChallenge: unlinking ${challengeFilePath}`);
|
||||
|
||||
if (!safe.fs.unlinkSync(challengeFilePath)) throw new BoxError(BoxError.FS_ERROR, `Error unlinking challenge: ${safe.error.message}`);
|
||||
};
|
||||
@@ -381,7 +381,7 @@ function getChallengeSubdomain(cn, domain) {
|
||||
challengeSubdomain = '_acme-challenge.' + cn.slice(0, -domain.length - 1);
|
||||
}
|
||||
|
||||
debug(`getChallengeSubdomain: challenge subdomain for cn ${cn} at domain ${domain} is ${challengeSubdomain}`);
|
||||
log(`getChallengeSubdomain: challenge subdomain for cn ${cn} at domain ${domain} is ${challengeSubdomain}`);
|
||||
|
||||
return challengeSubdomain;
|
||||
}
|
||||
@@ -389,7 +389,7 @@ function getChallengeSubdomain(cn, domain) {
|
||||
Acme2.prototype.prepareDnsChallenge = async function (cn, challenge) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`prepareDnsChallenge: preparing for challenge: ${JSON.stringify(challenge)}`);
|
||||
log(`prepareDnsChallenge: preparing for challenge: ${JSON.stringify(challenge)}`);
|
||||
|
||||
const keyAuthorization = await this.getKeyAuthorization(challenge.token);
|
||||
const shasum = crypto.createHash('sha256');
|
||||
@@ -398,7 +398,7 @@ Acme2.prototype.prepareDnsChallenge = async function (cn, challenge) {
|
||||
const txtValue = urlBase64Encode(shasum.digest('base64'));
|
||||
const challengeSubdomain = getChallengeSubdomain(cn, this.domain);
|
||||
|
||||
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
log(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
await dns.upsertDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]);
|
||||
|
||||
@@ -416,7 +416,7 @@ Acme2.prototype.cleanupDnsChallenge = async function (cn, challenge) {
|
||||
const txtValue = urlBase64Encode(shasum.digest('base64'));
|
||||
const challengeSubdomain = getChallengeSubdomain(cn, this.domain);
|
||||
|
||||
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
log(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
await dns.removeDnsRecords(challengeSubdomain, this.domain, 'TXT', [ `"${txtValue}"` ]);
|
||||
};
|
||||
@@ -425,7 +425,7 @@ Acme2.prototype.prepareChallenge = async function (cn, authorization) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
|
||||
debug(`prepareChallenge: http: ${this.forceHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
|
||||
log(`prepareChallenge: http: ${this.forceHttpAuthorization} cn: ${cn} authorization: ${JSON.stringify(authorization)}`);
|
||||
|
||||
// validation is cached by LE for 60 days or so. if a user switches from non-wildcard DNS (http challenge) to programmatic DNS (dns challenge), then
|
||||
// LE remembers the challenge type and won't give us a dns challenge for 60 days!
|
||||
@@ -447,7 +447,7 @@ Acme2.prototype.cleanupChallenge = async function (cn, challenge) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
|
||||
debug(`cleanupChallenge: http: ${this.forceHttpAuthorization}`);
|
||||
log(`cleanupChallenge: http: ${this.forceHttpAuthorization}`);
|
||||
|
||||
if (this.forceHttpAuthorization) {
|
||||
await this.cleanupHttpChallenge(challenge);
|
||||
@@ -461,7 +461,7 @@ Acme2.prototype.acmeFlow = async function () {
|
||||
const { order, orderUrl } = await this.newOrder();
|
||||
|
||||
for (const authorizationUrl of order.authorizations) {
|
||||
debug(`acmeFlow: authorizing ${authorizationUrl}`);
|
||||
log(`acmeFlow: authorizing ${authorizationUrl}`);
|
||||
|
||||
const response = await this.postAsGet(authorizationUrl);
|
||||
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code getting authorization : ${response.status}`);
|
||||
@@ -471,7 +471,7 @@ Acme2.prototype.acmeFlow = async function () {
|
||||
const challenge = await this.prepareChallenge(cn, authorization);
|
||||
await this.notifyChallengeReady(challenge);
|
||||
await this.waitForChallenge(challenge);
|
||||
await safe(this.cleanupChallenge(cn, challenge), { debug });
|
||||
await safe(this.cleanupChallenge(cn, challenge), { debug: log });
|
||||
}
|
||||
|
||||
const csr = await openssl.createCsr(this.key, this.cn, this.altNames);
|
||||
@@ -485,7 +485,7 @@ Acme2.prototype.acmeFlow = async function () {
|
||||
};
|
||||
|
||||
Acme2.prototype.loadDirectory = async function () {
|
||||
await promiseRetry({ times: 3, interval: 20000, debug }, async () => {
|
||||
await promiseRetry({ times: 3, interval: 20000, debug: log }, async () => {
|
||||
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
|
||||
|
||||
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching directory : ${response.status}`);
|
||||
@@ -502,11 +502,11 @@ Acme2.prototype.loadDirectory = async function () {
|
||||
};
|
||||
|
||||
Acme2.prototype.getCertificate = async function () {
|
||||
debug(`getCertificate: start acme flow for ${this.cn} from ${this.caDirectory}`);
|
||||
log(`getCertificate: start acme flow for ${this.cn} from ${this.caDirectory}`);
|
||||
|
||||
await this.loadDirectory();
|
||||
const result = await this.acmeFlow(); // { key, cert, csr, renewalInfo }
|
||||
debug(`getCertificate: acme flow completed for ${this.cn}. renewalInfo: ${JSON.stringify(result.renewalInfo)}`);
|
||||
log(`getCertificate: acme flow completed for ${this.cn}. renewalInfo: ${JSON.stringify(result.renewalInfo)}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -522,8 +522,8 @@ async function getCertificate(fqdn, domainObject, key) {
|
||||
const owner = await users.getOwner();
|
||||
const email = owner?.email || 'webmaster@cloudron.io'; // can error if not activated yet
|
||||
|
||||
return await promiseRetry({ times: 3, interval: 0, debug }, async function () {
|
||||
debug(`getCertificate: for fqdn ${fqdn} and domain ${domainObject.domain}`);
|
||||
return await promiseRetry({ times: 3, interval: 0, debug: log }, async function () {
|
||||
log(`getCertificate: for fqdn ${fqdn} and domain ${domainObject.domain}`);
|
||||
|
||||
const acme = new Acme2(fqdn, domainObject, email, key, { /* profile: 'shortlived' */ });
|
||||
return await acme.getCertificate();
|
||||
|
||||
Reference in New Issue
Block a user