From 3e149997e4d48b308c7656d0fd65c1e2aa347ee2 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Thu, 21 Apr 2022 18:11:30 +0200
Subject: [PATCH] middleware: add a maintenance middleware (#63939)

---
 debian/debian_config_common.py |  1 +
 hobo/middleware/maintenance.py | 50 ++++++++++++++++++++++++++++++++++
 tests/settings.py              |  1 +
 tests/test_maintenance.py      | 26 ++++++++++++++++++
 4 files changed, 78 insertions(+)
 create mode 100644 hobo/middleware/maintenance.py
 create mode 100644 tests/test_maintenance.py

diff --git a/debian/debian_config_common.py b/debian/debian_config_common.py
index 36fde1a..85dd4bc 100644
--- a/debian/debian_config_common.py
+++ b/debian/debian_config_common.py
@@ -268,6 +268,7 @@ if 'MIDDLEWARE' not in globals():
 MIDDLEWARE = (
     'hobo.middleware.VersionMiddleware',  # /__version__
     'hobo.middleware.cors.CORSMiddleware',
+    'hobo.middleware.maintenance.MaintenanceMiddleware',
 ) + MIDDLEWARE
 
 if PROJECT_NAME != 'wcs':
diff --git a/hobo/middleware/maintenance.py b/hobo/middleware/maintenance.py
new file mode 100644
index 0000000..b89d56d
--- /dev/null
+++ b/hobo/middleware/maintenance.py
@@ -0,0 +1,50 @@
+# hobo - portal to configure and deploy applications
+# Copyright (C) 2015-2022 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import dns.exception
+import dns.resolver
+from django.conf import settings
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+
+
+def pass_through(remote_addr):
+    pass_through_ips = getattr(settings, 'MAINTENANCE_PASS_THROUGH_IPS', [])
+    if remote_addr in pass_through_ips:
+        return True
+    pass_through_ddns = getattr(settings, 'MAINTENANCE_PASS_THROUGH_DDNS', None)
+    if pass_through_ddns:
+        domain = '.'.join(reversed(remote_addr.split('.'))) + '.' + pass_through_ddns
+        try:
+            answers = dns.resolver.query(domain, 'A', lifetime=1)
+            return any(answer.address for answer in answers)
+        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.exception.DNSException):
+            return False
+    return False
+
+
+class MaintenanceMiddleware:
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        maintenance_mode = getattr(settings, 'MAINTENANCE_MODE', None)
+        if maintenance_mode:
+            remote_addr = request.META.get('REMOTE_ADDR')
+            if not (remote_addr and pass_through(remote_addr)):
+                maintenance_msg = _('The site is under maintenance')
+                return HttpResponse('<h1>%s</h1>' % maintenance_msg, status=503)
+        return self.get_response(request)
diff --git a/tests/settings.py b/tests/settings.py
index 952c142..6022280 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -12,6 +12,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = True
 MIDDLEWARE = MIDDLEWARE + (
     'hobo.middleware.RobotsTxtMiddleware',
     'hobo.provisionning.middleware.ProvisionningMiddleware',
+    'hobo.middleware.maintenance.MaintenanceMiddleware',
 )
 
 HOBO_MANAGER_HOMEPAGE_URL_VAR = 'portal_agent_url'
diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py
new file mode 100644
index 0000000..4190e97
--- /dev/null
+++ b/tests/test_maintenance.py
@@ -0,0 +1,26 @@
+import mock
+from test_manager import login
+
+
+def test_maintenance_middleware(app, admin_user, db, monkeypatch, settings):
+    app = login(app)
+    resp = app.get('/')
+    assert resp.status_code == 200
+
+    settings.MAINTENANCE_MODE = True
+    resp = app.get('/', status=503)
+    assert 'The site is under maintenance' in resp.text
+
+    settings.MAINTENANCE_PASS_THROUGH_IPS = ['127.0.0.1']
+    resp = app.get('/')
+    assert resp.status_code == 200
+
+    settings.MAINTENANCE_PASS_THROUGH_IPS = []
+    resp = app.get('/', status=503)
+
+    settings.MAINTENANCE_PASS_THROUGH_DDNS = 'ddns.foo.bar'
+    with mock.patch('dns.resolver.resolve', return_value=[mock.Mock(address='127.0.0.2')]):
+        resp = app.get('/')
+        assert resp.status_code == 200
+    with mock.patch('dns.resolver.resolve', return_value=[]):
+        resp = app.get('/', status=503)
-- 
2.30.2

