Skip to content

Commit 3c4ce83

Browse files
authored
Merge pull request NginxProxyManager#635 from chaptergy/allow-more-dns-challenges
Allow DNS challenges not just for cloudflare
2 parents a6b9bd7 + ac9f052 commit 3c4ce83

File tree

27 files changed

+1115
-223
lines changed

27 files changed

+1115
-223
lines changed

Jenkinsfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pipeline {
6565
// See: https://github.com/yarnpkg/yarn/issues/3254
6666
sh '''docker run --rm \\
6767
-v "$(pwd)/backend:/app" \\
68+
-v "$(pwd)/global:/app/global" \\
6869
-w /app \\
6970
node:latest \\
7071
sh -c "yarn install && yarn eslint . && rm -rf node_modules"

backend/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ app.use(function (err, req, res, next) {
6666
}
6767
};
6868

69-
if (process.env.NODE_ENV === 'development') {
69+
if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
7070
payload.debug = {
7171
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
7272
previous: err.previous

backend/config/sqlite-test-db.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"knex": {
55
"client": "sqlite3",
66
"connection": {
7-
"filename": "/app/backend/config/mydb.sqlite"
7+
"filename": "/app/config/mydb.sqlite"
88
},
99
"pool": {
1010
"min": 0,

backend/internal/certificate.js

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const internalNginx = require('./nginx');
1313
const internalHost = require('./host');
1414
const certbot_command = '/usr/bin/certbot';
1515
const le_config = '/etc/letsencrypt.ini';
16+
const dns_plugins = require('../global/certbot-dns-plugins');
1617

1718
function omissions() {
1819
return ['is_deleted'];
@@ -141,11 +142,11 @@ const internalCertificate = {
141142
});
142143
})
143144
.then((in_use_result) => {
144-
// Is CloudFlare, no config needed, so skip 3 and 5.
145-
if (data.meta.cloudflare_use) {
145+
// With DNS challenge no config is needed, so skip 3 and 5.
146+
if (certificate.meta.dns_challenge) {
146147
return internalNginx.reload().then(() => {
147148
// 4. Request cert
148-
return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token);
149+
return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate);
149150
})
150151
.then(internalNginx.reload)
151152
.then(() => {
@@ -772,35 +773,70 @@ const internalCertificate = {
772773
},
773774

774775
/**
775-
* @param {Object} certificate the certificate row
776-
* @param {String} apiToken the cloudflare api token
776+
* @param {Object} certificate the certificate row
777+
* @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`)
778+
* @param {String | null} credentials the content of this providers credentials file
779+
* @param {String} propagation_seconds the cloudflare api token
777780
* @returns {Promise}
778781
*/
779-
requestLetsEncryptCloudFlareDnsSsl: (certificate, apiToken) => {
780-
logger.info('Requesting Let\'sEncrypt certificates via Cloudflare DNS for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
782+
requestLetsEncryptSslWithDnsChallenge: (certificate) => {
783+
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
784+
785+
if (!dns_plugin) {
786+
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
787+
}
788+
789+
logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
790+
791+
const credentials_loc = '/etc/letsencrypt/credentials-' + certificate.id;
792+
const credentials_cmd = 'echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'';
793+
const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version;
781794

782-
let tokenLoc = '~/cloudflare-token';
783-
let storeKey = 'echo "dns_cloudflare_api_token = ' + apiToken + '" > ' + tokenLoc;
795+
// Whether the plugin has a --<name>-credentials argument
796+
const has_config_arg = certificate.meta.dns_provider !== 'route53';
784797

785-
let cmd =
786-
storeKey + ' && ' +
798+
let main_cmd =
787799
certbot_command + ' certonly --non-interactive ' +
788800
'--cert-name "npm-' + certificate.id + '" ' +
789801
'--agree-tos ' +
790802
'--email "' + certificate.meta.letsencrypt_email + '" ' +
791803
'--domains "' + certificate.domain_names.join(',') + '" ' +
792-
'--dns-cloudflare --dns-cloudflare-credentials ' + tokenLoc +
793-
(le_staging ? ' --staging' : '')
794-
+ ' && rm ' + tokenLoc;
804+
'--authenticator ' + dns_plugin.full_plugin_name + ' ' +
805+
(
806+
has_config_arg
807+
? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentials_loc + '"'
808+
: ''
809+
) +
810+
(
811+
certificate.meta.propagation_seconds !== undefined
812+
? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
813+
: ''
814+
) +
815+
(le_staging ? ' --staging' : '');
816+
817+
// Prepend the path to the credentials file as an environment variable
818+
if (certificate.meta.dns_provider === 'route53') {
819+
main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
820+
}
821+
822+
const teardown_cmd = `rm '${credentials_loc}'`;
795823

796824
if (debug_mode) {
797-
logger.info('Command:', cmd);
825+
logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd} && ${teardown_cmd}`);
798826
}
799827

800-
return utils.exec(cmd).then((result) => {
801-
logger.info(result);
802-
return result;
803-
});
828+
return utils.exec(credentials_cmd)
829+
.then(() => {
830+
return utils.exec(prepare_cmd)
831+
.then(() => {
832+
return utils.exec(main_cmd)
833+
.then(async (result) => {
834+
await utils.exec(teardown_cmd);
835+
logger.info(result);
836+
return result;
837+
});
838+
});
839+
});
804840
},
805841

806842

@@ -817,7 +853,7 @@ const internalCertificate = {
817853
})
818854
.then((certificate) => {
819855
if (certificate.provider === 'letsencrypt') {
820-
let renewMethod = certificate.meta.cloudflare_use ? internalCertificate.renewLetsEncryptCloudFlareSsl : internalCertificate.renewLetsEncryptSsl;
856+
let renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl;
821857

822858
return renewMethod(certificate)
823859
.then(() => {
@@ -877,22 +913,47 @@ const internalCertificate = {
877913
* @param {Object} certificate the certificate row
878914
* @returns {Promise}
879915
*/
880-
renewLetsEncryptCloudFlareSsl: (certificate) => {
881-
logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
916+
renewLetsEncryptSslWithDnsChallenge: (certificate) => {
917+
const dns_plugin = dns_plugins[certificate.meta.dns_provider];
882918

883-
let cmd = certbot_command + ' renew --non-interactive ' +
919+
if (!dns_plugin) {
920+
throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
921+
}
922+
923+
logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
924+
925+
const credentials_loc = '/etc/letsencrypt/credentials-' + certificate.id;
926+
const credentials_cmd = 'echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentials_loc + '\' && chmod 600 \'' + credentials_loc + '\'';
927+
const prepare_cmd = 'pip3 install ' + dns_plugin.package_name + '==' + dns_plugin.package_version;
928+
929+
let main_cmd =
930+
certbot_command + ' renew --non-interactive ' +
884931
'--cert-name "npm-' + certificate.id + '" ' +
885-
'--disable-hook-validation ' +
886-
(le_staging ? '--staging' : '');
932+
'--disable-hook-validation' +
933+
(le_staging ? ' --staging' : '');
934+
935+
// Prepend the path to the credentials file as an environment variable
936+
if (certificate.meta.dns_provider === 'route53') {
937+
main_cmd = 'AWS_CONFIG_FILE=\'' + credentials_loc + '\' ' + main_cmd;
938+
}
939+
940+
const teardown_cmd = `rm '${credentials_loc}'`;
887941

888942
if (debug_mode) {
889-
logger.info('Command:', cmd);
943+
logger.info('Command:', `${credentials_cmd} && ${prepare_cmd} && ${main_cmd} && ${teardown_cmd}`);
890944
}
891945

892-
return utils.exec(cmd)
893-
.then((result) => {
894-
logger.info(result);
895-
return result;
946+
return utils.exec(credentials_cmd)
947+
.then(() => {
948+
return utils.exec(prepare_cmd)
949+
.then(() => {
950+
return utils.exec(main_cmd)
951+
.then(async (result) => {
952+
await utils.exec(teardown_cmd);
953+
logger.info(result);
954+
return result;
955+
});
956+
});
896957
});
897958
},
898959

backend/routes/api/nginx/certificates.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ router
5858
.post((req, res, next) => {
5959
apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body)
6060
.then((payload) => {
61+
req.setTimeout(900000); // 15 minutes timeout
6162
return internalCertificate.create(res.locals.access, payload);
6263
})
6364
.then((result) => {
@@ -197,6 +198,7 @@ router
197198
* Renew certificate
198199
*/
199200
.post((req, res, next) => {
201+
req.setTimeout(900000); // 15 minutes timeout
200202
internalCertificate.renew(res.locals.access, {
201203
id: parseInt(req.params.certificate_id, 10)
202204
})

backend/schema/endpoints/certificates.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,23 @@
4242
"letsencrypt_agree": {
4343
"type": "boolean"
4444
},
45-
"cloudflare_use": {
45+
"dns_challenge": {
4646
"type": "boolean"
4747
},
48-
"cloudflare_token": {
48+
"dns_provider": {
4949
"type": "string"
50+
},
51+
"dns_provider_credentials": {
52+
"type": "string"
53+
},
54+
"propagation_seconds": {
55+
"anyOf": [
56+
{
57+
"type": "integer",
58+
"minimum": 0
59+
}
60+
]
61+
5062
}
5163
}
5264
}

docker/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ ENV NODE_ENV=production
1717

1818
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
1919
&& apk update \
20-
&& apk add python2 py-pip certbot jq \
21-
&& pip install certbot-dns-cloudflare \
20+
&& apk add python3 certbot jq \
21+
&& python3 -m ensurepip \
2222
&& rm -rf /var/cache/apk/*
2323

2424
ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}"
@@ -34,6 +34,7 @@ EXPOSE 443
3434
COPY docker/rootfs /
3535
ADD backend /app
3636
ADD frontend/dist /app/frontend
37+
COPY global /app/global
3738

3839
WORKDIR /app
3940
RUN yarn install

docker/dev/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ ENV S6_FIX_ATTRS_HIDDEN=1
77

88
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \
99
&& apk update \
10-
&& apk add python2 py-pip certbot jq \
11-
&& pip install certbot-dns-cloudflare \
10+
&& apk add python3 certbot jq \
11+
&& python3 -m ensurepip \
1212
&& rm -rf /var/cache/apk/*
1313

1414
# Task

docker/docker-compose.dev.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ services:
1111
- 3080:80
1212
- 3081:81
1313
- 3443:443
14+
networks:
15+
- nginx_proxy_manager
1416
environment:
1517
- NODE_ENV=development
1618
- FORCE_COLOR=1
@@ -19,13 +21,17 @@ services:
1921
volumes:
2022
- npm_data:/data
2123
- le_data:/etc/letsencrypt
22-
- ..:/app
24+
- ../backend:/app
25+
- ../frontend:/app/frontend
26+
- ../global:/app/global
2327
depends_on:
2428
- db
2529
working_dir: /app
2630

2731
db:
2832
image: jc21/mariadb-aria
33+
networks:
34+
- nginx_proxy_manager
2935
environment:
3036
MYSQL_ROOT_PASSWORD: "npm"
3137
MYSQL_DATABASE: "npm"
@@ -38,6 +44,8 @@ services:
3844
image: 'swaggerapi/swagger-ui:latest'
3945
ports:
4046
- 3001:80
47+
networks:
48+
- nginx_proxy_manager
4149
environment:
4250
URL: "http://127.0.0.1:3081/api/schema"
4351
PORT: '80'
@@ -48,3 +56,6 @@ volumes:
4856
npm_data:
4957
le_data:
5058
db_data:
59+
60+
networks:
61+
nginx_proxy_manager:

docker/rootfs/etc/nginx/conf.d/dev.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ server {
1717
proxy_set_header X-Forwarded-Proto $scheme;
1818
proxy_set_header X-Forwarded-For $remote_addr;
1919
proxy_pass http://127.0.0.1:3000/;
20+
21+
proxy_read_timeout 15m;
22+
proxy_send_timeout 15m;
2023
}
2124

2225
___location / {

0 commit comments

Comments
 (0)