Repository: certbot/certbot

Issue:
http-01 provisioned resources and newlines
For some reason the Litespeed Webserver for my shared hosting appears to add new-lines to text/plain files meaning that the acme challenge fails due to there being a "\n" at the end, it'd be great if LE accepted the \n or knew to remove the \n as I can't find a way to stop it having a \n in the HTTP response (I tried nano -L and echo).
It was returned as a text/plain file (ForceType text/plain in .htaccess) which I checked with HttpFox.
[Relevant section of log](http://pastebin.com/9g0MNYSS)



Discussion:
@jsha, how should the spec be interpreted here?

The same problem occurs with nginx.

@kuba: I think it makes sense to allow a single newline at the end of the value. If the spec is ambiguous on the topic, would you mind making a pull request at https://github.com/ietf-wg-acme/acme/ improving it?

I haven't tested what the current Boulder behavior is: is this issue about the client's self-verify, or Boulder's verify, or both?

Client's `self_verify` is affected. I haven't checked Boulder.

@MiaP97 For this to work for me, I had to generate my verification file using "echo -n" to not append the newline, as in:

echo -n "abcd-etc-xyz" > filename

Does that work for you with your Litespeed based host?

@tedb 

> For this to work for me, I had to generate my verification file using "echo -n" to not append the newline, as in:
> echo -n "abcd-etc-xyz" > filename
> Does that work for you with your Litespeed based host?

It does, which is strange because I used nano -N and still got the error.
It still might be useful to allow a newline at the end because:
a) It's a common error, and
b) Some people may only have access to a web-based text editor that automatically adds new lines

I just had this newline problem as well when using the Linux editor Nano (in addition to having the charset in the Content-Type). Sounds like calling strip() on the file before the comparison would solve the newline problem  (and using a substring search on Content-type instead of == would solve that issue). 


Resolving patch:
diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py
--- a/acme/acme/challenges.py
+++ b/acme/acme/challenges.py
@@ -228,6 +228,9 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
 
     """
 
+    WHITESPACE_CUTSET = "\n\r\t "
+    """Whitespace characters which should be ignored at the end of the body."""
+
     def simple_verify(self, chall, domain, account_public_key, port=None):
         """Simple verify.
 
@@ -273,10 +276,11 @@ def simple_verify(self, chall, domain, account_public_key, port=None):
                          found_ct, chall.CONTENT_TYPE)
             return False
 
-        if self.key_authorization != http_response.text:
+        challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET)
+        if self.key_authorization != challenge_response:
             logger.debug("Key authorization from response (%r) doesn't match "
                          "HTTP response (%r)", self.key_authorization,
-                         http_response.text)
+                         challenge_response)
             return False
 
         return True


Test patch:
diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py
--- a/acme/acme/challenges_test.py
+++ b/acme/acme/challenges_test.py
@@ -126,6 +126,16 @@ def test_simple_verify_bad_validation(self, mock_get):
         self.assertFalse(self.response.simple_verify(
             self.chall, "local", KEY.public_key()))
 
+    @mock.patch("acme.challenges.requests.get")
+    def test_simple_verify_whitespace_validation(self, mock_get):
+        from acme.challenges import HTTP01Response
+        mock_get.return_value = mock.MagicMock(
+            text=(self.chall.validation(KEY) +
+                  HTTP01Response.WHITESPACE_CUTSET), headers=self.good_headers)
+        self.assertTrue(self.response.simple_verify(
+            self.chall, "local", KEY.public_key()))
+        mock_get.assert_called_once_with(self.chall.uri("local"))
+
     @mock.patch("acme.challenges.requests.get")
     def test_simple_verify_bad_content_type(self, mock_get):
         mock_get().text = self.chall.token