From 13b5220b930f327b898c46676b86b4b715ba0d7a Mon Sep 17 00:00:00 2001
From: MattIPv4 <me@mattcowley.co.uk>
Date: Thu, 11 Jun 2020 15:46:51 +0100
Subject: [PATCH] Add global reverse proxy timeout settings (fixes #74)

---
 src/nginxconfig/generators/conf/proxy.conf.js |   8 +-
 .../generators/conf/website.conf.js           |   2 +-
 src/nginxconfig/generators/index.js           |   2 +-
 src/nginxconfig/i18n/en/common.js             |   2 +
 .../domain_sections/reverse_proxy.js          |   9 +-
 .../en/templates/global_sections/index.js     |   3 +-
 .../global_sections/reverse_proxy.js          |  32 +++
 .../domain_sections/reverse_proxy.vue         |  16 +-
 .../templates/global_sections/index.js        |   1 +
 .../global_sections/reverse_proxy.vue         | 213 ++++++++++++++++++
 10 files changed, 271 insertions(+), 17 deletions(-)
 create mode 100644 src/nginxconfig/i18n/en/templates/global_sections/reverse_proxy.js
 create mode 100644 src/nginxconfig/templates/global_sections/reverse_proxy.vue

diff --git a/src/nginxconfig/generators/conf/proxy.conf.js b/src/nginxconfig/generators/conf/proxy.conf.js
index 1164877..3f136f3 100644
--- a/src/nginxconfig/generators/conf/proxy.conf.js
+++ b/src/nginxconfig/generators/conf/proxy.conf.js
@@ -24,12 +24,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 */
 
-export default () => {
+export default global => {
     const config = {};
 
     config.proxy_http_version = '1.1';
     config.proxy_cache_bypass = '$http_upgrade';
 
+    config['# Proxy headers'] = '';
     config['proxy_set_header Upgrade'] = '$http_upgrade';
     config['proxy_set_header Connection'] = '"upgrade"';
     config['proxy_set_header Host'] = '$host';
@@ -39,6 +40,11 @@ export default () => {
     config['proxy_set_header X-Forwarded-Host'] = '$host';
     config['proxy_set_header X-Forwarded-Port'] = '$server_port';
 
+    config['# Proxy timeouts'] = '';
+    config['proxy_connect_timeout'] = global.reverseProxy.proxyConnectTimeout.computed;
+    config['proxy_send_timeout'] = global.reverseProxy.proxySendTimeout.computed;
+    config['proxy_read_timeout'] = global.reverseProxy.proxyReadTimeout.computed;
+
     // Done!
     return config;
 };
diff --git a/src/nginxconfig/generators/conf/website.conf.js b/src/nginxconfig/generators/conf/website.conf.js
index 06ddc53..393a13c 100644
--- a/src/nginxconfig/generators/conf/website.conf.js
+++ b/src/nginxconfig/generators/conf/website.conf.js
@@ -200,7 +200,7 @@ export default (domain, domains, global) => {
             locConf.push(['include', 'nginxconfig.io/proxy.conf']);
         } else {
             // Unified
-            locConf.push(...Object.entries(proxyConf()));
+            locConf.push(...Object.entries(proxyConf(global)));
         }
 
         serverConfig.push(['# reverse proxy', '']);
diff --git a/src/nginxconfig/generators/index.js b/src/nginxconfig/generators/index.js
index 89f9706..89d41d5 100644
--- a/src/nginxconfig/generators/index.js
+++ b/src/nginxconfig/generators/index.js
@@ -70,7 +70,7 @@ export default (domains, global) => {
 
         // Reverse proxy
         if (domains.some(d => d.reverseProxy.reverseProxy.computed))
-            files['nginxconfig.io/proxy.conf'] = toConf(proxyConf());
+            files['nginxconfig.io/proxy.conf'] = toConf(proxyConf(global));
 
         // WordPress
         if (domains.some(d => d.php.wordPressRules.computed))
diff --git a/src/nginxconfig/i18n/en/common.js b/src/nginxconfig/i18n/en/common.js
index 1bfe8c8..42c1450 100644
--- a/src/nginxconfig/i18n/en/common.js
+++ b/src/nginxconfig/i18n/en/common.js
@@ -40,4 +40,6 @@ export default {
 	magento: 'Magento',
 	django: 'Django',
 	logging: 'Logging',
+    reverseProxy: 'Reverse proxy',
+    reverseProxyLower: 'reverse proxy',
 };
diff --git a/src/nginxconfig/i18n/en/templates/domain_sections/reverse_proxy.js b/src/nginxconfig/i18n/en/templates/domain_sections/reverse_proxy.js
index 2870b80..5a7ed97 100644
--- a/src/nginxconfig/i18n/en/templates/domain_sections/reverse_proxy.js
+++ b/src/nginxconfig/i18n/en/templates/domain_sections/reverse_proxy.js
@@ -27,10 +27,9 @@ THE SOFTWARE.
 import common from '../../common';
 
 export default {
-    reverseProxy: 'Reverse proxy',
-    reverseProxyIsDisabled: 'Reverse proxy is disabled.',
-    reverseProxyCannotBeEnabledWithPhp: `Reverse proxy cannot be enabled whilst ${common.php} is enabled.`,
-    reverseProxyCannotBeEnabledWithPython: `Reverse proxy cannot be enabled whilst ${common.python} is enabled.`,
-    enableReverseProxy: `${common.enable} reverse proxy`,
+    reverseProxyIsDisabled: `${common.reverseProxy} is disabled.`,
+    reverseProxyCannotBeEnabledWithPhp: `${common.reverseProxy} cannot be enabled whilst ${common.php} is enabled.`,
+    reverseProxyCannotBeEnabledWithPython: `${common.reverseProxy} cannot be enabled whilst ${common.python} is enabled.`,
+    enableReverseProxy: `${common.enable} ${common.reverseProxyLower}`,
     path: 'Path',
 };
diff --git a/src/nginxconfig/i18n/en/templates/global_sections/index.js b/src/nginxconfig/i18n/en/templates/global_sections/index.js
index 0805d80..32c3a17 100644
--- a/src/nginxconfig/i18n/en/templates/global_sections/index.js
+++ b/src/nginxconfig/i18n/en/templates/global_sections/index.js
@@ -30,7 +30,8 @@ import nginx from './nginx';
 import performance from './performance';
 import php from './php';
 import python from './python';
+import reverseProxy from './reverse_proxy';
 import security from './security';
 import tools from './tools';
 
-export default { https, logging, nginx, performance, php, python, security, tools };
+export default { https, logging, nginx, performance, php, python, reverseProxy, security, tools };
diff --git a/src/nginxconfig/i18n/en/templates/global_sections/reverse_proxy.js b/src/nginxconfig/i18n/en/templates/global_sections/reverse_proxy.js
new file mode 100644
index 0000000..612a9aa
--- /dev/null
+++ b/src/nginxconfig/i18n/en/templates/global_sections/reverse_proxy.js
@@ -0,0 +1,32 @@
+/*
+Copyright 2020 DigitalOcean
+
+This code is licensed under the MIT License.
+You may obtain a copy of the License at
+https://github.com/digitalocean/nginxconfig.io/blob/master/LICENSE or https://mit-license.org/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions :
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+import common from '../../common';
+
+export default {
+    reverseProxyMustBeEnabledOnOneSite: `${common.reverseProxy} must be enabled on at least one site to configure global ${common.reverseProxyLower} settings.`,
+    seconds: 'seconds',
+};
diff --git a/src/nginxconfig/templates/domain_sections/reverse_proxy.vue b/src/nginxconfig/templates/domain_sections/reverse_proxy.vue
index 5d19763..aa842d1 100644
--- a/src/nginxconfig/templates/domain_sections/reverse_proxy.vue
+++ b/src/nginxconfig/templates/domain_sections/reverse_proxy.vue
@@ -28,7 +28,7 @@ THE SOFTWARE.
     <div>
         <div v-if="!reverseProxyEnabled" class="field is-horizontal is-aligned-top">
             <div class="field-label">
-                <label class="label">{{ i18n.templates.domainSections.reverseProxy.reverseProxy }}</label>
+                <label class="label">{{ i18n.common.reverseProxy }}</label>
             </div>
             <div class="field-body">
                 <div class="field">
@@ -49,7 +49,7 @@ THE SOFTWARE.
 
         <div v-else class="field is-horizontal">
             <div class="field-label">
-                <label class="label">{{ i18n.templates.domainSections.reverseProxy.reverseProxy }}</label>
+                <label class="label">{{ i18n.common.reverseProxy }}</label>
             </div>
             <div class="field-body">
                 <div :class="`field${reverseProxyChanged ? ' is-changed' : ''}`">
@@ -123,22 +123,22 @@ THE SOFTWARE.
     };
 
     export default {
-        name: 'DomainReverseProxy',                                         // Component name
-        display: i18n.templates.domainSections.reverseProxy.reverseProxy,   // Display name for tab
-        key: 'reverseProxy',                                                // Key for data in parent
-        delegated: delegatedFromDefaults(defaults),                         // Data the parent will present here
+        name: 'DomainReverseProxy',                                 // Component name
+        display: i18n.common.reverseProxy,                          // Display name for tab
+        key: 'reverseProxy',                                        // Key for data in parent
+        delegated: delegatedFromDefaults(defaults),                 // Data the parent will present here
         components: {
             PrettyCheck,
         },
         props: {
-            data: Object,                                                   // Data delegated back to us from parent
+            data: Object,                                           // Data delegated back to us from parent
         },
         data () {
             return {
                 i18n,
             };
         },
-        computed: computedFromDefaults(defaults, 'reverseProxy'),           // Getters & setters for the delegated data
+        computed: computedFromDefaults(defaults, 'reverseProxy'),   // Getters & setters for the delegated data
         watch: {
             // If the PHP or Python is enabled, the Reverse proxy will be forced off
             '$parent.$props.data': {
diff --git a/src/nginxconfig/templates/global_sections/index.js b/src/nginxconfig/templates/global_sections/index.js
index 28b123f..1b850bc 100644
--- a/src/nginxconfig/templates/global_sections/index.js
+++ b/src/nginxconfig/templates/global_sections/index.js
@@ -28,6 +28,7 @@ export { default as HTTPS } from './https';
 export { default as Security } from './security';
 export { default as PHP } from './php';
 export { default as Python } from './python';
+export { default as ReverseProxy } from './reverse_proxy';
 export { default as Performance } from './performance';
 export { default as Logging } from './logging';
 export { default as NGINX } from './nginx';
diff --git a/src/nginxconfig/templates/global_sections/reverse_proxy.vue b/src/nginxconfig/templates/global_sections/reverse_proxy.vue
new file mode 100644
index 0000000..520ffba
--- /dev/null
+++ b/src/nginxconfig/templates/global_sections/reverse_proxy.vue
@@ -0,0 +1,213 @@
+<!--
+Copyright 2020 DigitalOcean
+
+This code is licensed under the MIT License.
+You may obtain a copy of the License at
+https://github.com/digitalocean/nginxconfig.io/blob/master/LICENSE or https://mit-license.org/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions :
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+-->
+
+<template>
+    <div>
+        <div v-if="!reverseProxyEnabled" class="field is-horizontal is-aligned-top">
+            <div class="field-label">
+                <label class="label">{{ i18n.common.reverseProxy }}</label>
+            </div>
+            <div class="field-body">
+                <div class="field">
+                    <div class="control">
+                        <label class="text">
+                            {{ i18n.templates.globalSections.reverseProxy.reverseProxyMustBeEnabledOnOneSite }}
+                        </label>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <template v-else>
+            <div class="field is-horizontal">
+                <div class="field-label">
+                    <label class="label">proxy_connect_timeout</label>
+                </div>
+                <div class="field-body">
+                    <div class="field has-addons">
+                        <div :class="`control is-expanded${proxyConnectTimeoutChanged ? ' is-changed' : ''}`">
+                            <input v-model.number="proxyConnectTimeout"
+                                   class="input"
+                                   type="number"
+                                   min="0"
+                                   step="1"
+                                   :placeholder="$props.data.proxyConnectTimeout.default"
+                            />
+                        </div>
+                        <div class="control">
+                            <a class="button is-static">
+                                {{ i18n.templates.globalSections.reverseProxy.seconds }}
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="field is-horizontal">
+                <div class="field-label">
+                    <label class="label">proxy_send_timeout</label>
+                </div>
+                <div class="field-body">
+                    <div class="field has-addons">
+                        <div :class="`control is-expanded${proxySendTimeoutChanged ? ' is-changed' : ''}`">
+                            <input v-model.number="proxySendTimeout"
+                                   class="input"
+                                   type="number"
+                                   min="0"
+                                   step="1"
+                                   :placeholder="$props.data.proxySendTimeout.default"
+                            />
+                        </div>
+                        <div class="control">
+                            <a class="button is-static">
+                                {{ i18n.templates.globalSections.reverseProxy.seconds }}
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="field is-horizontal">
+                <div class="field-label">
+                    <label class="label">proxy_read_timeout</label>
+                </div>
+                <div class="field-body">
+                    <div class="field has-addons">
+                        <div :class="`control is-expanded${proxyReadTimeoutChanged ? ' is-changed' : ''}`">
+                            <input v-model.number="proxyReadTimeout"
+                                   class="input"
+                                   type="number"
+                                   min="0"
+                                   step="1"
+                                   :placeholder="$props.data.proxyReadTimeout.default"
+                            />
+                        </div>
+                        <div class="control">
+                            <a class="button is-static">
+                                {{ i18n.templates.globalSections.reverseProxy.seconds }}
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </template>
+    </div>
+</template>
+
+<script>
+    import i18n from '../../i18n';
+    import delegatedFromDefaults from '../../util/delegated_from_defaults';
+    import computedFromDefaults from '../../util/computed_from_defaults';
+
+    const defaults = {
+        proxyConnectTimeout: {
+            default: 60,
+            computed: '60s', // We use a watcher to append 's'
+            enabled: false,
+        },
+        proxySendTimeout: {
+            default: 60,
+            computed: '60s', // We use a watcher to append 's'
+            enabled: false,
+        },
+        proxyReadTimeout: {
+            default: 60,
+            computed: '60s', // We use a watcher to append 's'
+            enabled: false,
+        },
+    };
+
+    const validTimeout = data => {
+        let val = parseFloat(data.computed);
+
+        // Use default if we've got an invalid setting
+        if (isNaN(val)) {
+            val = data.default;
+        }
+
+        // Set the value with 's' appended
+        data.computed = `${val}s`;
+    };
+
+    export default {
+        name: 'GlobalReverseProxy',                                 // Component name
+        display: i18n.common.reverseProxy,                          // Display name for tab
+        key: 'reverseProxy',                                        // Key for data in parent
+        delegated: delegatedFromDefaults(defaults),                 // Data the parent will present here
+        props: {
+            data: Object,                                           // Data delegated back to us from parent
+        },
+        data() {
+            return {
+                i18n,
+                reverseProxyEnabled: false,
+            };
+        },
+        computed: computedFromDefaults(defaults, 'reverseProxy'),   // Getters & setters for the delegated data
+        watch: {
+            // Disable all options if Reverse proxy is disabled
+            '$parent.$parent.$data.domains': {
+                handler(data) {
+                    for (const domain of data) {
+                        if (domain && domain.reverseProxy && domain.reverseProxy.reverseProxy
+                            && domain.reverseProxy.reverseProxy.computed) {
+                            this.$data.reverseProxyEnabled = true;
+                            this.$props.data.proxyConnectTimeout.enabled = true;
+                            this.$props.data.proxyConnectTimeout.computed = this.$props.data.proxyConnectTimeout.value;
+                            this.$props.data.proxySendTimeout.enabled = true;
+                            this.$props.data.proxySendTimeout.computed = this.$props.data.proxySendTimeout.value;
+                            this.$props.data.proxyReadTimeout.enabled = true;
+                            this.$props.data.proxyReadTimeout.computed = this.$props.data.proxyReadTimeout.value;
+                            return;
+                        }
+                    }
+
+                    this.$data.reverseProxyEnabled = false;
+                    this.$props.data.proxyConnectTimeout.enabled = false;
+                    this.$props.data.proxyConnectTimeout.computed = '';
+                    this.$props.data.proxySendTimeout.enabled = false;
+                    this.$props.data.proxySendTimeout.computed = '';
+                    this.$props.data.proxyReadTimeout.enabled = false;
+                    this.$props.data.proxyReadTimeout.computed = '';
+                },
+                deep: true,
+            },
+            // Ensure the timeouts are valid numbers
+            '$props.data.proxyConnectTimeout': {
+                handler: validTimeout,
+                deep: true,
+            },
+            '$props.data.proxySendTimeout': {
+                handler: validTimeout,
+                deep: true,
+            },
+            '$props.data.proxyReadTimeout': {
+                handler: validTimeout,
+                deep: true,
+            },
+        },
+    };
+</script>