Repository: certbot/certbot

Issue:
Don't use the default UA when verifying challenges
While related to #858, I feel like this is a slightly different issue. We have had reports of people running into problems with the User-Agent when using the `manual` plugin. `manual` uses `acme` to validate that the challenge has been set up correctly before responding to the server. This is done using the `requests` library whose default UA as of writing this e-mail is `python-requests/2.8.1`. Some webservers are rejecting requests with this UA, causing self validation to fail, and `letsencrypt` to exit.



Discussion:
Perhaps, we should use the UA `acme-client/ver`?

Another suggestion I've received was `Mozilla/5.0 (compatible; Let's Encrypt client self-validation; +https://www.letsencrypt.org)`.

Do we need to pretend to be `Mozilla`? I don't think so.
Web browsers say `Mozilla` for the historical reason, because they want the "modern" version of the web pages, but we are not a web browser, and we do not need the "modern" web pages.


Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -481,11 +481,13 @@ class ClientNetwork(object):
     JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
     REPLAY_NONCE_HEADER = 'Replay-Nonce'
 
-    def __init__(self, key, alg=jose.RS256, verify_ssl=True):
+    def __init__(self, key, alg=jose.RS256, verify_ssl=True,
+                 user_agent='acme-python'):
         self.key = key
         self.alg = alg
         self.verify_ssl = verify_ssl
         self._nonces = set()
+        self.user_agent = user_agent
 
     def _wrap_in_jws(self, obj, nonce):
         """Wrap `JSONDeSerializable` object in JWS.
@@ -578,6 +580,8 @@ def _send_request(self, method, url, *args, **kwargs):
         logging.debug('Sending %s request to %s. args: %r, kwargs: %r',
                       method, url, args, kwargs)
         kwargs['verify'] = self.verify_ssl
+        kwargs.setdefault('headers', {})
+        kwargs['headers'].setdefault('User-Agent', self.user_agent)
         response = requests.request(method, url, *args, **kwargs)
         logging.debug('Received %s. Headers: %s. Content: %r',
                       response, response.headers, response.content)


Test patch:
diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py
--- a/acme/acme/client_test.py
+++ b/acme/acme/client_test.py
@@ -396,7 +396,8 @@ def setUp(self):
 
         from acme.client import ClientNetwork
         self.net = ClientNetwork(
-            key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl)
+            key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl,
+            user_agent='acme-python-test')
 
         self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
         self.response.headers = {}
@@ -479,7 +480,7 @@ def test_send_request(self, mock_requests):
         self.assertEqual(self.response, self.net._send_request(
             'HEAD', 'url', 'foo', bar='baz'))
         mock_requests.request.assert_called_once_with(
-            'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz')
+            'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY)
 
     @mock.patch('acme.client.requests')
     def test_send_request_verify_ssl(self, mock_requests):
@@ -492,7 +493,20 @@ def test_send_request_verify_ssl(self, mock_requests):
             self.assertEqual(
                 self.response, self.net._send_request('GET', 'url'))
             mock_requests.request.assert_called_once_with(
-                'GET', 'url', verify=verify)
+                'GET', 'url', verify=verify, headers=mock.ANY)
+
+    @mock.patch('acme.client.requests')
+    def test_send_request_user_agent(self, mock_requests):
+        mock_requests.request.return_value = self.response
+        # pylint: disable=protected-access
+        self.net._send_request('GET', 'url', headers={'bar': 'baz'})
+        mock_requests.request.assert_called_once_with(
+            'GET', 'url', verify=mock.ANY,
+            headers={'User-Agent': 'acme-python-test', 'bar': 'baz'})
+
+        self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'})
+        mock_requests.request.assert_called_with(
+            'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'})
 
     @mock.patch('acme.client.requests')
     def test_requests_error_passthrough(self, mock_requests):