diff --git a/acme/src/acme/_internal/tests/client_test.py b/acme/src/acme/_internal/tests/client_test.py index 11d56cdd3..419bbda93 100644 --- a/acme/src/acme/_internal/tests/client_test.py +++ b/acme/src/acme/_internal/tests/client_test.py @@ -281,7 +281,9 @@ class ClientV2Test(unittest.TestCase): with pytest.raises(errors.Error, match="The certificate order failed"): self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9)) - def test_finalize_order_orderNotReady(self): + @mock.patch('acme.client.time.sleep') + @mock.patch('acme.client.datetime') + def test_finalize_order_orderNotReady(self, dt_mock, mock_sleep): # https://github.com/certbot/certbot/issues/9766 updated_order_processing = self.order.update(status=messages.STATUS_PROCESSING) updated_order_ready = self.order.update(status=messages.STATUS_READY) @@ -297,11 +299,18 @@ class ClientV2Test(unittest.TestCase): updated_order_ready.to_json(), updated_order_valid.to_json()] + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + self.response.headers['Retry-After'] = '50' + post = mock.MagicMock() post.side_effect = [messages.Error.with_code('orderNotReady'), # first begin_finalization - self.response, # first poll_finalization poll --> still returns pending + # sleep 1 + self.response, # first poll_finalization poll --> returns processing + # retry-after sleep here self.response, # second poll_finalization poll --> returns ready mock.MagicMock(), # second begin_finalization + # sleep 1 self.response, # third poll_finalization poll --> returns valid self.response # fetch cert ] @@ -309,6 +318,7 @@ class ClientV2Test(unittest.TestCase): self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9)) assert self.net.post.call_count == 6 + assert mock_sleep.call_args_list == [((1,),), ((50,),), ((1,),)] def test_finalize_order_otherErrorCode(self): post = mock.MagicMock() diff --git a/acme/src/acme/client.py b/acme/src/acme/client.py index 763554058..ffb989f19 100644 --- a/acme/src/acme/client.py +++ b/acme/src/acme/client.py @@ -253,9 +253,10 @@ class ClientV2: :returns: finalized order (with certificate) :rtype: messages.OrderResource """ - + sleep_seconds: float = 1 while datetime.datetime.now() < deadline: - time.sleep(1) + if sleep_seconds > 0: + time.sleep(sleep_seconds) response = self._post_as_get(orderr.uri) body = messages.Order.from_json(response.json()) if body.status == messages.STATUS_INVALID: @@ -271,6 +272,7 @@ class ClientV2: # fulfilled, and is awaiting finalization. Submit a finalization # request. self.begin_finalization(orderr) + sleep_seconds = 1 elif body.status == messages.STATUS_VALID and body.certificate is not None: # "valid": The server has issued the certificate and provisioned its # URL to the "certificate" field of the order. Download the @@ -282,6 +284,14 @@ class ClientV2: alt_chains = [self._post_as_get(url).text for url in alt_chains_urls] orderr = orderr.update(alternative_fullchains_pem=alt_chains) return orderr + elif body.status == messages.STATUS_PROCESSING: + # "processing": The certificate is being issued. Send a POST-as-GET request after + # the time given in the Retry-After header field of the response, if any. + retry_after = self.retry_after(response, 1) + # Whatever Retry-After the ACME server requests, the polling must not take + # longer than the overall deadline + retry_after = min(retry_after, deadline) + sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds() raise errors.TimeoutError() def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime, diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 08abbbd1b..4a2a6812a 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -28,7 +28,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). `ready`, and resubmits finalization request before polling for `valid` to download certificate. This conforms to RFC 8555 more accurately and avoids race conditions where all authorizations are fulfilled but order has not yet transitioned to ready state on - the server when the finalization request is sent. + the server when the finalization request is sent. It also respects retry-after when + polling for finalization readiness. * The --preferred-profile and --required-profile flags now have their values stored in the renewal configuration so the same setting will be used on renewal.