Skip to content

Commit db23c9a

Browse files
committed
Refactor certbot plugins install
- Added a script to install every single plugin, used in development and debugging - Improved certbot plugin install commands - Adjusted some version for plugins to install properly - It's noted that some plugins require deps that do not match other plugins, however these use cases should be extremely rare
1 parent 8646cb5 commit db23c9a

File tree

17 files changed

+703
-767
lines changed

17 files changed

+703
-767
lines changed

backend/internal/certificate.js

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ const error = require('../lib/error');
99
const utils = require('../lib/utils');
1010
const certificateModel = require('../models/certificate');
1111
const tokenModel = require('../models/token');
12-
const dnsPlugins = require('../global/certbot-dns-plugins');
12+
const dnsPlugins = require('../global/certbot-dns-plugins.json');
1313
const internalAuditLog = require('./audit-log');
1414
const internalNginx = require('./nginx');
1515
const internalHost = require('./host');
16+
const certbot = require('../lib/certbot');
1617
const archiver = require('archiver');
1718
const path = require('path');
1819
const { isArray } = require('lodash');
@@ -849,26 +850,20 @@ const internalCertificate = {
849850

850851
/**
851852
* @param {Object} certificate the certificate row
852-
* @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`)
853+
* @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.json`)
853854
* @param {String | null} credentials the content of this providers credentials file
854-
* @param {String} propagation_seconds the cloudflare api token
855+
* @param {String} propagation_seconds
855856
* @returns {Promise}
856857
*/
857-
requestLetsEncryptSslWithDnsChallenge: (certificate) => {
858-
const dns_plugin = dnsPlugins[certificate.meta.dns_provider];
859-
860-
if (!dns_plugin) {
861-
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
862-
}
863-
864-
logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
858+
requestLetsEncryptSslWithDnsChallenge: async (certificate) => {
859+
await certbot.installPlugin(certificate.meta.dns_provider);
860+
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
861+
logger.info(`Requesting Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
865862

866863
const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
867864
// Escape single quotes and backslashes
868865
const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\');
869866
const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\'';
870-
// we call `. /opt/certbot/bin/activate` (`.` is alternative to `source` in dash) to access certbot venv
871-
const prepareCmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies + ' && deactivate';
872867

873868
// Whether the plugin has a --<name>-credentials argument
874869
const hasConfigArg = certificate.meta.dns_provider !== 'route53';
@@ -881,15 +876,15 @@ const internalCertificate = {
881876
'--agree-tos ' +
882877
'--email "' + certificate.meta.letsencrypt_email + '" ' +
883878
'--domains "' + certificate.domain_names.join(',') + '" ' +
884-
'--authenticator ' + dns_plugin.full_plugin_name + ' ' +
879+
'--authenticator ' + dnsPlugin.full_plugin_name + ' ' +
885880
(
886881
hasConfigArg
887-
? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
882+
? '--' + dnsPlugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
888883
: ''
889884
) +
890885
(
891886
certificate.meta.propagation_seconds !== undefined
892-
? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
887+
? ' --' + dnsPlugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
893888
: ''
894889
) +
895890
(letsencryptStaging ? ' --staging' : '');
@@ -903,24 +898,19 @@ const internalCertificate = {
903898
mainCmd = mainCmd + ' --dns-duckdns-no-txt-restore';
904899
}
905900

906-
logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`);
907-
908-
return utils.exec(credentialsCmd)
909-
.then(() => {
910-
return utils.exec(prepareCmd)
911-
.then(() => {
912-
return utils.exec(mainCmd)
913-
.then(async (result) => {
914-
logger.info(result);
915-
return result;
916-
});
917-
});
918-
}).catch(async (err) => {
919-
// Don't fail if file does not exist
920-
const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`;
921-
await utils.exec(delete_credentialsCmd);
922-
throw err;
923-
});
901+
logger.info('Command:', `${credentialsCmd} && && ${mainCmd}`);
902+
903+
try {
904+
await utils.exec(credentialsCmd);
905+
const result = await utils.exec(mainCmd);
906+
logger.info(result);
907+
return result;
908+
} catch (err) {
909+
// Don't fail if file does not exist
910+
const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`;
911+
await utils.exec(delete_credentialsCmd);
912+
throw err;
913+
}
924914
},
925915

926916

@@ -999,13 +989,13 @@ const internalCertificate = {
999989
* @returns {Promise}
1000990
*/
1001991
renewLetsEncryptSslWithDnsChallenge: (certificate) => {
1002-
const dns_plugin = dnsPlugins[certificate.meta.dns_provider];
992+
const dnsPlugin = dnsPlugins[certificate.meta.dns_provider];
1003993

1004-
if (!dns_plugin) {
994+
if (!dnsPlugin) {
1005995
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
1006996
}
1007997

1008-
logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
998+
logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
1009999

10101000
let mainCmd = certbotCommand + ' renew --force-renewal ' +
10111001
'--config "' + letsencryptConfig + '" ' +

backend/lib/certbot.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const dnsPlugins = require('../global/certbot-dns-plugins.json');
2+
const utils = require('./utils');
3+
const error = require('./error');
4+
const logger = require('../logger').certbot;
5+
6+
// const letsencryptStaging = config.useLetsencryptStaging();
7+
// const letsencryptConfig = '/etc/letsencrypt.ini';
8+
// const certbotCommand = 'certbot';
9+
10+
// const acmeVersion = '1.32.0';
11+
const CERTBOT_VERSION_REPLACEMENT = '$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')';
12+
13+
const certbot = {
14+
15+
/**
16+
* Installs a cerbot plugin given the key for the object from
17+
* ../global/certbot-dns-plugins.json
18+
*
19+
* @param {string} pluginKey
20+
* @returns {Object}
21+
*/
22+
installPlugin: async function (pluginKey) {
23+
if (typeof dnsPlugins[pluginKey] === 'undefined') {
24+
// throw Error(`Certbot plugin ${pluginKey} not found`);
25+
throw new error.ItemNotFoundError(pluginKey);
26+
}
27+
28+
const plugin = dnsPlugins[pluginKey];
29+
logger.start(`Installing ${pluginKey}...`);
30+
31+
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
32+
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
33+
34+
const cmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir ' + plugin.dependencies + ' ' + plugin.package_name + plugin.version + ' ' + ' && deactivate';
35+
return utils.exec(cmd)
36+
.then((result) => {
37+
logger.complete(`Installed ${pluginKey}`);
38+
return result;
39+
})
40+
.catch((err) => {
41+
throw err;
42+
});
43+
},
44+
};
45+
46+
module.exports = certbot;

backend/lib/error.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,16 @@ module.exports = {
8282
this.message = message;
8383
this.public = false;
8484
this.status = 400;
85-
}
85+
},
86+
87+
CommandError: function (stdErr, code, previous) {
88+
Error.captureStackTrace(this, this.constructor);
89+
this.name = this.constructor.name;
90+
this.previous = previous;
91+
this.message = stdErr;
92+
this.code = code;
93+
this.public = false;
94+
},
8695
};
8796

8897
_.forEach(module.exports, function (error) {

backend/lib/utils.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ const exec = require('child_process').exec;
33
const execFile = require('child_process').execFile;
44
const { Liquid } = require('liquidjs');
55
const logger = require('../logger').global;
6+
const error = require('./error');
67

78
module.exports = {
89

9-
/**
10-
* @param {String} cmd
11-
* @returns {Promise}
12-
*/
13-
exec: function (cmd) {
14-
return new Promise((resolve, reject) => {
15-
exec(cmd, function (err, stdout, /*stderr*/) {
16-
if (err && typeof err === 'object') {
17-
reject(err);
10+
exec: async function(cmd, options = {}) {
11+
logger.debug('CMD:', cmd);
12+
13+
const { stdout, stderr } = await new Promise((resolve, reject) => {
14+
const child = exec(cmd, options, (isError, stdout, stderr) => {
15+
if (isError) {
16+
reject(new error.CommandError(stderr, isError));
1817
} else {
19-
resolve(stdout.trim());
18+
resolve({ stdout, stderr });
2019
}
2120
});
21+
22+
child.on('error', (e) => {
23+
reject(new error.CommandError(stderr, 1, e));
24+
});
2225
});
26+
return stdout;
2327
},
2428

2529
/**
@@ -28,7 +32,8 @@ module.exports = {
2832
* @returns {Promise}
2933
*/
3034
execFile: function (cmd, args) {
31-
logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
35+
// logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : ''));
36+
3237
return new Promise((resolve, reject) => {
3338
execFile(cmd, args, function (err, stdout, /*stderr*/) {
3439
if (err && typeof err === 'object') {

backend/logger.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
access: new Signale({scope: 'Access '}),
88
nginx: new Signale({scope: 'Nginx '}),
99
ssl: new Signale({scope: 'SSL '}),
10+
certbot: new Signale({scope: 'Certbot '}),
1011
import: new Signale({scope: 'Importer '}),
1112
setup: new Signale({scope: 'Setup '}),
1213
ip_ranges: new Signale({scope: 'IP Ranges'})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/node
2+
3+
// Usage:
4+
// Install all plugins defined in `certbot-dns-plugins.json`:
5+
// ./install-certbot-plugins
6+
// Install one or more specific plugins:
7+
// ./install-certbot-plugins route53 cloudflare
8+
//
9+
// Usage with a running docker container:
10+
// docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins"
11+
//
12+
13+
const dnsPlugins = require('../global/certbot-dns-plugins.json');
14+
const certbot = require('../lib/certbot');
15+
const logger = require('../logger').certbot;
16+
const batchflow = require('batchflow');
17+
18+
let hasErrors = false;
19+
let failingPlugins = [];
20+
21+
let pluginKeys = Object.keys(dnsPlugins);
22+
if (process.argv.length > 2) {
23+
pluginKeys = process.argv.slice(2);
24+
}
25+
26+
batchflow(pluginKeys).sequential()
27+
.each((i, pluginKey, next) => {
28+
certbot.installPlugin(pluginKey)
29+
.then(() => {
30+
next();
31+
})
32+
.catch((err) => {
33+
hasErrors = true;
34+
failingPlugins.push(pluginKey);
35+
next(err);
36+
});
37+
})
38+
.error((err) => {
39+
logger.error(err.message);
40+
})
41+
.end(() => {
42+
if (hasErrors) {
43+
logger.error('Some plugins failed to install. Please check the logs above. Failing plugins: ' + '\n - ' + failingPlugins.join('\n - '));
44+
process.exit(1);
45+
} else {
46+
logger.complete('Plugins installed successfully');
47+
process.exit(0);
48+
}
49+
});

docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ chown -R "$PUID:$PGID" /etc/nginx/nginx.conf
2424
chown -R "$PUID:$PGID" /etc/nginx/conf.d
2525

2626
# Prevents errors when installing python certbot plugins when non-root
27+
chown "$PUID:$PGID" /opt/certbot /opt/certbot/bin
2728
chown -R "$PUID:$PGID" /opt/certbot/lib/python*/site-packages

frontend/js/app/nginx/certificates/form.ejs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<div class="mb-3 test-domains-container">
2323
<button type="button" class="btn btn-secondary test-domains col-sm-12"><%- i18n('certificates', 'test-reachability') %></button>
2424
<div class="text-secondary small">
25-
<i class="fe fe-info"></i>
25+
<i class="fe fe-info"></i>
2626
<%- i18n('certificates', 'reachability-info') %>
2727
</div>
2828
</div>
@@ -38,11 +38,11 @@
3838
<div class="col-sm-12 col-md-12">
3939
<div class="form-group">
4040
<label class="custom-switch">
41-
<input
42-
type="checkbox"
43-
class="custom-switch-input"
44-
name="meta[dns_challenge]"
45-
value="1"
41+
<input
42+
type="checkbox"
43+
class="custom-switch-input"
44+
name="meta[dns_challenge]"
45+
value="1"
4646
<%- getUseDnsChallenge() ? 'checked' : '' %>
4747
>
4848
<span class="custom-switch-indicator"></span>
@@ -59,22 +59,22 @@
5959
<div class="col-sm-12 col-md-12">
6060
<div class="form-group">
6161
<label class="form-label"><%- i18n('ssl', 'dns-provider') %> <span class="form-required">*</span></label>
62-
<select
63-
name="meta[dns_provider]"
62+
<select
63+
name="meta[dns_provider]"
6464
id="dns_provider"
6565
class="form-control custom-select"
6666
>
67-
<option
68-
value=""
69-
disabled
67+
<option
68+
value=""
69+
disabled
7070
hidden
7171
<%- getDnsProvider() === null ? 'selected' : '' %>
7272
>Please Choose...</option>
7373
<% _.each(dns_plugins, function(plugin_info, plugin_name){ %>
74-
<option
74+
<option
7575
value="<%- plugin_name %>"
7676
<%- getDnsProvider() === plugin_name ? 'selected' : '' %>
77-
><%- plugin_info.display_name %></option>
77+
><%- plugin_info.name %></option>
7878
<% }); %>
7979
</select>
8080
</div>
@@ -86,17 +86,17 @@
8686
<div class="col-sm-12 col-md-12">
8787
<div class="form-group">
8888
<label class="form-label"><%- i18n('ssl', 'credentials-file-content') %> <span class="form-required">*</span></label>
89-
<textarea
90-
name="meta[dns_provider_credentials]"
91-
class="form-control text-monospace"
92-
id="dns_provider_credentials"
89+
<textarea
90+
name="meta[dns_provider_credentials]"
91+
class="form-control text-monospace"
92+
id="dns_provider_credentials"
9393
><%- getDnsProviderCredentials() %></textarea>
9494
<div class="text-secondary small">
95-
<i class="fe fe-info"></i>
95+
<i class="fe fe-info"></i>
9696
<%= i18n('ssl', 'credentials-file-content-info') %>
9797
</div>
9898
<div class="text-red small">
99-
<i class="fe fe-alert-triangle"></i>
99+
<i class="fe fe-alert-triangle"></i>
100100
<%= i18n('ssl', 'stored-as-plaintext-info') %>
101101
</div>
102102
</div>
@@ -108,16 +108,16 @@
108108
<div class="col-sm-12 col-md-12">
109109
<div class="form-group mb-0">
110110
<label class="form-label"><%- i18n('ssl', 'propagation-seconds') %></label>
111-
<input
111+
<input
112112
type="number"
113113
min="0"
114-
name="meta[propagation_seconds]"
115-
class="form-control"
116-
id="propagation_seconds"
114+
name="meta[propagation_seconds]"
115+
class="form-control"
116+
id="propagation_seconds"
117117
value="<%- getPropagationSeconds() %>"
118118
>
119119
<div class="text-secondary small">
120-
<i class="fe fe-info"></i>
120+
<i class="fe fe-info"></i>
121121
<%= i18n('ssl', 'propagation-seconds-info') %>
122122
</div>
123123
</div>

0 commit comments

Comments
 (0)