Manual SimpleHTTP integration tests.

This commit is contained in:
Jakub Warmuz
2015-07-03 09:49:14 +00:00
parent 108bd22ca3
commit 74ce332b5a
6 changed files with 81 additions and 15 deletions
+6 -1
View File
@@ -156,7 +156,7 @@ class AuthHandler(object):
active_achalls = [] active_achalls = []
for achall, resp in itertools.izip(achalls, resps): for achall, resp in itertools.izip(achalls, resps):
# Don't send challenges for None and False authenticator responses # Don't send challenges for None and False authenticator responses
if resp: if resp is not None and resp:
self.network.answer_challenge(achall.challb, resp) self.network.answer_challenge(achall.challb, resp)
# TODO: answer_challenge returns challr, with URI, # TODO: answer_challenge returns challr, with URI,
# that can be used in _find_updated_challr # that can be used in _find_updated_challr
@@ -166,6 +166,11 @@ class AuthHandler(object):
chall_update[achall.domain].append(achall) chall_update[achall.domain].append(achall)
else: else:
chall_update[achall.domain] = [achall] chall_update[achall.domain] = [achall]
else: # resp is None or not resp
# XXX: make sure that achalls corresponding to None or
# False returned from Authenticator are removed from
# the queue and thus avoid infinite loop
active_achalls.append(achall)
return active_achalls return active_achalls
+65 -12
View File
@@ -1,6 +1,12 @@
"""Manual plugin.""" """Manual plugin."""
import os import os
import logging
import shutil
import signal
import subprocess
import sys import sys
import tempfile
import time
import zope.component import zope.component
import zope.interface import zope.interface
@@ -8,10 +14,14 @@ import zope.interface
from acme import challenges from acme import challenges
from acme import jose from acme import jose
from letsencrypt import errors
from letsencrypt import interfaces from letsencrypt import interfaces
from letsencrypt.plugins import common from letsencrypt.plugins import common
logger = logging.getLogger(__name__)
class ManualAuthenticator(common.Plugin): class ManualAuthenticator(common.Plugin):
"""Manual Authenticator. """Manual Authenticator.
@@ -43,8 +53,8 @@ command on the target server (as root):
# anything recursively under the cwd # anything recursively under the cwd
HTTP_TEMPLATE = """\ HTTP_TEMPLATE = """\
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH} mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html cd {root}/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server: # run only once per server:
python -c "import BaseHTTPServer, SimpleHTTPServer; \\ python -c "import BaseHTTPServer, SimpleHTTPServer; \\
@@ -55,8 +65,8 @@ s.serve_forever()" """
# https://www.piware.de/2011/01/creating-an-https-server-in-python/ # https://www.piware.de/2011/01/creating-an-https-server-in-python/
HTTPS_TEMPLATE = """\ HTTPS_TEMPLATE = """\
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH} mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html cd {root}/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server: # 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
@@ -77,6 +87,14 @@ s.serve_forever()" """
super(ManualAuthenticator, self).__init__(*args, **kwargs) super(ManualAuthenticator, self).__init__(*args, **kwargs)
self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls
else self.HTTPS_TEMPLATE) else self.HTTPS_TEMPLATE)
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
else "/tmp/letsencrypt")
@classmethod
def add_parser_arguments(cls, add):
add("test-mode", action="store_true",
help="Test mode. Executes the manual command in subprocess. "
"Requires openssl to be installed unless --no-simple-http-tls.")
def prepare(self): # pylint: disable=missing-docstring,no-self-use def prepare(self): # pylint: disable=missing-docstring,no-self-use
pass # pragma: no cover pass # pragma: no cover
@@ -110,17 +128,44 @@ binary for temporary key/certificate generation.""".replace("\n", "")
tls=(not self.config.no_simple_http_tls)) tls=(not self.config.no_simple_http_tls))
assert response.good_path # is encoded os.urandom(18) good? assert response.good_path # is encoded os.urandom(18) good?
self._notify_and_wait(self.MESSAGE_TEMPLATE.format( command = self.template.format(
achall=achall, response=response, uri=response.uri(achall.domain), root=self._root, achall=achall, response=response,
ct=response.CONTENT_TYPE, command=self.template.format( ct=response.CONTENT_TYPE, port=(
achall=achall, response=response, ct=response.CONTENT_TYPE, response.port if self.config.simple_http_port is None
port=(response.port if self.config.simple_http_port is None else self.config.simple_http_port))
else self.config.simple_http_port)))) if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command)
try:
# pylint: disable=attribute-defined-outside-init
self._httpd = subprocess.Popen(
command,
# don't care about setting stdout and stderr,
# we're in test mode anyway
shell=True,
# "preexec_fn" is UNIX specific, but so is "command"
preexec_fn=os.setsid)
except OSError as error: # ValueError should not happen!
logging.debug(
"Couldn't execute manual command", error, exc_info=True)
return False
logger.debug("Manual command running as PID %s.", self._httpd.pid)
# give it some time to bootstrap, before we try to verify
# (cert generation in case of simpleHttpS might take time)
time.sleep(4) # XXX
if self._httpd.poll():
raise errors.Error("Couldn't execute manual command")
else:
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
achall=achall, response=response,
uri=response.uri(achall.domain), ct=response.CONTENT_TYPE,
command=command))
if response.simple_verify( if response.simple_verify(
achall.challb, achall.domain, self.config.simple_http_port): achall.challb, achall.domain, self.config.simple_http_port):
return response return response
else: else:
if self.conf("test-mode") and self._httpd.poll():
return False
return None return None
def _notify_and_wait(self, message): # pylint: disable=no-self-use def _notify_and_wait(self, message): # pylint: disable=no-self-use
@@ -130,5 +175,13 @@ binary for temporary key/certificate generation.""".replace("\n", "")
sys.stdout.write(message) sys.stdout.write(message)
raw_input("Press ENTER to continue") raw_input("Press ENTER to continue")
def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use def cleanup(self, achalls):
pass # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument
if self.conf("test-mode"):
if self._httpd.poll() is None:
logger.debug("Terminating manual command process")
os.killpg(self._httpd.pid, signal.SIGTERM)
else:
logger.debug("Manual command process already terminated "
"with %s code", self._httpd.returncode)
shutil.rmtree(self._root)
+2 -1
View File
@@ -15,7 +15,8 @@ class ManualAuthenticatorTest(unittest.TestCase):
def setUp(self): def setUp(self):
from letsencrypt.plugins.manual import ManualAuthenticator from letsencrypt.plugins.manual import ManualAuthenticator
self.config = mock.MagicMock( self.config = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430) no_simple_http_tls=True, simple_http_port=4430,
manual_test_mode=False)
self.auth = ManualAuthenticator(config=self.config, name="manual") self.auth = ManualAuthenticator(config=self.config, name="manual")
self.achalls = [achallenges.SimpleHTTP( self.achalls = [achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)]
@@ -197,6 +197,10 @@ class StandaloneAuthenticator(common.Plugin):
""" """
signal.signal(signal.SIGINT, self.subproc_signal_handler) signal.signal(signal.SIGINT, self.subproc_signal_handler)
self.sock = socket.socket() self.sock = socket.socket()
# SO_REUSEADDR flag tells the kernel to reuse a local socket
# in TIME_WAIT state, without waiting for its natural timeout
# to expire.
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try: try:
self.sock.bind(("0.0.0.0", port)) self.sock.bind(("0.0.0.0", port))
except socket.error, error: except socket.error, error:
+2
View File
@@ -23,6 +23,8 @@ common() {
common --domains le1.wtf auth common --domains le1.wtf auth
common --domains le2.wtf run common --domains le2.wtf run
common -a manual -d le.wtf auth
common -a manual -d le.wtf --no-simple-http-tls auth
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf OPENSSL_CNF=examples/openssl.cnf
+2 -1
View File
@@ -10,11 +10,12 @@ store_flags="$store_flags --logs-dir $root/logs"
export root store_flags export root store_flags
letsencrypt_test () { letsencrypt_test () {
# first three flags required, rest is handy defaults
letsencrypt \ letsencrypt \
--server "${SERVER:-http://localhost:4000/acme/new-reg}" \ --server "${SERVER:-http://localhost:4000/acme/new-reg}" \
--no-verify-ssl \ --no-verify-ssl \
--dvsni-port 5001 \ --dvsni-port 5001 \
--simple-http-port 5001 \
--manual-test-mode \
$store_flags \ $store_flags \
--text \ --text \
--agree-eula \ --agree-eula \