Merge pull request #570 from kuba/simplehttp

SimpleHTTP fixes
This commit is contained in:
James Kasten
2015-07-02 09:19:21 -07:00
7 changed files with 152 additions and 53 deletions
+27 -34
View File
@@ -1,9 +1,7 @@
"""Manual plugin."""
import logging
import os
import sys
import requests
import zope.component
import zope.interface
@@ -14,9 +12,6 @@ from letsencrypt import interfaces
from letsencrypt.plugins import common
logger = logging.getLogger(__name__)
class ManualAuthenticator(common.Plugin):
"""Manual Authenticator.
@@ -34,36 +29,47 @@ Make sure your web server displays the following content at
{achall.token}
Content-Type header MUST be set to {ct}.
If you don't have HTTP server configured, you can run the following
command on the target server (as root):
{command}
"""
# "cd /tmp/letsencrypt" makes sure user doesn't serve /root,
# separate "public_html" ensures that cert.pem/key.pem are not
# served and makes it more obvious that Python command will serve
# anything recursively under the cwd
HTTP_TEMPLATE = """\
mkdir -p {response.URI_ROOT_PATH}
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server:
python -m SimpleHTTPServer 80"""
python -c "import BaseHTTPServer, SimpleHTTPServer; \\
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.serve_forever()" """
"""Non-TLS command template."""
# https://www.piware.de/2011/01/creating-an-https-server-in-python/
HTTPS_TEMPLATE = """\
mkdir -p {response.URI_ROOT_PATH} # run only once per server
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server:
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout key.pem -out cert.pem
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem
python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\
s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.socket = ssl.wrap_socket(s.socket, keyfile='key.pem', certfile='cert.pem'); \\
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.socket = ssl.wrap_socket(s.socket, keyfile='../key.pem', certfile='../cert.pem'); \\
s.serve_forever()" """
"""TLS command template.
According to the ACME specification, "the ACME server MUST ignore
the certificate provided by the HTTPS server", so the first command
generates temporary self-signed certificate. For the same reason
``requests.get`` in `_verify` sets ``verify=False``. Python HTTPS
server command serves the ``token`` on all URIs.
generates temporary self-signed certificate.
"""
@@ -105,11 +111,14 @@ binary for temporary key/certificate generation.""".replace("\n", "")
assert response.good_path # is encoded os.urandom(18) good?
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
achall=achall, response=response,
uri=response.uri(achall.domain),
command=self.template.format(achall=achall, response=response)))
achall=achall, response=response, uri=response.uri(achall.domain),
ct=response.CONTENT_TYPE, command=self.template.format(
achall=achall, response=response, ct=response.CONTENT_TYPE,
port=(response.port if self.config.simple_http_port is None
else self.config.simple_http_port))))
if self._verify(achall, response):
if response.simple_verify(
achall.challb, achall.domain, self.config.simple_http_port):
return response
else:
return None
@@ -121,21 +130,5 @@ binary for temporary key/certificate generation.""".replace("\n", "")
sys.stdout.write(message)
raw_input("Press ENTER to continue")
def _verify(self, achall, chall_response): # pylint: disable=no-self-use
uri = chall_response.uri(achall.domain)
logger.debug("Verifying %s...", uri)
try:
response = requests.get(uri, verify=False)
except requests.exceptions.ConnectionError as error:
logger.exception(error)
return False
ret = response.text == achall.token
if not ret:
logger.error("Unable to verify %s! Expected: %r, returned: %r.",
uri, achall.token, response.text)
return ret
def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use
pass # pragma: no cover
+10 -13
View File
@@ -2,7 +2,6 @@
import unittest
import mock
import requests
from acme import challenges
@@ -15,7 +14,8 @@ class ManualAuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.manual import ManualAuthenticator
self.config = mock.MagicMock(no_simple_http_tls=True)
self.config = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430)
self.auth = ManualAuthenticator(config=self.config, name="manual")
self.achalls = [achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)]
@@ -32,28 +32,25 @@ class ManualAuthenticatorTest(unittest.TestCase):
@mock.patch("letsencrypt.plugins.manual.sys.stdout")
@mock.patch("letsencrypt.plugins.manual.os.urandom")
@mock.patch("letsencrypt.plugins.manual.requests.get")
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify")
@mock.patch("__builtin__.raw_input")
def test_perform(self, mock_raw_input, mock_get, mock_urandom, mock_stdout):
def test_perform(self, mock_raw_input, mock_verify, mock_urandom,
mock_stdout):
mock_urandom.return_value = "foo"
mock_get().text = self.achalls[0].token
mock_verify.return_value = True
self.assertEqual(
[challenges.SimpleHTTPResponse(tls=False, path='Zm9v')],
self.auth.perform(self.achalls))
resp = challenges.SimpleHTTPResponse(tls=False, path='Zm9v')
self.assertEqual([resp], self.auth.perform(self.achalls))
mock_raw_input.assert_called_once()
mock_get.assert_called_with(
"http://foo.com/.well-known/acme-challenge/Zm9v", verify=False)
mock_verify.assert_called_with(self.achalls[0].challb, "foo.com", 4430)
message = mock_stdout.write.mock_calls[0][1][0]
self.assertTrue(self.achalls[0].token in message)
self.assertTrue('Zm9v' in message)
mock_get().text = self.achalls[0].token + '!'
mock_verify.return_value = False
self.assertEqual([None], self.auth.perform(self.achalls))
mock_get.side_effect = requests.exceptions.ConnectionError
self.assertEqual([None], self.auth.perform(self.achalls))
if __name__ == "__main__":
unittest.main() # pragma: no cover