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 -->