From 09b1e27c1f8f3ea964388a1bb2f285005462817c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 29 Aug 2022 08:33:05 +0200
Subject: [PATCH 2/2] general: give a timeout to all HTTP requests (#68470)

---
 src/authentic2/api_views.py                              | 4 +++-
 src/authentic2/http_utils.py                             | 3 ++-
 src/authentic2/saml/common.py                            | 4 +++-
 src/authentic2/saml/forms.py                             | 3 ++-
 src/authentic2/saml/management/commands/sync-metadata.py | 3 ++-
 src/authentic2/saml/models.py                            | 2 +-
 src/authentic2/settings.py                               | 4 ++++
 src/authentic2_auth_oidc/backends.py                     | 2 ++
 src/authentic2_idp_cas/views.py                          | 5 ++++-
 9 files changed, 23 insertions(+), 7 deletions(-)

diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py
index d66db13c4..7b545fe24 100644
--- a/src/authentic2/api_views.py
+++ b/src/authentic2/api_views.py
@@ -1487,7 +1487,9 @@ class AddressAutocompleteAPI(APIView):
         if not getattr(settings, 'ADDRESS_AUTOCOMPLETE_URL', None):
             return Response({})
         try:
-            response = requests.get(settings.ADDRESS_AUTOCOMPLETE_URL, params=request.GET)
+            response = requests.get(
+                settings.ADDRESS_AUTOCOMPLETE_URL, params=request.GET, timeout=settings.REQUESTS_TIMEOUT
+            )
             response.raise_for_status()
             return Response(response.json())
         except RequestException:
diff --git a/src/authentic2/http_utils.py b/src/authentic2/http_utils.py
index ee53621fc..98917211c 100644
--- a/src/authentic2/http_utils.py
+++ b/src/authentic2/http_utils.py
@@ -16,6 +16,7 @@
 
 
 import requests
+from django.conf import settings
 
 from authentic2 import app_settings
 
@@ -25,4 +26,4 @@ def get_url(url):
     verify = app_settings.A2_VERIFY_SSL
     if verify and app_settings.CAFILE:
         verify = app_settings.CAFILE
-    return requests.get(url, verify=verify).text
+    return requests.get(url, verify=verify, timeout=settings.REQUESTS_TIMEOUT).text
diff --git a/src/authentic2/saml/common.py b/src/authentic2/saml/common.py
index 045ab40de..9fb9ccf80 100644
--- a/src/authentic2/saml/common.py
+++ b/src/authentic2/saml/common.py
@@ -466,7 +466,9 @@ def soap_call(url, msg):
     logger = logging.getLogger(__name__)
     try:
         logger.debug('SOAP call to %r with data %r', url, msg[:10000])
-        response = requests.post(url, data=msg, headers={'Content-Type': 'text/xml'})
+        response = requests.post(
+            url, data=msg, headers={'Content-Type': 'text/xml'}, timeout=settings.REQUESTS_TIMEOUT
+        )
         response.raise_for_status()
     except requests.RequestException as e:
         logging.error('SOAP call to %r error %s with data %r', url, e, msg[:10000])
diff --git a/src/authentic2/saml/forms.py b/src/authentic2/saml/forms.py
index 904a3e80f..cfb5f720b 100644
--- a/src/authentic2/saml/forms.py
+++ b/src/authentic2/saml/forms.py
@@ -18,6 +18,7 @@ import xml.etree.ElementTree as ET
 
 import requests
 from django import forms
+from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.utils.encoding import force_text
 from django.utils.translation import ugettext_lazy as _
@@ -49,7 +50,7 @@ class AddLibertyProviderFromUrlForm(forms.Form):
         self.childs = []
         if name and slug and url:
             try:
-                response = requests.get(url)
+                response = requests.get(url, timeout=settings.REQUESTS_TIMEOUT)
                 response.raise_for_status()
                 content = force_text(response.content)
             except requests.RequestException as e:
diff --git a/src/authentic2/saml/management/commands/sync-metadata.py b/src/authentic2/saml/management/commands/sync-metadata.py
index cfaa59514..9a9a00bd5 100644
--- a/src/authentic2/saml/management/commands/sync-metadata.py
+++ b/src/authentic2/saml/management/commands/sync-metadata.py
@@ -22,6 +22,7 @@ import warnings
 import xml.etree.ElementTree as etree
 
 import requests
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.management.base import BaseCommand, CommandError
 from django.db.transaction import atomic
@@ -341,7 +342,7 @@ Any other kind of attribute filter policy is unsupported.
         source = options['source']
         metadata_file_path = options['metadata_file_path']
         if metadata_file_path.startswith('http://') or metadata_file_path.startswith('https://'):
-            response = requests.get(metadata_file_path)
+            response = requests.get(metadata_file_path, timeout=settings.REQUESTS_TIMEOUT)
             if not response.ok:
                 raise CommandError('Unable to open url %s' % metadata_file_path)
             metadata_file = io.BytesIO(response.content)
diff --git a/src/authentic2/saml/models.py b/src/authentic2/saml/models.py
index d341d2eba..6e9f93eb6 100644
--- a/src/authentic2/saml/models.py
+++ b/src/authentic2/saml/models.py
@@ -414,7 +414,7 @@ class LibertyProvider(Service):
         try:
             if not self.metadata_url:
                 raise ValidationError(_('No metadata URL'))
-            response = requests.get(self.metadata_url)
+            response = requests.get(self.metadata_url, timeout=settings.REQUESTS_TIMEOUT)
         except requests.RequestException as e:
             raise ValidationError(_('Retrieval of metadata failed: %s') % e)
         else:
diff --git a/src/authentic2/settings.py b/src/authentic2/settings.py
index 393b58ee7..db8f26b34 100644
--- a/src/authentic2/settings.py
+++ b/src/authentic2/settings.py
@@ -332,6 +332,10 @@ MELLON_LOOKUP_BY_ATTRIBUTES = [
     {"saml_attribute": "username", "user_field": "username"},
 ]
 
+# timeout used in python-requests call, in seconds
+# we use 28s by default: timeout just before web server, which is usually 30s
+REQUESTS_TIMEOUT = 28
+
 # Permissions
 
 DJANGO_RBAC_PERMISSIONS_HIERARCHY = {
diff --git a/src/authentic2_auth_oidc/backends.py b/src/authentic2_auth_oidc/backends.py
index 49ffbcae9..e8a9da024 100644
--- a/src/authentic2_auth_oidc/backends.py
+++ b/src/authentic2_auth_oidc/backends.py
@@ -18,6 +18,7 @@ import datetime
 import logging
 
 import requests
+from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.contrib.auth.backends import ModelBackend
 from django.db.transaction import atomic
@@ -163,6 +164,7 @@ class OIDCBackend(ModelBackend):
                     headers={
                         'Authorization': 'Bearer %s' % access_token,
                     },
+                    timeout=settings.REQUESTS_TIMEOUT,
                 )
                 response.raise_for_status()
             except requests.RequestException as e:
diff --git a/src/authentic2_idp_cas/views.py b/src/authentic2_idp_cas/views.py
index 63a04c6b2..cda7dcf1d 100644
--- a/src/authentic2_idp_cas/views.py
+++ b/src/authentic2_idp_cas/views.py
@@ -20,6 +20,7 @@ from datetime import timedelta
 from xml.etree import ElementTree as ET
 
 import requests
+from django.conf import settings
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.utils.timezone import now
 from django.views.generic.base import View
@@ -369,7 +370,9 @@ class ServiceValidateView(ValidateBaseView):
         # Skip PGT_URL check for testing purpose
         # instead store PGT_IOU / PGT association in session
         if app_settings.CHECK_PGT_URL:
-            response = requests.get(pgt_url, params={PGT_ID_PARAM: pgt, PGT_IOU_PARAM: pgt_iou})
+            response = requests.get(
+                pgt_url, params={PGT_ID_PARAM: pgt, PGT_IOU_PARAM: pgt_iou}, timeout=settings.REQUESTS_TIMEOUT
+            )
             if response.status_code != 200:
                 self.logger.warning('pgtUrl %r returned non 200 code: %d', pgt_url, response.status_code)
                 return
-- 
2.37.2

