From 099dd66265dfdc94e162eba4c1ec3a33b305bc26 Mon Sep 17 00:00:00 2001
From: Tim Burke <tim.burke@gmail.com>
Date: Fri, 17 Mar 2023 16:14:15 -0700
Subject: [PATCH] Retry on SSLError

...provided it isn't something to do with certification validation.

Closes-Bug: #1641363
Closes-Bug: #2011731
Change-Id: Ic3acbf431e444fcd8bc0fe79571fe2db4140cf22
---
 swiftclient/client.py         |  9 ++++--
 test/unit/test_swiftclient.py | 58 ++++++++++++++++++++++++++++++++++-
 2 files changed, 64 insertions(+), 3 deletions(-)

diff --git a/swiftclient/client.py b/swiftclient/client.py
index b9f12aac..06350901 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -1798,8 +1798,13 @@ class Connection:
                           service_token=self.service_token, **kwargs)
                 self._add_response_dict(caller_response_dict, kwargs)
                 return rv
-            except SSLError:
-                raise
+            except SSLError as e:
+                self._add_response_dict(caller_response_dict, kwargs)
+                if ('certificate verify' in str(e)) or \
+                        ('hostname' in str(e)) or \
+                        self.attempts > self.retries:
+                    raise
+                self.http_conn = None
             except (socket.error, RequestException):
                 self._add_response_dict(caller_response_dict, kwargs)
                 if self.attempts > self.retries:
diff --git a/test/unit/test_swiftclient.py b/test/unit/test_swiftclient.py
index 55b4679a..42d470cd 100644
--- a/test/unit/test_swiftclient.py
+++ b/test/unit/test_swiftclient.py
@@ -25,7 +25,7 @@ import warnings
 import tempfile
 from hashlib import md5
 from urllib.parse import urlparse
-from requests.exceptions import RequestException
+from requests.exceptions import RequestException, SSLError
 
 from .utils import (MockHttpTest, fake_get_auth_keystone, StubResponse,
                     FakeKeystone)
@@ -2176,6 +2176,62 @@ class TestConnection(MockHttpTest):
         self.assertEqual(mock_auth.call_count, 1)
         self.assertEqual(conn.attempts, conn.retries + 1)
 
+    def test_no_retry_with_cert_sslerror(self):
+        def quick_sleep(*args):
+            pass
+        c.sleep = quick_sleep
+        for err in (
+            # Taken from real testing (requests==2.25.1, urllib3==1.26.5,
+            # pyOpenSSL==21.0.0) but note that these are actually way more
+            # messy/wrapped up in other exceptions
+            SSLError(
+                '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: '
+                'certificate has expired (_ssl.c:997)'),
+            SSLError(
+                "hostname 'wrong.host.badssl.com' doesn't match either of "
+                "'*.badssl.com', 'badssl.com'"),
+            SSLError(
+                '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: '
+                'self-signed certificate (_ssl.c:997)'),
+            SSLError(
+                '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: '
+                'self-signed certificate in certificate chain (_ssl.c:997)'),
+            SSLError(
+                '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: '
+                'unable to get local issuer certificate (_ssl.c:997)'),
+            SSLError(
+                '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: '
+                'CA signature digest algorithm too weak (_ssl.c:997)'),
+        ):
+            conn = c.Connection('http://www.test.com', 'asdf', 'asdf')
+            with self.subTest(err=err), mock.patch(
+                    'swiftclient.client.http_connection') as \
+                    fake_http_connection, \
+                    mock.patch('swiftclient.client.get_auth_1_0') as mock_auth:
+                mock_auth.return_value = ('http://mock.com', 'mock_token')
+                fake_http_connection.side_effect = err
+                self.assertRaises(socket.error, conn.head_account)
+                self.assertEqual(mock_auth.call_count, 1)
+                self.assertEqual(conn.attempts, 1)
+
+    def test_retry_with_non_cert_sslerror(self):
+        def quick_sleep(*args):
+            pass
+        c.sleep = quick_sleep
+        conn = c.Connection('http://www.test.com', 'asdf', 'asdf')
+        with mock.patch('swiftclient.client.http_connection') as \
+                fake_http_connection, \
+                mock.patch('swiftclient.client.get_auth_1_0') as mock_auth:
+            mock_auth.return_value = ('http://mock.com', 'mock_token')
+            fake_http_connection.side_effect = SSLError(
+                "HTTPSConnectionPool(host='example.com', port=443): "
+                "Max retries exceeded with url: /v1/AUTH_test (Caused by "
+                "SSLError(SSLZeroReturnError(6, 'TLS/SSL connection has "
+                "been closed (EOF) (_ssl.c:997)')))")
+            self.assertRaises(socket.error, conn.head_account)
+        self.assertEqual(mock_auth.call_count, 1)
+        self.assertEqual(conn.attempts, conn.retries + 1)
+
     def test_retry_with_force_auth_retry_exceptions(self):
         def quick_sleep(*args):
             pass