Repository: urllib3/urllib3

Issue:
Incorrect handling of Retry-After (2.7)
`email.utils.parsedate_tz` behaves differently on python2.7/3 if the input string has no timezone. On [python3](https://docs.python.org/3/library/email.utils.html#email.utils.parsedate_tz), the last item of the return value is `0` where on [python2.7](https://docs.python.org/2/library/email.utils.html#email.utils.parsedate_tz) it is `None`. 

Since `email.utils.mktime_tz` assumes a ```None``` timezone to be the local timezone, the resulting date might be wrong when calculating `Retry-After` timeout of a value without a timezone.

To reproduce run the following test on a python2.7 machine with timezone != UTC:
`test/test_retry.py::TestRetry::test_respect_retry_after_header_sleep[Mon Jun  3 11:30:12 2019-True-1812]`

(Luckily, `freezegun` doesn't hide the issue since it freezes only the time using `tz_offset` but it doesn't patch the timezone.)


Discussion:

Resolving patch:
diff --git a/src/urllib3/util/retry.py b/src/urllib3/util/retry.py
--- a/src/urllib3/util/retry.py
+++ b/src/urllib3/util/retry.py
@@ -269,6 +269,13 @@ def parse_retry_after(self, retry_after):
             retry_date_tuple = email.utils.parsedate_tz(retry_after)
             if retry_date_tuple is None:
                 raise InvalidHeader("Invalid Retry-After header: %s" % retry_after)
+            if retry_date_tuple[9] is None:  # Python 2
+                # Assume UTC if no timezone was specified
+                # On Python2.7, parsedate_tz returns None for a timezone offset
+                # instead of 0 if no timezone is given, where mktime_tz treats
+                # a None timezone offset as local time.
+                retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
+
             retry_date = email.utils.mktime_tz(retry_date_tuple)
             seconds = retry_date - time.time()
 


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -8,6 +8,8 @@
 import trustme
 from tornado import web, ioloop
 
+from .tz_stub import stub_timezone_ctx
+
 from dummyserver.handlers import TestingApp
 from dummyserver.server import run_tornado_app
 from dummyserver.server import HAS_IPV6
@@ -96,3 +98,12 @@ def ipv6_san_server(tmp_path_factory):
 
     with run_server_in_thread("https", "::1", tmpdir, ca, server_cert) as cfg:
         yield cfg
+
+
+@pytest.yield_fixture
+def stub_timezone(request):
+    """
+    A pytest fixture that runs the test with a stub timezone.
+    """
+    with stub_timezone_ctx(request.param):
+        yield
diff --git a/test/test_retry.py b/test/test_retry.py
--- a/test/test_retry.py
+++ b/test/test_retry.py
@@ -332,6 +332,16 @@ def test_respect_retry_after_header_propagated(self, respect_retry_after_header)
             ("Mon Jun  3 11:30:12 2019", True, 1812),
         ],
     )
+    @pytest.mark.parametrize(
+        "stub_timezone",
+        [
+            "UTC",
+            "Asia/Jerusalem",
+            None,
+        ],
+        indirect=True,
+    )
+    @pytest.mark.usefixtures("stub_timezone")
     def test_respect_retry_after_header_sleep(
         self, retry_after_header, respect_retry_after_header, sleep_duration
     ):
diff --git a/test/tz_stub.py b/test/tz_stub.py
new file mode 100644
--- /dev/null
+++ b/test/tz_stub.py
@@ -0,0 +1,39 @@
+from contextlib import contextmanager
+import time
+import datetime
+import os
+import pytest
+from dateutil import tz
+
+
+@contextmanager
+def stub_timezone_ctx(tzname):
+    """
+    Switch to a locally-known timezone specified by `tzname`.
+    On exit, restore the previous timezone.
+    If `tzname` is `None`, do nothing.
+    """
+    if tzname is None:
+        yield
+        return
+
+    # Only supported on Unix
+    if not hasattr(time, "tzset"):
+        pytest.skip("Timezone patching is not supported")
+
+    # Make sure the new timezone exists, at least in dateutil
+    new_tz = tz.gettz(tzname)
+    if new_tz is None:
+        raise ValueError("Invalid timezone specified: %r" % (tzname,))
+
+    # Get the current timezone
+    local_tz = tz.tzlocal()
+    if local_tz is None:
+        raise EnvironmentError("Cannot determine current timezone")
+    old_tzname = datetime.datetime.now(local_tz).tzname()
+
+    os.environ["TZ"] = tzname
+    time.tzset()
+    yield
+    os.environ["TZ"] = old_tzname
+    time.tzset()