diff --git a/Dockerfile b/Dockerfile index b6a07388c..78aa7a75b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/ COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/ +# requirements.txt not installed! RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ /opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src diff --git a/README.rst b/README.rst index db32889db..40c054fe3 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,13 @@ +.. notice for github users + +Official **documentation**, including `installation instructions`_, is +available at https://letsencrypt.readthedocs.org. + +Generic information about Let's Encrypt project can be found at +https://letsencrypt.org. Please read `Frequently Asked Questions (FAQ) +`_. + + About the Let's Encrypt Client ============================== @@ -47,6 +57,9 @@ server automatically!:: :target: https://quay.io/repository/letsencrypt/lets-encrypt-preview :alt: Docker Repository on Quay.io +.. _`installation instructions`: + https://letsencrypt.readthedocs.org/en/latest/using.html + .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU @@ -85,7 +98,7 @@ Current Features Links ----- -Documentation: https://letsencrypt.readthedocs.org/ +Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/lets-encrypt-preview diff --git a/Vagrantfile b/Vagrantfile index 7eb2b4cce..1d3b48f06 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -8,8 +8,6 @@ VAGRANTFILE_API_VERSION = "2" $ubuntu_setup_script = <`. @@ -48,7 +56,7 @@ synced to ``/vagrant``, so you can get started with: vagrant ssh cd /vagrant - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt .[dev,docs,testing] sudo ./venv/bin/letsencrypt Support for other Linux distributions coming soon. diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index 9cca3b795..1c73a4a42 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -51,9 +51,6 @@ Errors :members: - :members: - - Utilities --------- diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..96eb62b05 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,9 +5,9 @@ Using the Let's Encrypt client Quick start =========== -Using docker you can quickly get yourself a testing cert. From the +Using Docker_ you can quickly get yourself a testing cert. From the server that the domain your requesting a cert for resolves to, -download docker, and issue the following command +`install Docker`_, issue the following command: .. code-block:: shell @@ -16,9 +16,31 @@ download docker, and issue the following command -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/lets-encrypt-preview:latest -And follow the instructions. Your new cert will be available in +and follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. +.. _Docker: https://docker.com +.. _`install Docker`: https://docs.docker.com/docker/userguide/ + + +Getting the code +================ + +Please `install Git`_ and run the following commands: + +.. code-block:: shell + + git clone https://github.com/letsencrypt/lets-encrypt-preview + cd lets-encrypt-preview + +Alternatively you could `download the ZIP archive`_ and extract the +snapshot of our repository, but it's strongly recommended to use the +above method instead. + +.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git +.. _`download the ZIP archive`: + https://github.com/letsencrypt/lets-encrypt-preview/archive/master.zip + Prerequisites ============= @@ -30,8 +52,8 @@ are provided mainly for the :ref:`developers ` reference. In general: * ``sudo`` is required as a suggested way of running privileged process -* `swig`_ is required for compiling `m2crypto`_ -* `augeas`_ is required for the ``python-augeas`` bindings +* `SWIG`_ is required for compiling `M2Crypto`_ +* `Augeas`_ is required for the Python bindings Ubuntu @@ -65,25 +87,71 @@ Mac OSX sudo ./bootstrap/mac.sh +Fedora +------ + +.. code-block:: shell + + sudo ./bootstrap/fedora.sh + + +Centos 7 +-------- + +.. code-block:: shell + + sudo ./bootstrap/centos.sh + +For installation run this modified command (note the trailing +backslash): + +.. code-block:: shell + + SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ + ./venv/bin/pip install -r requirements.txt . + + Installation ============ .. code-block:: shell virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt . + +.. warning:: Please do **not** use ``python setup.py install``. Please + do **not** attempt the installation commands as + superuser/root and/or without Virtualenv_, e.g. ``sudo + python setup.py install``, ``sudo pip install``, ``sudo + ./venv/bin/...``. These modes of operation might corrupt + your operating system and are **not supported** by the + Let's Encrypt team! + +.. note:: If your operating system uses SWIG 3.0.5+, you will need to + run ``pip install -r requirements-swig-3.0.5.txt -r + requirements.txt .`` instead. Known affected systems: + + * Fedora 22 + * some versions of Mac OS X Usage ===== -The letsencrypt commandline tool has a builtin help: +To get a new certificate run: + +.. code-block:: shell + + ./venv/bin/letsencrypt auth + +The ``letsencrypt`` commandline tool has a builtin help: .. code-block:: shell ./venv/bin/letsencrypt --help -.. _augeas: http://augeas.net/ -.. _m2crypto: https://github.com/M2Crypto/M2Crypto -.. _swig: http://www.swig.org/ +.. _Augeas: http://augeas.net/ +.. _M2Crypto: https://github.com/M2Crypto/M2Crypto +.. _SWIG: http://www.swig.org/ +.. _Virtualenv: https://virtualenv.pypa.io diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 3f8e3d012..93a949050 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -186,7 +186,7 @@ class Account(object): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (optional, press Enter to skip)") + "Enter email address") if code == display_util.OK: try: diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 77e362f22..46ef167e0 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -62,10 +62,10 @@ class DVSNI(AnnotatedChallenge): return cert_pem, response -class SimpleHTTPS(AnnotatedChallenge): - """Client annotated "simpleHttps" ACME challenge.""" +class SimpleHTTP(AnnotatedChallenge): + """Client annotated "simpleHttp" ACME challenge.""" __slots__ = ('challb', 'domain', 'key') - acme_type = challenges.SimpleHTTPS + acme_type = challenges.SimpleHTTP class DNS(AnnotatedChallenge): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 37d818dbe..5665fe83d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -134,9 +134,11 @@ class AuthHandler(object): self._send_responses(self.cont_c, cont_resp, chall_update)) # Check for updated status... - self._poll_challenges(chall_update, best_effort) - # This removes challenges from self.dv_c and self.cont_c - self._cleanup_challenges(active_achalls) + try: + self._poll_challenges(chall_update, best_effort) + finally: + # This removes challenges from self.dv_c and self.cont_c + self._cleanup_challenges(active_achalls) def _send_responses(self, achalls, resps, chall_update): """Send responses and make sure errors are handled. @@ -336,9 +338,9 @@ def challb_to_achall(challb, key, domain): logging.info(" DVSNI challenge for %s.", domain) return achallenges.DVSNI( challb=challb, domain=domain, key=key) - elif isinstance(chall, challenges.SimpleHTTPS): - logging.info(" SimpleHTTPS challenge for %s.", domain) - return achallenges.SimpleHTTPS( + elif isinstance(chall, challenges.SimpleHTTP): + logging.info(" SimpleHTTP challenge for %s.", domain) + return achallenges.SimpleHTTP( challb=challb, domain=domain, key=key) elif isinstance(chall, challenges.DNS): logging.info(" DNS challenge for %s.", domain) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d1e084e08..0217598b1 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -252,6 +252,9 @@ def create_parser(plugins): add("-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") + add("--no-simple-http-tls", action="store_true", + help=config_help("no_simple_http_tls")) + testing_group = parser.add_argument_group( "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index c407f8825..41c92be50 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -34,7 +34,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.SimpleHTTPS])]) + challenges.DVSNI, challenges.SimpleHTTP])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 17905149a..8ea9241a3 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -183,6 +183,10 @@ class IConfig(zope.interface.Interface): "Port number to perform DVSNI challenge. " "Boulder in testing mode defaults to 5001.") + # TODO: not implemented + no_simple_http_tls = zope.interface.Attribute( + "Do not use TLS when solving SimpleHTTP challenges.") + class IInstaller(IPlugin): """Generic Let's Encrypt Installer Interface. diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index faf23f414..a20194a79 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -10,6 +10,7 @@ import requests import werkzeug from acme import jose +from acme import jws as acme_jws from acme import messages2 from letsencrypt import errors @@ -24,7 +25,7 @@ class Network(object): .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()``. + instances of `.DeserializationError` raised in `from_json()`. :ivar str new_reg_uri: Location of new-reg :ivar key: `.JWK` (private) @@ -33,26 +34,32 @@ class Network(object): """ + # TODO: Move below to acme module? DER_CONTENT_TYPE = 'application/pkix-cert' JSON_CONTENT_TYPE = 'application/json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' + REPLAY_NONCE_HEADER = 'Replay-Nonce' def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): self.new_reg_uri = new_reg_uri self.key = key self.alg = alg self.verify_ssl = verify_ssl + self._nonces = set() - def _wrap_in_jws(self, obj): + def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. + .. todo:: Implement ``acmePath``. + + :param JSONDeSerializable obj: :rtype: `.JWS` """ dumps = obj.json_dumps() logging.debug('Serialized JSON: %s', dumps) - return jose.JWS.sign( - payload=dumps, key=self.key, alg=self.alg).json_dumps() + return acme_jws.JWS.sign( + payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() @classmethod def _check_response(cls, response, content_type=None): @@ -126,9 +133,31 @@ class Network(object): self._check_response(response, content_type=content_type) return response - def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs): + def _add_nonce(self, response): + if self.REPLAY_NONCE_HEADER in response.headers: + nonce = response.headers[self.REPLAY_NONCE_HEADER] + error = acme_jws.Header.validate_nonce(nonce) + if error is None: + logging.debug('Storing nonce: %r', nonce) + self._nonces.add(nonce) + else: + raise errors.NetworkError('Invalid nonce ({0}): {1}'.format( + nonce, error)) + else: + raise errors.NetworkError( + 'Server {0} response did not include a replay nonce'.format( + response.request.method)) + + def _get_nonce(self, uri): + if not self._nonces: + logging.debug('Requesting fresh nonce by sending HEAD to %s', uri) + self._add_nonce(requests.head(uri)) + return self._nonces.pop() + + def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs): """Send POST data. + :param JSONDeSerializable obj: Will be wrapped in JWS. :param str content_type: Expected ``Content-Type``, fails if not set. :raises acme.messages2.NetworkError: @@ -137,6 +166,7 @@ class Network(object): :rtype: `requests.Response` """ + data = self._wrap_in_jws(obj, self._get_nonce(uri)) logging.debug('Sending POST data to %s: %s', uri, data) kwargs.setdefault('verify', self.verify_ssl) try: @@ -145,6 +175,7 @@ class Network(object): raise errors.NetworkError(error) logging.debug('Received response %s: %r', response, response.text) + self._add_nonce(response) self._check_response(response, content_type=content_type) return response @@ -182,7 +213,7 @@ class Network(object): """ new_reg = messages2.Registration(contact=contact) - response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) + response = self._post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors regr = self._regr_from_response(response) @@ -219,20 +250,19 @@ class Network(object): :rtype: `.RegistrationResource` """ - response = self._post(regr.uri, self._wrap_in_jws(regr.body)) + response = self._post(regr.uri, regr.body) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) - updated_regr = self._regr_from_response( response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, terms_of_service=regr.terms_of_service) if updated_regr != regr: - # TODO: Boulder reregisters with new recoveryToken and new URI raise errors.UnexpectedUpdate(regr) + return updated_regr def agree_to_tos(self, regr): @@ -280,7 +310,7 @@ class Network(object): """ new_authz = messages2.Authorization(identifier=identifier) - response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz)) + response = self._post(new_authzr_uri, new_authz) assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) @@ -316,7 +346,7 @@ class Network(object): :raises errors.UnexpectedUpdate: """ - response = self._post(challb.uri, self._wrap_in_jws(response)) + response = self._post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: @@ -395,7 +425,7 @@ class Network(object): content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 - self._wrap_in_jws(req), + req, content_type=content_type, headers={'Accept': content_type}) @@ -546,7 +576,7 @@ class Network(object): """ rev = messages2.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, self._wrap_in_jws(rev)) + response = self._post(certr.uri, rev) if response.status_code != httplib.OK: raise errors.NetworkError( 'Successful revocation must return HTTP OK status') diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 32bee2b49..03ddf7df7 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -1,8 +1,14 @@ """Plugin common functions.""" +import os +import pkg_resources +import shutil +import tempfile + import zope.interface from acme.jose import util as jose_util +from letsencrypt import constants from letsencrypt import interfaces @@ -69,3 +75,127 @@ class Plugin(object): with unique plugin name prefix. """ + +# other + +class Addr(object): + r"""Represents an virtual host address. + + :param str addr: addr part of vhost address + :param str port: port number or \*, or "" + + """ + def __init__(self, tup): + self.tup = tup + + @classmethod + def fromstring(cls, str_addr): + """Initialize Addr from string.""" + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) + + def __str__(self): + if self.tup[1]: + return "%s:%s" % self.tup + return self.tup[0] + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.tup == other.tup + return False + + def __hash__(self): + return hash(self.tup) + + def get_addr(self): + """Return addr part of Addr object.""" + return self.tup[0] + + def get_port(self): + """Return port.""" + return self.tup[1] + + def get_addr_obj(self, port): + """Return new address object with same addr and new port.""" + return self.__class__((self.tup[0], port)) + + +class Dvsni(object): + """Class that perform DVSNI challenges.""" + + def __init__(self, configurator): + self.configurator = configurator + self.achalls = [] + self.indices = [] + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_dvsni_cert_challenge.conf") + # self.completed = 0 + + def add_chall(self, achall, idx=None): + """Add challenge to DVSNI object to perform at once. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :param int idx: index to challenge in a larger array + + """ + self.achalls.append(achall) + if idx is not None: + self.indices.append(idx) + + def get_cert_file(self, achall): + """Returns standardized name for challenge certificate. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :returns: certificate file name + :rtype: str + + """ + return os.path.join( + self.configurator.config.work_dir, achall.nonce_domain + ".crt") + + def _setup_challenge_cert(self, achall, s=None): + # pylint: disable=invalid-name + """Generate and write out challenge certificate.""" + cert_path = self.get_cert_file(achall) + # Register the path before you write out the file + self.configurator.reverter.register_file_creation(True, cert_path) + + cert_pem, response = achall.gen_cert_and_response(s) + + # Write out challenge cert + with open(cert_path, "w") as cert_chall_fd: + cert_chall_fd.write(cert_pem) + + return response + + +# test utils + +def setup_ssl_options(config_dir, src, dest): + """Move the ssl_options into position and return the path.""" + option_path = os.path.join(config_dir, dest) + shutil.copyfile(src, option_path) + return option_path + + +def dir_setup(test_dir, pkg): + """Setup the directories necessary for the configurator.""" + temp_dir = tempfile.mkdtemp("temp") + config_dir = tempfile.mkdtemp("config") + work_dir = tempfile.mkdtemp("work") + + os.chmod(temp_dir, constants.CONFIG_DIRS_MODE) + os.chmod(config_dir, constants.CONFIG_DIRS_MODE) + os.chmod(work_dir, constants.CONFIG_DIRS_MODE) + + test_configs = pkg_resources.resource_filename( + pkg, os.path.join("testdata", test_dir)) + + shutil.copytree( + test_configs, os.path.join(temp_dir, test_dir), symlinks=True) + + return temp_dir, config_dir, work_dir diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 12dd18bdf..6de86f2b8 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -1,8 +1,16 @@ """Tests for letsencrypt.plugins.common.""" +import pkg_resources import unittest import mock +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt import le_util + +from letsencrypt.tests import acme_util + class NamespaceFunctionsTest(unittest.TestCase): """Tests for letsencrypt.plugins.common.*_namespace functions.""" @@ -57,5 +65,103 @@ class PluginTest(unittest.TestCase): "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) +class AddrTest(unittest.TestCase): + """Tests for letsencrypt.client.plugins.common.Addr.""" + + def setUp(self): + from letsencrypt.plugins.common import Addr + self.addr1 = Addr.fromstring("192.168.1.1") + self.addr2 = Addr.fromstring("192.168.1.1:*") + self.addr3 = Addr.fromstring("192.168.1.1:80") + + def test_fromstring(self): + self.assertEqual(self.addr1.get_addr(), "192.168.1.1") + self.assertEqual(self.addr1.get_port(), "") + self.assertEqual(self.addr2.get_addr(), "192.168.1.1") + self.assertEqual(self.addr2.get_port(), "*") + self.assertEqual(self.addr3.get_addr(), "192.168.1.1") + self.assertEqual(self.addr3.get_port(), "80") + + def test_str(self): + self.assertEqual(str(self.addr1), "192.168.1.1") + self.assertEqual(str(self.addr2), "192.168.1.1:*") + self.assertEqual(str(self.addr3), "192.168.1.1:80") + + def test_get_addr_obj(self): + self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443") + self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1") + self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*") + + def test_eq(self): + self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) + self.assertNotEqual(self.addr1, self.addr2) + self.assertFalse(self.addr1 == 3333) + + def test_set_inclusion(self): + from letsencrypt.plugins.common import Addr + set_a = set([self.addr1, self.addr2]) + addr1b = Addr.fromstring("192.168.1.1") + addr2b = Addr.fromstring("192.168.1.1:*") + set_b = set([addr1b, addr2b]) + + self.assertEqual(set_a, set_b) + + +class DvsniTest(unittest.TestCase): + """Tests for letsencrypt.plugins.common.DvsniTest.""" + + rsa256_file = pkg_resources.resource_filename( + "acme.jose", "testdata/rsa256_key.pem") + rsa256_pem = pkg_resources.resource_string( + "acme.jose", "testdata/rsa256_key.pem") + + auth_key = le_util.Key(rsa256_file, rsa256_pem) + achalls = [ + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" + "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", + nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + ), "pending"), + domain="encryption-example.demo", key=auth_key), + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\xba\xa9\xda? {response.URI_ROOT_PATH}/{response.path} +# run only once per server: +python -m SimpleHTTPServer 80""" + """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 +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 +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'); \\ +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. + + """ + + def __init__(self, *args, **kwargs): + super(ManualAuthenticator, self).__init__(*args, **kwargs) + self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls + else self.HTTPS_TEMPLATE) + + def prepare(self): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return """\ +This plugin requires user's manual intervention in setting up a HTTP +server for solving SimpleHTTP challenges and thus does not need to be +run as a privilidged process. Alternatively shows instructions on how +to use Python's built-in HTTP server and, in case of HTTPS, openssl +binary for temporary key/certificate generation.""".replace("\n", "") + + def get_chall_pref(self, domain): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return [challenges.SimpleHTTP] + + def perform(self, achalls): # pylint: disable=missing-docstring + responses = [] + # TODO: group achalls by the same socket.gethostbyname(_ex) + # and prompt only once per server (one "echo -n" per domain) + for achall in achalls: + responses.append(self._perform_single(achall)) + return responses + + def _perform_single(self, achall): + # same path for each challenge response would be easier for + # users, but will not work if multiple domains point at the + # same server: default command doesn't support virtual hosts + response = challenges.SimpleHTTPResponse( + path=jose.b64encode(os.urandom(18)), + tls=(not self.config.no_simple_http_tls)) + 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))) + + if self._verify(achall, response): + return response + else: + return None + + def _notify_and_wait(self, message): # pylint: disable=no-self-use + # TODO: IDisplay wraps messages, breaking the command + #answer = zope.component.getUtility(interfaces.IDisplay).notification( + # message=message, height=25, pause=True) + 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) + logging.debug("Verifying %s...", uri) + try: + response = requests.get(uri, verify=False) + except requests.exceptions.ConnectionError as error: + logging.exception(error) + return False + + ret = response.text == achall.token + if not ret: + logging.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 diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py new file mode 100644 index 000000000..c95654dec --- /dev/null +++ b/letsencrypt/plugins/manual_test.py @@ -0,0 +1,59 @@ +"""Tests for letsencrypt.plugins.manual.""" +import unittest + +import mock +import requests + +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt.tests import acme_util + + +class ManualAuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" + + def setUp(self): + from letsencrypt.plugins.manual import ManualAuthenticator + self.config = mock.MagicMock(no_simple_http_tls=True) + self.auth = ManualAuthenticator(config=self.config, name="manual") + self.achalls = [achallenges.SimpleHTTP( + challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] + + def test_more_info(self): + self.assertTrue(isinstance(self.auth.more_info(), str)) + + def test_get_chall_pref(self): + self.assertTrue(all(issubclass(pref, challenges.Challenge) + for pref in self.auth.get_chall_pref("foo.com"))) + + def test_perform_empty(self): + self.assertEqual([], self.auth.perform([])) + + @mock.patch("letsencrypt.plugins.manual.sys.stdout") + @mock.patch("letsencrypt.plugins.manual.os.urandom") + @mock.patch("letsencrypt.plugins.manual.requests.get") + @mock.patch("__builtin__.raw_input") + def test_perform(self, mock_raw_input, mock_get, mock_urandom, mock_stdout): + mock_urandom.return_value = "foo" + mock_get().text = self.achalls[0].token + + self.assertEqual( + [challenges.SimpleHTTPResponse(tls=False, path='Zm9v')], + 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) + + 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 + '!' + 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 diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 8780e8095..daf651059 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -16,7 +16,7 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( "acme.jose", os.path.join("testdata", "rsa512_key.pem")))) # Challenges -SIMPLE_HTTPS = challenges.SimpleHTTPS( +SIMPLE_HTTP = challenges.SimpleHTTP( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6\xbf'\xb3" @@ -47,7 +47,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] +CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -86,13 +86,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) -SIMPLE_HTTPS_P = chall_to_challb(SIMPLE_HTTPS, messages2.STATUS_PENDING) +SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) -CHALLENGES_P = [SIMPLE_HTTPS_P, DVSNI_P, DNS_P, +CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 85bcfe8cf..8cbc0e604 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -17,7 +17,7 @@ from letsencrypt.tests import acme_util TRANSLATE = { "dvsni": "DVSNI", - "simpleHttps": "SimpleHTTPS", + "simpleHttp": "SimpleHTTP", "dns": "DNS", "recoveryToken": "RecoveryToken", "recoveryContact": "RecoveryContact", @@ -299,8 +299,8 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and SimpleHTTPS with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P) + """Given DVSNI and SimpleHTTP with appropriate combos.""" + challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -315,7 +315,7 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.RECOVERY_TOKEN_P, acme_util.RECOVERY_CONTACT_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P) + acme_util.SIMPLE_HTTP_P) prefs = [challenges.RecoveryToken, challenges.DVSNI] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -328,13 +328,13 @@ class GenChallengePathTest(unittest.TestCase): acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P, + acme_util.SIMPLE_HTTP_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic prefs = [challenges.RecoveryToken, challenges.ProofOfPossession, - challenges.SimpleHTTPS, + challenges.SimpleHTTP, challenges.DVSNI, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) @@ -403,8 +403,8 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.SimpleHTTPS]), - frozenset([challenges.DNS, challenges.SimpleHTTPS]), + frozenset([challenges.DVSNI, challenges.SimpleHTTP]), + frozenset([challenges.DNS, challenges.SimpleHTTP]), ])) def test_empty_satisfied(self): @@ -413,7 +413,7 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTPS_P]))) + acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index 7bffcf0f4..3f745ffa7 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -13,6 +13,7 @@ import requests from acme import challenges from acme import jose +from acme import jws as acme_jws from acme import messages2 from letsencrypt import account @@ -40,15 +41,23 @@ class NetworkTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes,too-many-public-methods def setUp(self): - from letsencrypt.network2 import Network self.verify_ssl = mock.MagicMock() + self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) + + from letsencrypt.network2 import Network self.net = Network( new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) + self.nonce = jose.b64encode('Nonce') + self.net._nonces.add(self.nonce) # pylint: disable=protected-access + self.response = mock.MagicMock(ok=True, status_code=httplib.OK) self.response.headers = {} self.response.links = {} + self.post = mock.MagicMock(return_value=self.response) + self.get = mock.MagicMock(return_value=self.response) + self.identifier = messages2.Identifier( typ=messages2.IDENTIFIER_FQDN, value='example.com') @@ -89,8 +98,8 @@ class NetworkTest(unittest.TestCase): def _mock_post_get(self): # pylint: disable=protected-access - self.net._post = mock.MagicMock(return_value=self.response) - self.net._get = mock.MagicMock(return_value=self.response) + self.net._post = self.post + self.net._get = self.get def test_init(self): self.assertTrue(self.net.verify_ssl is self.verify_ssl) @@ -106,8 +115,12 @@ class NetworkTest(unittest.TestCase): def from_json(cls, value): pass # pragma: no cover # pylint: disable=protected-access - jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo')) - self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"') + jws_dump = self.net._wrap_in_jws( + MockJSONDeSerializable('foo'), nonce='Tg') + jws = acme_jws.JWS.json_loads(jws_dump) + self.assertEqual(jws.payload, '"foo"') + self.assertEqual(jws.signature.combined.nonce, 'Tg') + # TODO: check that nonce is in protected header def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False @@ -169,33 +182,73 @@ class NetworkTest(unittest.TestCase): self.net._check_response.assert_called_once_with( requests_mock.get('uri'), content_type='ct') + def _mock_wrap_in_jws(self): + # pylint: disable=protected-access + self.net._wrap_in_jws = self.wrap_in_jws + @mock.patch('letsencrypt.network2.requests') def test_post_requests_error_passthrough(self, requests_mock): requests_mock.exceptions = requests.exceptions requests_mock.post.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access - self.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data') + self._mock_wrap_in_jws() + self.assertRaises( + errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) @mock.patch('letsencrypt.network2.requests') def test_post(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() - self.net._post('uri', 'data', content_type='ct') + self._mock_wrap_in_jws() + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj, content_type='ct') self.net._check_response.assert_called_once_with( - requests_mock.post('uri', 'data'), content_type='ct') + requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') + + @mock.patch('letsencrypt.network2.requests') + def test_post_replay_nonce_handling(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + + self.net._nonces.clear() + self.assertRaises( + errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) + + nonce2 = jose.b64encode('Nonce2') + requests_mock.head('uri').headers = { + self.net.REPLAY_NONCE_HEADER: nonce2} + requests_mock.post('uri').headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + + self.net._post('uri', mock.sentinel.obj) + + requests_mock.head.assert_called_with('uri') + self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2) + self.assertEqual(self.net._nonces, set([self.nonce])) + + # wrong nonce + requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} + self.assertRaises( + errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) @mock.patch('letsencrypt.client.network2.requests') def test_get_post_verify_ssl(self, requests_mock): # pylint: disable=protected-access + self._mock_wrap_in_jws() self.net._check_response = mock.MagicMock() for verify_ssl in [True, False]: self.net.verify_ssl = verify_ssl self.net._get('uri') - self.net._post('uri', 'data') + self.net._nonces.add('N') + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj) requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) - requests_mock.post.assert_called_once_with( - 'uri', data='data', verify=verify_ssl) + requests_mock.post.assert_called_with( + 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) requests_mock.reset_mock() def test_register(self): @@ -498,8 +551,7 @@ class NetworkTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() self.net.revoke(self.certr, when=messages2.Revocation.NOW) - # pylint: disable=protected-access - self.net._post.assert_called_once_with(self.certr.uri, mock.ANY) + self.post.assert_called_once_with(self.certr.uri, mock.ANY) def test_revoke_bad_status_raises_error(self): self.response.status_code = httplib.METHOD_NOT_ALLOWED diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 078e61564..9bc5af979 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -18,6 +18,8 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util +from letsencrypt.plugins import common + from letsencrypt_apache import constants from letsencrypt_apache import dvsni from letsencrypt_apache import obj @@ -236,7 +238,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return vhost # Checking for domain name in vhost address # This technique is not recommended by Apache but is technically valid - target_addr = obj.Addr((target_name, "443")) + target_addr = common.Addr((target_name, "443")) for vhost in self.vhosts: if target_addr in vhost.addrs: self.assoc[target_name] = vhost @@ -327,7 +329,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): addrs = set() args = self.aug.match(path + "/arg") for arg in args: - addrs.add(obj.Addr.fromstring(self.aug.get(arg))) + addrs.add(common.Addr.fromstring(self.aug.get(arg))) is_ssl = False if self.parser.find_dir( @@ -493,7 +495,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): addr_match % (ssl_fp, parser.case_i("VirtualHost"))) for addr in ssl_addr_p: - old_addr = obj.Addr.fromstring( + old_addr = common.Addr.fromstring( str(self.aug.get(addr))) ssl_addr = old_addr.get_addr_obj("443") self.aug.set(addr, str(ssl_addr)) @@ -796,8 +798,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Instead... should look for vhost of the form *:80 # Should we prompt the user? ssl_addrs = ssl_vhost.addrs - if ssl_addrs == obj.Addr.fromstring("_default_:443"): - ssl_addrs = [obj.Addr.fromstring("*:443")] + if ssl_addrs == common.Addr.fromstring("_default_:443"): + ssl_addrs = [common.Addr.fromstring("*:443")] for vhost in self.vhosts: found = 0 diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index ed7a216bb..fb78cfced 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -2,10 +2,12 @@ import logging import os +from letsencrypt.plugins import common + from letsencrypt_apache import parser -class ApacheDvsni(object): +class ApacheDvsni(common.Dvsni): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object @@ -18,7 +20,7 @@ class ApacheDvsni(object): larger array. ApacheDvsni is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator - maintaining state about where all of the SimpleHTTPS Challenges, + maintaining state about where all of the SimpleHTTP Challenges, Dvsni Challenges belong in the response array. This is an optional utility. @@ -42,26 +44,6 @@ class ApacheDvsni(object): """ - def __init__(self, configurator): - self.configurator = configurator - self.achalls = [] - self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_dvsni_cert_challenge.conf") - # self.completed = 0 - - def add_chall(self, achall, idx=None): - """Add challenge to DVSNI object to perform at once. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :param int idx: index to challenge in a larger array - - """ - self.achalls.append(achall) - if idx is not None: - self.indices.append(idx) def perform(self): """Peform a DVSNI challenge.""" @@ -107,28 +89,12 @@ class ApacheDvsni(object): return responses - def _setup_challenge_cert(self, achall, s=None): - # pylint: disable=invalid-name - """Generate and write out challenge certificate.""" - cert_path = self.get_cert_file(achall) - # Register the path before you write out the file - self.configurator.reverter.register_file_creation(True, cert_path) - - cert_pem, response = achall.gen_cert_and_response(s) - - # Write out challenge cert - with open(cert_path, "w") as cert_chall_fd: - cert_chall_fd.write(cert_pem) - - return response - def _mod_config(self, ll_addrs): """Modifies Apache config files to include challenge vhosts. Result: Apache config includes virtual servers for issued challs - :param list ll_addrs: list of list of - :class:`letsencrypt.plugins.apache.obj.Addr` to apply + :param list ll_addrs: list of list of `~.common.Addr` to apply """ # TODO: Use ip address of existing vhost instead of relying on FQDN @@ -167,7 +133,7 @@ class ApacheDvsni(object): :type achall: :class:`letsencrypt.achallenges.DVSNI` :param list ip_addrs: addresses of challenged domain - :class:`list` of type :class:`~apache.obj.Addr` + :class:`list` of type `~.common.Addr` :returns: virtual host configuration text :rtype: str @@ -186,16 +152,3 @@ class ApacheDvsni(object): ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], cert_path=self.get_cert_file(achall), key_path=achall.key.file, document_root=document_root).replace("\n", os.linesep) - - def get_cert_file(self, achall): - """Returns standardized name for challenge certificate. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :returns: certificate file name - :rtype: str - - """ - return os.path.join( - self.configurator.config.work_dir, achall.nonce_domain + ".crt") diff --git a/letsencrypt_apache/obj.py b/letsencrypt_apache/obj.py index 905e3f192..fecf46ff9 100644 --- a/letsencrypt_apache/obj.py +++ b/letsencrypt_apache/obj.py @@ -1,54 +1,13 @@ """Module contains classes used by the Apache Configurator.""" -class Addr(object): - r"""Represents an Apache VirtualHost address. - - :param str addr: addr part of vhost address - :param str port: port number or \*, or "" - - """ - def __init__(self, tup): - self.tup = tup - - @classmethod - def fromstring(cls, str_addr): - """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) - - def __str__(self): - if self.tup[1]: - return "%s:%s" % self.tup - return self.tup[0] - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.tup == other.tup - return False - - def __hash__(self): - return hash(self.tup) - - def get_addr(self): - """Return addr part of Addr object.""" - return self.tup[0] - - def get_port(self): - """Return port.""" - return self.tup[1] - - def get_addr_obj(self, port): - """Return new address object with same addr and new port.""" - return self.__class__((self.tup[0], port)) - - class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Apache Virtualhost. :ivar str filep: file path of VH :ivar str path: Augeas path to virtual host - :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) + :ivar set addrs: Virtual Host addresses (:class:`set` of + :class:`common.Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index e732e1bce..c8383e71f 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -12,10 +12,11 @@ from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util +from letsencrypt.plugins import common + from letsencrypt.tests import acme_util from letsencrypt_apache import configurator -from letsencrypt_apache import obj from letsencrypt_apache import parser from letsencrypt_apache.tests import util @@ -111,7 +112,7 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].filep) def test_is_name_vhost(self): - addr = obj.Addr.fromstring("*:80") + addr = common.Addr.fromstring("*:80") self.assertTrue(self.config.is_name_vhost(addr)) self.config.version = (2, 2) self.assertFalse(self.config.is_name_vhost(addr)) @@ -132,7 +133,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(ssl_vhost.path, "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") self.assertEqual(len(ssl_vhost.addrs), 1) - self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) + self.assertEqual(set([common.Addr.fromstring("*:443")]), ssl_vhost.addrs) self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) self.assertTrue(ssl_vhost.ssl) self.assertFalse(ssl_vhost.enabled) diff --git a/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt_apache/tests/dvsni_test.py index 088ac9557..27e9b2584 100644 --- a/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt_apache/tests/dvsni_test.py @@ -1,5 +1,4 @@ """Test for letsencrypt_apache.dvsni.""" -import pkg_resources import unittest import shutil @@ -7,18 +6,17 @@ import mock from acme import challenges -from letsencrypt import achallenges -from letsencrypt import le_util +from letsencrypt.plugins import common +from letsencrypt.plugins import common_test -from letsencrypt.tests import acme_util - -from letsencrypt_apache import obj from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" + achalls = common_test.DvsniTest.achalls + def setUp(self): super(DvsniPerformTest, self).setUp() @@ -31,32 +29,6 @@ class DvsniPerformTest(util.ApacheTest): from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) - rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") - rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") - - auth_key = le_util.Key(rsa256_file, rsa256_pem) - self.achalls = [ - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" - "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", - ), "pending"), - domain="encryption-example.demo", key=auth_key), - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\xba\xa9\xda? +# include_package_data=True, +# File "/opt/python/2.6.9/lib/python2.6/distutils/core.py", line 152, in setup +# dist.run_commands() +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 975, in run_commands +# self.run_command(cmd) +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 995, in run_command +# cmd_obj.run() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 142, in run +# self.with_project_on_sys_path(self.run_tests) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path +# func() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 163, in run_tests +# testRunner=self._resolve_as_ep(self.test_runner), +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 816, in __init__ +# self.parseArgs(argv) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 843, in parseArgs +# self.createTests() +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 849, in createTests +# self.module) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 613, in loadTestsFromNames +# suites = [self.loadTestsFromName(name, module) for name in names] +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 587, in loadTestsFromName +# return self.loadTestsFromModule(obj) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 37, in loadTestsFromModule +# tests.append(self.loadTestsFromName(submodule)) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 584, in loadTestsFromName +# parent, obj = obj, getattr(obj, part) +#AttributeError: 'module' object has no attribute 'continuity_auth' + +# the above error happens because letsencrypt.continuity_auth cannot import M2Crypto: + +#>>> import M2Crypto +#Traceback (most recent call last): +# File "", line 1, in +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/__init__.py", line 22, in +# import m2crypto +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 26, in +# _m2crypto = swig_import_helper() +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 22, in swig_import_helper +# _mod = imp.load_module('_m2crypto', fp, pathname, description) +#ImportError: /root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/_m2crypto.so: undefined symbol: SSLv2_method + +# For more info see: + +# - https://github.com/martinpaljak/M2Crypto/commit/84977c532c2444c5487db57146d81bb68dd5431d +# - http://stackoverflow.com/questions/10547332/install-m2crypto-on-a-virtualenv-without-system-packages +# - http://stackoverflow.com/questions/8206546/undefined-symbol-sslv2-method + +# In short: Python has been built without SSLv2 support, and +# github.com/M2Crypto/M2Crypto version doesn't contain necessary +# patch, but it's the only one that has a patch for newer versions of +# swig... + +# Problem seems not exists on Python 2.7. It's unlikely that the +# target distribution has swig 3.0.5+ and doesn't have Python 2.7, so +# this file should only be used in conjuction with Python 2.6. diff --git a/requirements.txt b/requirements.txt index 0f0223dab..972e87eaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ # https://github.com/bw2/ConfigArgParse/issues/17 --e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse --e . +git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse diff --git a/setup.py b/setup.py index 145b75a69..2bd994859 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires = [ 'argparse', 'ConfigArgParse', 'configobj', - 'jsonschema', + 'jsonschema<2.5.1', # https://github.com/Julian/jsonschema/issues/233 'mock', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'parsedatetime', @@ -120,6 +120,7 @@ setup( 'jws = letsencrypt.acme.jose.jws:CLI.run', ], 'letsencrypt.plugins': [ + 'manual = letsencrypt.plugins.manual:ManualAuthenticator', 'standalone = letsencrypt.plugins.standalone.authenticator' ':StandaloneAuthenticator', diff --git a/tox.cover.sh b/tox.cover.sh index 80b6474d7..172d2e05b 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -15,8 +15,10 @@ cover () { "$1" --cover-min-percentage="$2" "$1" } +rm -f .coverage # --cover-erase is off, make sure stats are correct + # don't use sequential composition (;), if letsencrypt_nginx returns # 0, coveralls submit will be triggered (c.f. .travis.yml, # after_success) cover letsencrypt 95 && cover acme 100 && \ - cover letsencrypt_apache 78 && cover letsencrypt_nginx 96 + cover letsencrypt_apache 76 && cover letsencrypt_nginx 96 diff --git a/tox.ini b/tox.ini index 0367b5498..aed60f454 100644 --- a/tox.ini +++ b/tox.ini @@ -22,12 +22,12 @@ setenv = [testenv:cover] basepython = python2.7 commands = - pip install -e .[testing] + pip install -r requirements.txt -e .[testing] ./tox.cover.sh [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) basepython = python2.7 commands = - pip install -e .[dev] + pip install -r requirements.txt -e .[dev] pylint --rcfile=.pylintrc letsencrypt acme letsencrypt_apache letsencrypt_nginx