From 01660b5b805e94f1e5cba1802a526c5c127cf5b3 Mon Sep 17 00:00:00 2001 From: Jocelyn Le Sage <jocelyn@le-sage.com> Date: Thu, 6 Aug 2020 17:16:22 -0400 Subject: [PATCH 01/17] Fixed now_helper for sqlite: it should also returns the time. --- backend/models/now_helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js index a1258a8..def16d0 100644 --- a/backend/models/now_helper.js +++ b/backend/models/now_helper.js @@ -6,7 +6,7 @@ Model.knex(db); module.exports = function () { if (config.database.knex && config.database.knex.client === 'sqlite3') { - return Model.raw('date(\'now\')'); + return Model.raw('datetime(\'now\',\'localtime\')'); } else { return Model.raw('NOW()'); } From 70346138a72b1864117554787102184cc88832cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2020 00:02:04 +0000 Subject: [PATCH 02/17] Bump prismjs from 1.20.0 to 1.21.0 in /docs Bumps [prismjs](https://github.com/PrismJS/prism) from 1.20.0 to 1.21.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.20.0...v1.21.0) Signed-off-by: dependabot[bot] <support@github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 02434dd..4dd7fac 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -7679,9 +7679,9 @@ pretty-time@^1.1.0: integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== prismjs@^1.13.0, prismjs@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" - integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== + version "1.21.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" + integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== optionalDependencies: clipboard "^2.0.0" From 5d6516677791bc2edc22bf3e386eaa05fbfa9f12 Mon Sep 17 00:00:00 2001 From: Jamie Curnow <jc@jc21.com> Date: Wed, 12 Aug 2020 09:32:40 +1000 Subject: [PATCH 03/17] Ignore local subnets for real IP determination --- docker/rootfs/etc/nginx/nginx.conf | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 0643cc2..23335e5 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -27,9 +27,9 @@ http { tcp_nodelay on; client_body_temp_path /tmp/nginx/body 1 2; keepalive_timeout 90s; - proxy_connect_timeout 90s; - proxy_send_timeout 90s; - proxy_read_timeout 90s; + proxy_connect_timeout 90s; + proxy_send_timeout 90s; + proxy_read_timeout 90s; ssl_prefer_server_ciphers on; gzip on; proxy_ignore_client_abort off; @@ -60,6 +60,9 @@ http { # Real IP Determination # Docker subnet: set_real_ip_from 172.0.0.0/8; + # Local subnets: + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 192.0.0.0/8; # NPM generated CDN ip ranges: include conf.d/include/ip_ranges.conf; # always put the following 2 lines after ip subnets: From f539e813aafc997d52633dc17ee7a4f9c828b8a3 Mon Sep 17 00:00:00 2001 From: Jocelyn Le Sage <jocelyn@le-sage.com> Date: Fri, 14 Aug 2020 14:27:44 -0400 Subject: [PATCH 04/17] Removed the hardcoded `--webroot` certbot argument to better support DNS challenge. Also, this option is already set in the default `letsencrypt.ini`. --- backend/internal/certificate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 4f0caf3..62947da 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -733,7 +733,6 @@ const internalCertificate = { '--agree-tos ' + '--email "' + certificate.meta.letsencrypt_email + '" ' + '--preferred-challenges "dns,http" ' + - '--webroot ' + '--domains "' + certificate.domain_names.join(',') + '" ' + (le_staging ? '--staging' : ''); From 83fad8bcda54944b73496485de8874db055a1e3a Mon Sep 17 00:00:00 2001 From: Jocelyn Le Sage <jocelyn@le-sage.com> Date: Fri, 14 Aug 2020 19:23:19 -0400 Subject: [PATCH 05/17] Removed usage of `FROM_UNIXTIME` mysql-specific function. This provide better interoperability with different databases (e.g. sqlite). Fixes #557 --- backend/internal/certificate.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 4f0caf3..0d8cb85 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -77,7 +77,7 @@ const internalCertificate = { .where('id', certificate.id) .andWhere('provider', 'letsencrypt') .patch({ - expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') + expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') }); }) .catch((err) => { @@ -180,7 +180,7 @@ const internalCertificate = { return certificateModel .query() .patchAndFetchById(certificate.id, { - expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') + expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') }) .then((saved_row) => { // Add cert data for audit log @@ -558,7 +558,7 @@ const internalCertificate = { // TODO: This uses a mysql only raw function that won't translate to postgres return internalCertificate.update(access, { id: data.id, - expires_on: certificateModel.raw('FROM_UNIXTIME(' + validations.certificate.dates.to + ')'), + expires_on: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'), domain_names: [validations.certificate.cn], meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later }) @@ -769,7 +769,7 @@ const internalCertificate = { return certificateModel .query() .patchAndFetchById(certificate.id, { - expires_on: certificateModel.raw('FROM_UNIXTIME(' + cert_info.dates.to + ')') + expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') }); }) .then((updated_certificate) => { From f78a4c6ad128dc78b02ff3df7cf00503dfd25756 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2020 17:01:00 +0000 Subject: [PATCH 06/17] Bump bcrypt from 4.0.1 to 5.0.0 in /backend Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 4.0.1 to 5.0.0. - [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases) - [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v4.0.1...v5.0.0) Signed-off-by: dependabot[bot] <support@github.com> --- backend/package.json | 2 +- backend/yarn.lock | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/backend/package.json b/backend/package.json index d2a8c4c..b4edda6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "dependencies": { "ajv": "^6.12.0", "batchflow": "^0.4.0", - "bcrypt": "^4.0.1", + "bcrypt": "^5.0.0", "body-parser": "^1.19.0", "compression": "^1.7.4", "config": "^3.3.1", diff --git a/backend/yarn.lock b/backend/yarn.lock index f95dbf7..8e3d3df 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -249,13 +249,13 @@ batchflow@^0.4.0: resolved "https://registry.yarnpkg.com/batchflow/-/batchflow-0.4.0.tgz#7d419df79b6b7587b06f9ea34f96ccef6f74e5b5" integrity sha1-fUGd95trdYewb56jT5bM72905bU= -bcrypt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-4.0.1.tgz#06e21e749a061020e4ff1283c1faa93187ac57fe" - integrity sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ== +bcrypt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.0.tgz#051407c7cd5ffbfb773d541ca3760ea0754e37e2" + integrity sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg== dependencies: - node-addon-api "^2.0.0" - node-pre-gyp "0.14.0" + node-addon-api "^3.0.0" + node-pre-gyp "0.15.0" bignumber.js@9.0.0: version "9.0.0" @@ -2166,7 +2166,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -2235,7 +2235,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1: +needle@^2.2.1, needle@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.0.tgz#e6fc4b3cc6c25caed7554bd613a5cf0bac8c31c0" integrity sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA== @@ -2254,19 +2254,19 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-addon-api@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" - integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-addon-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.0.tgz#812446a1001a54f71663bed188314bba07e09247" + integrity sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg== -node-pre-gyp@0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== +node-pre-gyp@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087" + integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA== dependencies: detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" + mkdirp "^0.5.3" + needle "^2.5.0" nopt "^4.0.1" npm-packlist "^1.1.6" npmlog "^4.0.2" From 251aac716a707d730733444b71247402be678f08 Mon Sep 17 00:00:00 2001 From: Jaap-Jan <jipjan@gmail.com> Date: Fri, 21 Aug 2020 09:49:43 +0200 Subject: [PATCH 07/17] Add CloudFlare DNS plugin to certbot --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e3eefb3..5224416 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,7 +17,8 @@ ENV NODE_ENV=production RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ && apk update \ - && apk add python2 certbot jq \ + && apk add python2 py-pip certbot jq \ + && pip install certbot-dns-cloudflare \ && rm -rf /var/cache/apk/* ENV NPM_BUILD_VERSION="${BUILD_VERSION}" NPM_BUILD_COMMIT="${BUILD_COMMIT}" NPM_BUILD_DATE="${BUILD_DATE}" From 2d7576c57ea9d0219a5321678adf162f580e26b3 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 10:54:36 +0000 Subject: [PATCH 08/17] add cloudflare dns also to dev docker file --- docker/dev/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 35f5651..5b67981 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -7,7 +7,8 @@ ENV S6_FIX_ATTRS_HIDDEN=1 RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ && apk update \ - && apk add python2 certbot jq \ + && apk add python2 py-pip certbot jq \ + && pip install certbot-dns-cloudflare \ && rm -rf /var/cache/apk/* # Task From b9a95840e09fa2a633c8cade91c206dfc5821492 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 11:40:41 +0000 Subject: [PATCH 09/17] add cloudflare dns option to letsencrypt via manual certificate --- frontend/js/app/nginx/certificates/form.ejs | 18 ++++++++++++++++++ frontend/js/app/nginx/certificates/form.js | 13 ++++++++++++- frontend/js/i18n/messages.json | 3 ++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs index 32edb6b..98de260 100644 --- a/frontend/js/app/nginx/certificates/form.ejs +++ b/frontend/js/app/nginx/certificates/form.ejs @@ -20,6 +20,24 @@ <input name="meta[letsencrypt_email]" type="email" class="form-control" placeholder="" value="<%- getLetsencryptEmail() %>" required> </div> </div> + + <!-- CloudFlare --> + <div class="col-sm-12 col-md-12"> + <div class="form-group"> + <label class="custom-switch"> + <input type="checkbox" class="custom-switch-input" name="use_cloudflare" value="1"> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description"><%= i18n('ssl', 'use-cloudflare') %></span> + </label> + </div> + </div> + <div class="col-sm-12 col-md-12 cloudflare"> + <div class="form-group"> + <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> + <input type="text" name="cloudflare_dns_api_token" class="form-control" id="input-domains" required> + </div> + </div> + <div class="col-sm-12 col-md-12"> <div class="form-group"> <label class="custom-switch"> diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js index 4c315c1..bdb4f6c 100644 --- a/frontend/js/app/nginx/certificates/form.js +++ b/frontend/js/app/nginx/certificates/form.js @@ -20,10 +20,20 @@ module.exports = Mn.View.extend({ save: 'button.save', other_certificate: '#other_certificate', other_certificate_key: '#other_certificate_key', - other_intermediate_certificate: '#other_intermediate_certificate' + other_intermediate_certificate: '#other_intermediate_certificate', + cloudflare_switch: 'input[name="use_cloudflare"]', + cloudflare: '.cloudflare' }, events: { + 'change @ui.cloudflare_switch': function() { + let checked = this.ui.cloudflare_switch.prop('checked'); + if (checked) { + this.ui.cloudflare.show(); + } else { + this.ui.cloudflare.hide(); + } + }, 'click @ui.save': function (e) { e.preventDefault(); @@ -146,6 +156,7 @@ module.exports = Mn.View.extend({ }, createFilter: /^(?:[^.*]+\.?)+[^.]$/ }); + this.ui.cloudflare.hide(); }, initialize: function (options) { diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 7b5205a..d0c9d8e 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -101,7 +101,8 @@ "letsencrypt-email": "Email Address for Let's Encrypt", "letsencrypt-agree": "I Agree to the <a href=\"{url}\" target=\"_blank\">Let's Encrypt Terms of Service</a>", "delete-ssl": "The SSL certificates attached will NOT be removed, they will need to be removed manually.", - "hosts-warning": "These domains must be already configured to point to this installation" + "hosts-warning": "These domains must be already configured to point to this installation", + "use-cloudflare": "Use CloudFlare DNS verification" }, "proxy-hosts": { "title": "Proxy Hosts", From ff1770204c8b553b287b6f4214489e4c3394ce6d Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 12:50:41 +0000 Subject: [PATCH 10/17] request via cloudflare dns working --- backend/internal/certificate.js | 40 ++++++++++++++++++++- backend/schema/endpoints/certificates.json | 6 ++++ frontend/js/app/nginx/certificates/form.ejs | 4 +-- frontend/js/app/nginx/certificates/form.js | 9 ++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 4f0caf3..1c71a45 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -146,7 +146,11 @@ const internalCertificate = { .then(internalNginx.reload) .then(() => { // 4. Request cert - return internalCertificate.requestLetsEncryptSsl(certificate); + if (data.meta.cloudflare_use) { + return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token); + } else { + return internalCertificate.requestLetsEncryptSsl(certificate); + } }) .then(() => { // 5. Remove LE config @@ -748,6 +752,40 @@ const internalCertificate = { }); }, + /** + * @param {Object} certificate the certificate row + * @param {String} apiToken the cloudflare api token + * @returns {Promise} + */ + requestLetsEncryptCloudFlareDnsSsl: (certificate, apiToken) => { + logger.info('Requesting Let\'sEncrypt certificates via Cloudflare DNS for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); + + let tokenLoc = '~/cloudflare-token'; + let storeKey = 'echo "dns_cloudflare_api_token = ' + apiToken + '" > ' + tokenLoc; + + let cmd = certbot_command + ' certonly --non-interactive ' + + '--cert-name "npm-' + certificate.id + '" ' + + '--agree-tos ' + + '--email "' + certificate.meta.letsencrypt_email + '" ' + + '--domains "' + certificate.domain_names.join(',') + '" ' + + '--dns-cloudflare --dns-cloudflare-credentials ' + tokenLoc + ' ' + + (le_staging ? '--staging' : ''); + + if (debug_mode) { + logger.info('Command:', cmd); + } + + return utils.exec(storeKey).then((result) => { + utils.exec(cmd).then((result) => { + utils.exec('rm ' + tokenLoc).then(result => { + logger.success(result); + return result; + }); + }); + }); + }, + + /** * @param {Access} access * @param {Object} data diff --git a/backend/schema/endpoints/certificates.json b/backend/schema/endpoints/certificates.json index d3294f8..27ea2d2 100644 --- a/backend/schema/endpoints/certificates.json +++ b/backend/schema/endpoints/certificates.json @@ -41,6 +41,12 @@ }, "letsencrypt_agree": { "type": "boolean" + }, + "cloudflare_use": { + "type": "boolean" + }, + "cloudflare_token": { + "type": "string" } } } diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs index 98de260..2af4345 100644 --- a/frontend/js/app/nginx/certificates/form.ejs +++ b/frontend/js/app/nginx/certificates/form.ejs @@ -25,7 +25,7 @@ <div class="col-sm-12 col-md-12"> <div class="form-group"> <label class="custom-switch"> - <input type="checkbox" class="custom-switch-input" name="use_cloudflare" value="1"> + <input type="checkbox" class="custom-switch-input" name="meta[cloudflare_use]" value="1"> <span class="custom-switch-indicator"></span> <span class="custom-switch-description"><%= i18n('ssl', 'use-cloudflare') %></span> </label> @@ -34,7 +34,7 @@ <div class="col-sm-12 col-md-12 cloudflare"> <div class="form-group"> <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> - <input type="text" name="cloudflare_dns_api_token" class="form-control" id="input-domains" required> + <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token" required> </div> </div> diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js index bdb4f6c..7387202 100644 --- a/frontend/js/app/nginx/certificates/form.js +++ b/frontend/js/app/nginx/certificates/form.js @@ -21,7 +21,7 @@ module.exports = Mn.View.extend({ other_certificate: '#other_certificate', other_certificate_key: '#other_certificate_key', other_intermediate_certificate: '#other_intermediate_certificate', - cloudflare_switch: 'input[name="use_cloudflare"]', + cloudflare_switch: 'input[name="meta[cloudflare_use]"]', cloudflare: '.cloudflare' }, @@ -50,6 +50,9 @@ module.exports = Mn.View.extend({ if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; } + if (typeof data.meta !== 'undefined' && typeof data.meta.cloudflare_use !== 'undefined') { + data.meta.cloudflare_use = !!data.meta.cloudflare_use; + } if (typeof data.domain_names === 'string' && data.domain_names) { data.domain_names = data.domain_names.split(','); @@ -140,6 +143,10 @@ module.exports = Mn.View.extend({ getLetsencryptAgree: function () { return typeof this.meta.letsencrypt_agree !== 'undefined' ? this.meta.letsencrypt_agree : false; + }, + + getCloudflareUse: function () { + return typeof this.meta.cloudflare_use !== 'undefined' ? this.meta.cloudflare_use : false; } }, From 077cf75ef20c16eaa9e8bdb2a3cc8fc0a8b2dc1b Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 13:24:20 +0000 Subject: [PATCH 11/17] wildcard support --- backend/internal/certificate.js | 73 ++++++++++++++-------- frontend/js/app/nginx/certificates/form.js | 5 +- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 1c71a45..c5e6a46 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -141,20 +141,11 @@ const internalCertificate = { }); }) .then((in_use_result) => { - // 3. Generate the LE config - return internalNginx.generateLetsEncryptRequestConfig(certificate) - .then(internalNginx.reload) - .then(() => { + // Is CloudFlare, no config needed, so skip 3 and 5. + if (data.meta.cloudflare_use) { + return internalNginx.reload().then(() => { // 4. Request cert - if (data.meta.cloudflare_use) { - return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token); - } else { - return internalCertificate.requestLetsEncryptSsl(certificate); - } - }) - .then(() => { - // 5. Remove LE config - return internalNginx.deleteLetsEncryptRequestConfig(certificate); + return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token); }) .then(internalNginx.reload) .then(() => { @@ -166,15 +157,44 @@ const internalCertificate = { }) .catch((err) => { // In the event of failure, revert things and throw err back - return internalNginx.deleteLetsEncryptRequestConfig(certificate) - .then(() => { - return internalCertificate.enableInUseHosts(in_use_result); - }) + return internalCertificate.enableInUseHosts(in_use_result) .then(internalNginx.reload) .then(() => { throw err; }); }); + } else { + // 3. Generate the LE config + return internalNginx.generateLetsEncryptRequestConfig(certificate) + .then(internalNginx.reload) + .then(() => { + // 4. Request cert + return internalCertificate.requestLetsEncryptSsl(certificate); + }) + .then(() => { + // 5. Remove LE config + return internalNginx.deleteLetsEncryptRequestConfig(certificate); + }) + .then(internalNginx.reload) + .then(() => { + // 6. Re-instate previously disabled hosts + return internalCertificate.enableInUseHosts(in_use_result); + }) + .then(() => { + return certificate; + }) + .catch((err) => { + // In the event of failure, revert things and throw err back + return internalNginx.deleteLetsEncryptRequestConfig(certificate) + .then(() => { + return internalCertificate.enableInUseHosts(in_use_result); + }) + .then(internalNginx.reload) + .then(() => { + throw err; + }); + }); + } }) .then(() => { // At this point, the letsencrypt cert should exist on disk. @@ -763,26 +783,25 @@ const internalCertificate = { let tokenLoc = '~/cloudflare-token'; let storeKey = 'echo "dns_cloudflare_api_token = ' + apiToken + '" > ' + tokenLoc; - let cmd = certbot_command + ' certonly --non-interactive ' + + let cmd = + storeKey + " && " + + certbot_command + ' certonly --non-interactive ' + '--cert-name "npm-' + certificate.id + '" ' + '--agree-tos ' + '--email "' + certificate.meta.letsencrypt_email + '" ' + '--domains "' + certificate.domain_names.join(',') + '" ' + - '--dns-cloudflare --dns-cloudflare-credentials ' + tokenLoc + ' ' + - (le_staging ? '--staging' : ''); + '--dns-cloudflare --dns-cloudflare-credentials ' + tokenLoc + + (le_staging ? ' --staging' : '') + + ' && rm ' + tokenLoc; if (debug_mode) { logger.info('Command:', cmd); } - return utils.exec(storeKey).then((result) => { - utils.exec(cmd).then((result) => { - utils.exec('rm ' + tokenLoc).then(result => { - logger.success(result); - return result; - }); + return utils.exec(cmd).then((result) => { + logger.info(result); + return result; }); - }); }, diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js index 7387202..de4432b 100644 --- a/frontend/js/app/nginx/certificates/form.js +++ b/frontend/js/app/nginx/certificates/form.js @@ -39,6 +39,7 @@ module.exports = Mn.View.extend({ if (!this.ui.form[0].checkValidity()) { $('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); + $(this).addClass('btn-loading'); return; } @@ -94,6 +95,7 @@ module.exports = Mn.View.extend({ } this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + this.ui.save.addClass('btn-loading'); // compile file data let form_data = new FormData(); @@ -132,6 +134,7 @@ module.exports = Mn.View.extend({ .catch(err => { alert(err.message); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + this.ui.save.removeClass('btn-loading'); }); } }, @@ -161,7 +164,7 @@ module.exports = Mn.View.extend({ text: input }; }, - createFilter: /^(?:[^.*]+\.?)+[^.]$/ + createFilter: /^(?:[^.]+\.?)+[^.]$/ }); this.ui.cloudflare.hide(); }, From cff6c4d1f5ca239c42069393c45facde3ee39f44 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 16:48:14 +0000 Subject: [PATCH 12/17] - prevent wildcard generation when not using Cloudflare dns - fix cloudflare token required logic --- frontend/js/app/nginx/certificates/form.ejs | 2 +- frontend/js/app/nginx/certificates/form.js | 27 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/js/app/nginx/certificates/form.ejs b/frontend/js/app/nginx/certificates/form.ejs index 2af4345..19ea2c7 100644 --- a/frontend/js/app/nginx/certificates/form.ejs +++ b/frontend/js/app/nginx/certificates/form.ejs @@ -34,7 +34,7 @@ <div class="col-sm-12 col-md-12 cloudflare"> <div class="form-group"> <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> - <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token" required> + <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token"> </div> </div> diff --git a/frontend/js/app/nginx/certificates/form.js b/frontend/js/app/nginx/certificates/form.js index de4432b..5d263df 100644 --- a/frontend/js/app/nginx/certificates/form.js +++ b/frontend/js/app/nginx/certificates/form.js @@ -22,16 +22,19 @@ module.exports = Mn.View.extend({ other_certificate_key: '#other_certificate_key', other_intermediate_certificate: '#other_intermediate_certificate', cloudflare_switch: 'input[name="meta[cloudflare_use]"]', + cloudflare_token: 'input[name="meta[cloudflare_token]"', cloudflare: '.cloudflare' }, events: { 'change @ui.cloudflare_switch': function() { let checked = this.ui.cloudflare_switch.prop('checked'); - if (checked) { + if (checked) { + this.ui.cloudflare_token.prop('required', 'required'); this.ui.cloudflare.show(); - } else { - this.ui.cloudflare.hide(); + } else { + this.ui.cloudflare_token.prop('required', false); + this.ui.cloudflare.hide(); } }, 'click @ui.save': function (e) { @@ -39,7 +42,7 @@ module.exports = Mn.View.extend({ if (!this.ui.form[0].checkValidity()) { $('<input type="submit">').hide().appendTo(this.ui.form).click().remove(); - $(this).addClass('btn-loading'); + $(this).removeClass('btn-loading'); return; } @@ -47,6 +50,22 @@ module.exports = Mn.View.extend({ let data = this.ui.form.serializeJSON(); data.provider = this.model.get('provider'); + + + let domain_err = false; + if (!data.meta.cloudflare_use) { + data.domain_names.split(',').map(function (name) { + if (name.match(/\*/im)) { + domain_err = true; + } + }); + } + + if (domain_err) { + alert('Cannot request Let\'s Encrypt Certificate for wildcard domains when not using CloudFlare DNS'); + return; + } + // Manipulate if (typeof data.meta !== 'undefined' && typeof data.meta.letsencrypt_agree !== 'undefined') { data.meta.letsencrypt_agree = !!data.meta.letsencrypt_agree; From c5aa2b9f771cbd4c78c239ed0791aeb8d9e4d2e4 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 18:29:16 +0000 Subject: [PATCH 13/17] add cloudflare renew and make revoke working for both by deleting unnecessary config command --- backend/internal/certificate.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index c5e6a46..2dadb34 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -818,7 +818,9 @@ const internalCertificate = { }) .then((certificate) => { if (certificate.provider === 'letsencrypt') { - return internalCertificate.renewLetsEncryptSsl(certificate) + let renewMethod = certificate.meta.cloudflare_use ? internalCertificate.renewLetsEncryptCloudFlareSsl : internalCertificate.renewLetsEncryptSsl; + + return renewMethod(certificate) .then(() => { return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); }) @@ -872,6 +874,29 @@ const internalCertificate = { }); }, + /** + * @param {Object} certificate the certificate row + * @returns {Promise} + */ + renewLetsEncryptCloudFlareSsl: (certificate) => { + logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); + + let cmd = certbot_command + ' renew --non-interactive ' + + '--cert-name "npm-' + certificate.id + '" ' + + '--disable-hook-validation ' + + (le_staging ? '--staging' : ''); + + if (debug_mode) { + logger.info('Command:', cmd); + } + + return utils.exec(cmd) + .then((result) => { + logger.info(result); + return result; + }); + }, + /** * @param {Object} certificate the certificate row * @param {Boolean} [throw_errors] @@ -881,7 +906,6 @@ const internalCertificate = { logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); let cmd = certbot_command + ' revoke --non-interactive ' + - '--config "' + le_config + '" ' + '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + '--delete-after-revoke ' + (le_staging ? '--staging' : ''); From ab67481e99e9135309318339e7be04eda90fdb0d Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Sun, 23 Aug 2020 18:56:25 +0000 Subject: [PATCH 14/17] fix eslint errors --- backend/internal/certificate.js | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 2dadb34..3508daf 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -147,22 +147,22 @@ const internalCertificate = { // 4. Request cert return internalCertificate.requestLetsEncryptCloudFlareDnsSsl(certificate, data.meta.cloudflare_token); }) - .then(internalNginx.reload) - .then(() => { - // 6. Re-instate previously disabled hosts - return internalCertificate.enableInUseHosts(in_use_result); - }) - .then(() => { - return certificate; - }) - .catch((err) => { - // In the event of failure, revert things and throw err back - return internalCertificate.enableInUseHosts(in_use_result) - .then(internalNginx.reload) - .then(() => { - throw err; - }); - }); + .then(internalNginx.reload) + .then(() => { + // 6. Re-instate previously disabled hosts + return internalCertificate.enableInUseHosts(in_use_result); + }) + .then(() => { + return certificate; + }) + .catch((err) => { + // In the event of failure, revert things and throw err back + return internalCertificate.enableInUseHosts(in_use_result) + .then(internalNginx.reload) + .then(() => { + throw err; + }); + }); } else { // 3. Generate the LE config return internalNginx.generateLetsEncryptRequestConfig(certificate) @@ -784,7 +784,7 @@ const internalCertificate = { let storeKey = 'echo "dns_cloudflare_api_token = ' + apiToken + '" > ' + tokenLoc; let cmd = - storeKey + " && " + + storeKey + ' && ' + certbot_command + ' certonly --non-interactive ' + '--cert-name "npm-' + certificate.id + '" ' + '--agree-tos ' + @@ -799,9 +799,9 @@ const internalCertificate = { } return utils.exec(cmd).then((result) => { - logger.info(result); - return result; - }); + logger.info(result); + return result; + }); }, From e8596c155460199e508c71b86e273dc31b8e58e7 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Mon, 24 Aug 2020 09:00:00 +0000 Subject: [PATCH 15/17] cloudflare DNS also possible while adding proxy, redirection and 404 --- frontend/js/app/nginx/dead/form.ejs | 17 ++++++++++ frontend/js/app/nginx/dead/form.js | 38 ++++++++++++++++----- frontend/js/app/nginx/proxy/form.ejs | 17 ++++++++++ frontend/js/app/nginx/proxy/form.js | 39 +++++++++++++++++----- frontend/js/app/nginx/redirection/form.ejs | 17 ++++++++++ frontend/js/app/nginx/redirection/form.js | 37 +++++++++++++++----- 6 files changed, 140 insertions(+), 25 deletions(-) diff --git a/frontend/js/app/nginx/dead/form.ejs b/frontend/js/app/nginx/dead/form.ejs index f94d2cc..d48820f 100644 --- a/frontend/js/app/nginx/dead/form.ejs +++ b/frontend/js/app/nginx/dead/form.ejs @@ -73,6 +73,23 @@ </div> </div> + <!-- CloudFlare --> + <div class="col-sm-12 col-md-12 letsencrypt"> + <div class="form-group"> + <label class="custom-switch"> + <input type="checkbox" class="custom-switch-input" name="meta[cloudflare_use]" value="1"> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description"><%= i18n('ssl', 'use-cloudflare') %></span> + </label> + </div> + </div> + <div class="col-sm-12 col-md-12 cloudflare letsencrypt"> + <div class="form-group"> + <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> + <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token"> + </div> + </div> + <!-- Lets encrypt --> <div class="col-sm-12 col-md-12 letsencrypt"> <div class="form-group"> diff --git a/frontend/js/app/nginx/dead/form.js b/frontend/js/app/nginx/dead/form.js index 4d7ef6b..aca367a 100644 --- a/frontend/js/app/nginx/dead/form.js +++ b/frontend/js/app/nginx/dead/form.js @@ -23,6 +23,9 @@ module.exports = Mn.View.extend({ hsts_enabled: 'input[name="hsts_enabled"]', hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', + cloudflare_switch: 'input[name="meta[cloudflare_use]"]', + cloudflare_token: 'input[name="meta[cloudflare_token]"', + cloudflare: '.cloudflare', letsencrypt: '.letsencrypt' }, @@ -31,10 +34,12 @@ module.exports = Mn.View.extend({ let id = this.ui.certificate_select.val(); if (id === 'new') { this.ui.letsencrypt.show().find('input').prop('disabled', false); + this.ui.cloudflare.hide(); } else { this.ui.letsencrypt.hide().find('input').prop('disabled', true); } + let enabled = id === 'new' || parseInt(id, 10) > 0; let inputs = this.ui.ssl_forced.add(this.ui.http2_support); @@ -76,6 +81,17 @@ module.exports = Mn.View.extend({ } }, + 'change @ui.cloudflare_switch': function() { + let checked = this.ui.cloudflare_switch.prop('checked'); + if (checked) { + this.ui.cloudflare_token.prop('required', 'required'); + this.ui.cloudflare.show(); + } else { + this.ui.cloudflare_token.prop('required', false); + this.ui.cloudflare.hide(); + } + }, + 'click @ui.save': function (e) { e.preventDefault(); @@ -98,20 +114,23 @@ module.exports = Mn.View.extend({ } // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { + if (data.certificate_id === 'new') { let domain_err = false; - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); + if (!data.meta.cloudflare_use) { + data.domain_names.map(function (name) { + if (name.match(/\*/im)) { + domain_err = true; + } + }); + } if (domain_err) { - alert('Cannot request Let\'s Encrypt Certificate for wildcard domains'); + alert('Cannot request Let\'s Encrypt Certificate for wildcard domains without CloudFlare DNS.'); return; } - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; + data.meta.cloudflare_use = data.meta.cloudflare_use === '1'; + data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; } else { data.certificate_id = parseInt(data.certificate_id, 10); } @@ -127,6 +146,8 @@ module.exports = Mn.View.extend({ } this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + this.ui.save.addClass('btn-loading'); + method(data) .then(result => { view.model.set(result); @@ -140,6 +161,7 @@ module.exports = Mn.View.extend({ .catch(err => { alert(err.message); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + this.ui.save.removeClass('btn-loading'); }); } }, diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 0cc0d54..e003597 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -141,6 +141,23 @@ </div> </div> + <!-- CloudFlare --> + <div class="col-sm-12 col-md-12 letsencrypt"> + <div class="form-group"> + <label class="custom-switch"> + <input type="checkbox" class="custom-switch-input" name="meta[cloudflare_use]" value="1"> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description"><%= i18n('ssl', 'use-cloudflare') %></span> + </label> + </div> + </div> + <div class="col-sm-12 col-md-12 cloudflare letsencrypt"> + <div class="form-group"> + <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> + <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token"> + </div> + </div> + <!-- Lets encrypt --> <div class="col-sm-12 col-md-12 letsencrypt"> <div class="form-group"> diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index eb93bc8..0f64281 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -33,6 +33,9 @@ module.exports = Mn.View.extend({ hsts_enabled: 'input[name="hsts_enabled"]', hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', + cloudflare_switch: 'input[name="meta[cloudflare_use]"]', + cloudflare_token: 'input[name="meta[cloudflare_token]"', + cloudflare: '.cloudflare', forward_scheme: 'select[name="forward_scheme"]', letsencrypt: '.letsencrypt' }, @@ -46,6 +49,7 @@ module.exports = Mn.View.extend({ let id = this.ui.certificate_select.val(); if (id === 'new') { this.ui.letsencrypt.show().find('input').prop('disabled', false); + this.ui.cloudflare.hide(); } else { this.ui.letsencrypt.hide().find('input').prop('disabled', true); } @@ -91,6 +95,17 @@ module.exports = Mn.View.extend({ } }, + 'change @ui.cloudflare_switch': function() { + let checked = this.ui.cloudflare_switch.prop('checked'); + if (checked) { + this.ui.cloudflare_token.prop('required', 'required'); + this.ui.cloudflare.show(); + } else { + this.ui.cloudflare_token.prop('required', false); + this.ui.cloudflare.hide(); + } + }, + 'click @ui.add_location_btn': function (e) { e.preventDefault(); @@ -134,20 +149,23 @@ module.exports = Mn.View.extend({ } // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { + if (data.certificate_id === 'new') { let domain_err = false; - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); + if (!data.meta.cloudflare_use) { + data.domain_names.map(function (name) { + if (name.match(/\*/im)) { + domain_err = true; + } + }); + } if (domain_err) { - alert('Cannot request Let\'s Encrypt Certificate for wildcard domains'); + alert('Cannot request Let\'s Encrypt Certificate for wildcard domains without CloudFlare DNS.'); return; } - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; + data.meta.cloudflare_use = data.meta.cloudflare_use === '1'; + data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; } else { data.certificate_id = parseInt(data.certificate_id, 10); } @@ -163,6 +181,8 @@ module.exports = Mn.View.extend({ } this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + this.ui.save.addClass('btn-loading'); + method(data) .then(result => { view.model.set(result); @@ -176,6 +196,7 @@ module.exports = Mn.View.extend({ .catch(err => { alert(err.message); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + this.ui.save.removeClass('btn-loading'); }); } }, @@ -203,7 +224,7 @@ module.exports = Mn.View.extend({ text: input }; }, - createFilter: /^(?:\*\.)?(?:[^.*]+\.?)+[^.]$/ + createFilter: /^(?:\.)?(?:[^.*]+\.?)+[^.]$/ }); // Access Lists diff --git a/frontend/js/app/nginx/redirection/form.ejs b/frontend/js/app/nginx/redirection/form.ejs index 7cdb8a3..7d49769 100644 --- a/frontend/js/app/nginx/redirection/form.ejs +++ b/frontend/js/app/nginx/redirection/form.ejs @@ -97,6 +97,23 @@ </div> </div> + <!-- CloudFlare --> + <div class="col-sm-12 col-md-12 letsencrypt"> + <div class="form-group"> + <label class="custom-switch"> + <input type="checkbox" class="custom-switch-input" name="meta[cloudflare_use]" value="1"> + <span class="custom-switch-indicator"></span> + <span class="custom-switch-description"><%= i18n('ssl', 'use-cloudflare') %></span> + </label> + </div> + </div> + <div class="col-sm-12 col-md-12 cloudflare letsencrypt"> + <div class="form-group"> + <label class="form-label">CloudFlare DNS API Token <span class="form-required">*</span></label> + <input type="text" name="meta[cloudflare_token]" class="form-control" id="cloudflare_token"> + </div> + </div> + <!-- Lets encrypt --> <div class="col-sm-12 col-md-12 letsencrypt"> <div class="form-group"> diff --git a/frontend/js/app/nginx/redirection/form.js b/frontend/js/app/nginx/redirection/form.js index 0cef1a3..4e5b168 100644 --- a/frontend/js/app/nginx/redirection/form.js +++ b/frontend/js/app/nginx/redirection/form.js @@ -23,6 +23,9 @@ module.exports = Mn.View.extend({ hsts_enabled: 'input[name="hsts_enabled"]', hsts_subdomains: 'input[name="hsts_subdomains"]', http2_support: 'input[name="http2_support"]', + cloudflare_switch: 'input[name="meta[cloudflare_use]"]', + cloudflare_token: 'input[name="meta[cloudflare_token]"', + cloudflare: '.cloudflare', letsencrypt: '.letsencrypt' }, @@ -31,6 +34,7 @@ module.exports = Mn.View.extend({ let id = this.ui.certificate_select.val(); if (id === 'new') { this.ui.letsencrypt.show().find('input').prop('disabled', false); + this.ui.cloudflare.hide(); } else { this.ui.letsencrypt.hide().find('input').prop('disabled', true); } @@ -76,6 +80,17 @@ module.exports = Mn.View.extend({ } }, + 'change @ui.cloudflare_switch': function() { + let checked = this.ui.cloudflare_switch.prop('checked'); + if (checked) { + this.ui.cloudflare_token.prop('required', 'required'); + this.ui.cloudflare.show(); + } else { + this.ui.cloudflare_token.prop('required', false); + this.ui.cloudflare.hide(); + } + }, + 'click @ui.save': function (e) { e.preventDefault(); @@ -100,20 +115,23 @@ module.exports = Mn.View.extend({ } // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { + if (data.certificate_id === 'new') { let domain_err = false; - data.domain_names.map(function (name) { - if (name.match(/\*/im)) { - domain_err = true; - } - }); + if (!data.meta.cloudflare_use) { + data.domain_names.map(function (name) { + if (name.match(/\*/im)) { + domain_err = true; + } + }); + } if (domain_err) { - alert('Cannot request Let\'s Encrypt Certificate for wildcard domains'); + alert('Cannot request Let\'s Encrypt Certificate for wildcard domains without CloudFlare DNS.'); return; } - data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; + data.meta.cloudflare_use = data.meta.cloudflare_use === '1'; + data.meta.letsencrypt_agree = data.meta.letsencrypt_agree === '1'; } else { data.certificate_id = parseInt(data.certificate_id, 10); } @@ -129,6 +147,8 @@ module.exports = Mn.View.extend({ } this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + this.ui.save.addClass('btn-loading'); + method(data) .then(result => { view.model.set(result); @@ -142,6 +162,7 @@ module.exports = Mn.View.extend({ .catch(err => { alert(err.message); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + this.ui.save.removeClass('btn-loading'); }); } }, From a5616056534bee2f70bd11e1af1a68c4e10512ea Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit <jaap-jan@dodotech.dev> Date: Mon, 24 Aug 2020 09:09:52 +0000 Subject: [PATCH 16/17] show in ssl certificates list that CloudFlare is used --- frontend/js/app/nginx/certificates/list/item.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/js/app/nginx/certificates/list/item.ejs b/frontend/js/app/nginx/certificates/list/item.ejs index e7ee216..857a08b 100644 --- a/frontend/js/app/nginx/certificates/list/item.ejs +++ b/frontend/js/app/nginx/certificates/list/item.ejs @@ -28,7 +28,7 @@ </div> </td> <td> - <%- i18n('ssl', provider) %> + <%- i18n('ssl', provider) %><% if (meta.cloudflare_use) { %> - CloudFlare DNS<% } %> </td> <td class="<%- isExpired() ? 'text-danger' : '' %>"> <%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %> From a6b9bd7b01f6771c529ccf6a605575fb6d8506dc Mon Sep 17 00:00:00 2001 From: Jamie Curnow <jc@jc21.com> Date: Thu, 3 Sep 2020 14:11:44 +1000 Subject: [PATCH 17/17] Version bump and contributors --- .version | 2 +- README.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.version b/.version index 9183195..fad066f 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.4.0 \ No newline at end of file +2.5.0 \ No newline at end of file diff --git a/README.md b/README.md index 62bf6af..b82e515 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ <p align="center"> <img src="https://nginxproxymanager.com/github.png"> <br><br> - <img src="https://img.shields.io/badge/version-2.4.0-green.svg?style=for-the-badge"> + <img src="https://img.shields.io/badge/version-2.5.0-green.svg?style=for-the-badge"> <a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge"> </a> @@ -173,6 +173,18 @@ Special thanks to the following contributors: <br /><sub><b>vrenjith</b></sub> </a> </td> + <td align="center"> + <a href="https://github.com/duhruh"> + <img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80px;" alt=""/> + <br /><sub><b>David Rivera</b></sub> + </a> + </td> + <td align="center"> + <a href="https://github.com/jipjan"> + <img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80px;" alt=""/> + <br /><sub><b>Jaap-Jan de Wit</b></sub> + </a> + </td> </tr> </table> <!-- markdownlint-enable -->