diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2ce8a8209 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# this file uses slightly different syntax than .gitignore, +# e.g. ".tox/" will not ignore .tox directory + +# well, official docker build should be done on clean git checkout +# anyway, so .tox should be empty... But I'm sure people will try to +# test docker on their git working directories. + +.git +.tox +venv +docs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b6a07388c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# https://github.com/letsencrypt/lets-encrypt-preview/pull/431#issuecomment-103659297 +# it is more likely developers will already have ubuntu:trusty rather +# than e.g. debian:jessie and image size differences are negligible +FROM ubuntu:trusty +MAINTAINER Jakub Warmuz +MAINTAINER William Budington + +# Note: this only exposes the port to other docker containers. You +# still have to bind to 443@host at runtime, as per the ACME spec. +EXPOSE 443 + +# TODO: make sure --config-dir and --work-dir cannot be changed +# through the CLI (letsencrypt-docker wrapper that uses standalone +# authenticator and text mode only?) +VOLUME /etc/letsencrypt /var/lib/letsencrypt + +WORKDIR /opt/letsencrypt + +# no need to mkdir anything: +# https://docs.docker.com/reference/builder/#copy +# If doesn't exist, it is created along with all missing +# directories in its path. + +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ +RUN /opt/letsencrypt/src/ubuntu.sh && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* + +# the above is not likely to change, so by putting it further up the +# Dockerfile we make sure we cache as much as possible + + +COPY setup.py README.rst CHANGES.rst MANIFEST.in /opt/letsencrypt/src/ + +# all above files are necessary for setup.py, however, package source +# code directory has to be copied separately to a subdirectory... +# https://docs.docker.com/reference/builder/#copy: "If is a +# directory, the entire contents of the directory are copied, +# including filesystem metadata. Note: The directory itself is not +# copied, just its contents." Order again matters, three files are far +# more likely to be cached than the whole project directory + +COPY letsencrypt /opt/letsencrypt/src/letsencrypt/ +COPY acme /opt/letsencrypt/src/acme/ +COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/ +COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/ + + +RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ + /opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src + +# install in editable mode (-e) to save space: it's not possible to +# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image); +# this might also help in debugging: you can "docker run --entrypoint +# bash" and investigate, apply patches, etc. + +ENV PATH /opt/letsencrypt/venv/bin:$PATH +# TODO: is --text really necessary? +ENTRYPOINT [ "letsencrypt", "--text" ] diff --git a/MANIFEST.in b/MANIFEST.in index b628121e1..f9364d64f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,8 +3,7 @@ include CHANGES.rst include CONTRIBUTING.md include linter_plugin.py include letsencrypt/EULA - -recursive-include letsencrypt/client/tests/testdata * +recursive-include letsencrypt/tests/testdata * recursive-include acme/schemata *.json recursive-include acme/jose/testdata * diff --git a/README.rst b/README.rst index 3ec317c55..28034406e 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ All you need to do is:: user@www:~$ sudo letsencrypt -d www.example.org auth -and if you have a compatbile web server (Apache or Nginx), Let's Encrypt can +and if you have a compatible web server (Apache or Nginx), Let's Encrypt can not only get a new certificate, but also deploy it and configure your server automatically!:: diff --git a/acme/messages2.py b/acme/messages2.py index 419bb0b4e..253aaa95b 100644 --- a/acme/messages2.py +++ b/acme/messages2.py @@ -18,11 +18,9 @@ class Error(jose.JSONObjectWithFields, Exception): 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', } - # TODO: Boulder omits 'type' and 'instance', spec requires, boulder#128 - typ = jose.Field('type', omitempty=True) + typ = jose.Field('type') title = jose.Field('title', omitempty=True) detail = jose.Field('detail') - instance = jose.Field('instance', omitempty=True) @typ.encoder def typ(value): # pylint: disable=missing-docstring,no-self-argument @@ -227,10 +225,6 @@ class Authorization(ResourceBody): challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) - # TODO: acme-spec #92, #98 - key = Registration._fields['key'] - contact = Registration._fields['contact'] - status = jose.Field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST diff --git a/acme/messages2_test.py b/acme/messages2_test.py index 5f9441b4f..c1521e2c3 100644 --- a/acme/messages2_test.py +++ b/acme/messages2_test.py @@ -21,7 +21,8 @@ class ErrorTest(unittest.TestCase): def setUp(self): from acme.messages2 import Error - self.error = Error(detail='foo', typ='malformed') + self.error = Error(detail='foo', typ='malformed', title='title') + self.jobj = {'detail': 'foo', 'title': 'some title'} def test_typ_prefix(self): self.assertEqual('malformed', self.error.typ) @@ -32,15 +33,15 @@ class ErrorTest(unittest.TestCase): def test_typ_decoder_missing_prefix(self): from acme.messages2 import Error - self.assertRaises(jose.DeserializationError, Error.from_json, - {'detail': 'foo', 'type': 'malformed'}) - self.assertRaises(jose.DeserializationError, Error.from_json, - {'detail': 'foo', 'type': 'not valid bare type'}) + self.jobj['type'] = 'malformed' + self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) + self.jobj['type'] = 'not valid bare type' + self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) def test_typ_decoder_not_recognized(self): from acme.messages2 import Error - self.assertRaises(jose.DeserializationError, Error.from_json, - {'detail': 'foo', 'type': 'urn:acme:error:baz'}) + self.jobj['type'] = 'urn:acme:error:baz' + self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) def test_description(self): self.assertEqual( diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index b09130d77..4e4c75b33 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -10,21 +10,35 @@ # - 7.8 "wheezy" (x64) # - 8.0 "jessie" (x64) + # virtualenv binary can be found in different packages depending on # distro version (#346) -distro=$(lsb_release -si) -# 6.0.10 => 60, 14.04 => 1404 -version=$(lsb_release -sr | awk -F '.' '{print $1 $2}') -if [ "$distro" = "Ubuntu" -a "$version" -ge 1410 ] -then - virtualenv="virtualenv" -elif [ "$distro" = "Debian" -a "$version" -ge 80 ] +newer () { + distro=$(lsb_release -si) + # 6.0.10 => 60, 14.04 => 1404 + # TODO: in sid version==unstable + version=$(lsb_release -sr | awk -F '.' '{print $1 $2}') + if [ "$distro" = "Ubuntu" -a "$version" -ge 1410 ] + then + return 0; + elif [ "$distro" = "Debian" -a "$version" -ge 80 ] + then + return 0; + else + return 1; + fi +} + +# you can force newer if lsb_release is not available (e.g. Docker +# debian:jessie base image) +if [ "$1" = "newer" ] || newer then virtualenv="virtualenv" else virtualenv="python-virtualenv" fi + # dpkg-dev: dpkg-architecture binary necessary to compile M2Crypto, c.f. # #276, https://github.com/martinpaljak/M2Crypto/issues/62, # M2Crypto setup.py:add_multiarch_paths diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..dbe6e4f01 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +production: + build: . + ports: + - "443:443" + +# For development, mount git root to /opt/letsencrypt/src in order to +# make the dev workflow more vagrant-like. +development: + build: . + ports: + - "443:443" + volumes: + - .:/opt/letsencrypt/src + - /opt/letsencrypt/venv diff --git a/docs/api/client/proof_of_possession.rst b/docs/api/client/proof_of_possession.rst deleted file mode 100644 index 9f1ea0793..000000000 --- a/docs/api/client/proof_of_possession.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.proof_of_possession` --------------------------------------------------- - -.. automodule:: letsencrypt.client.proof_of_possession - :members: diff --git a/docs/api/proof_of_possession.rst b/docs/api/proof_of_possession.rst new file mode 100644 index 000000000..db8c6c563 --- /dev/null +++ b/docs/api/proof_of_possession.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.proof_of_possession` +-------------------------------------- + +.. automodule:: letsencrypt.proof_of_possession + :members: diff --git a/docs/using.rst b/docs/using.rst index daa2425ea..89cbc48f6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -2,6 +2,24 @@ Using the Let's Encrypt client ============================== +Quick start +=========== + +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 + +.. code-block:: shell + + sudo docker run -it --rm -p 443:443 --name letsencrypt \ + -v "/etc/letsencrypt:/etc/letsencrypt" \ + -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 +``/etc/letsencrypt/certs``. + + Prerequisites ============= @@ -31,7 +49,7 @@ Debian sudo ./bootstrap/debian.sh -For squezze you will need to: +For squeeze you will need to: - Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``. diff --git a/examples/plugins/letsencrypt_example_plugins.py b/examples/plugins/letsencrypt_example_plugins.py index a364ae905..2810d0d40 100644 --- a/examples/plugins/letsencrypt_example_plugins.py +++ b/examples/plugins/letsencrypt_example_plugins.py @@ -20,7 +20,7 @@ class Authenticator(common.Plugin): # "self" as first argument, e.g. def prepare(self)... -class Installer(common.Plugins): +class Installer(common.Plugin): """Example Installer.""" zope.interface.implements(interfaces.IInstaller) zope.interface.classProvides(interfaces.IPluginFactory) diff --git a/letsencrypt/continuity_auth.py b/letsencrypt/continuity_auth.py index 8bfc0bfeb..739e33d43 100644 --- a/letsencrypt/continuity_auth.py +++ b/letsencrypt/continuity_auth.py @@ -19,7 +19,7 @@ class ContinuityAuthenticator(object): :ivar proof_of_pos: Performs "proofOfPossession" challenges. :type proof_of_pos: - :class:`letsencrypt.client.proof_of_possession.Proof_of_Possession` + :class:`letsencrypt.proof_of_possession.Proof_of_Possession` """ zope.interface.implements(interfaces.IAuthenticator) @@ -32,7 +32,7 @@ class ContinuityAuthenticator(object): :type config: :class:`letsencrypt.interfaces.IConfig` :param installer: Let's Encrypt Installer. - :type installer: :class:`letsencrypt.client.interfaces.IInstaller` + :type installer: :class:`letsencrypt.interfaces.IInstaller` """ self.rec_token = recovery_token.RecoveryToken( diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index f89f281cb..94617eef6 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -1,4 +1,4 @@ -"""Let's Encrypt client crypto utility functions +"""Let's Encrypt client crypto utility functions. .. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server is capable of handling the signatures. @@ -13,6 +13,7 @@ import Crypto.PublicKey.RSA import Crypto.Signature.PKCS1_v1_5 import M2Crypto +import OpenSSL from letsencrypt import le_util @@ -231,3 +232,44 @@ def make_ss_cert(key_str, domains, not_before=None, assert cert.verify() # print check_purpose(,0 return cert.as_pem() + + +def _request_san(req): # TODO: implement directly in PyOpenSSL! + # constants based on implementation of + # OpenSSL.crypto.X509Error._subjectAltNameString + parts_separator = ", " + part_separator = ":" + extension_short_name = "subjectAltName" + + # pylint: disable=protected-access,no-member + label = OpenSSL.crypto.X509Extension._prefixes[OpenSSL.crypto._lib.GEN_DNS] + assert parts_separator not in label + prefix = label + part_separator + + extensions = [ext._subjectAltNameString().split(parts_separator) + for ext in req.get_extensions() + if ext.get_short_name() == extension_short_name] + # WARNING: this function assumes that no SAN can include + # parts_separator, hence the split! + + return [part.split(part_separator)[1] for parts in extensions + for part in parts if part.startswith(prefix)] + + +def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get list of Subject Alternative Names from signing request. + + :param str csr: Certificate Signing Request in PEM format (must contain + one or more subjectAlternativeNames, or the function will fail, + raising ValueError) + + :returns: List of referenced subject alternative names + :rtype: list + + """ + try: + request = OpenSSL.crypto.load_certificate_request(typ, csr) + except OpenSSL.crypto.Error as error: + logging.exception(error) + raise + return _request_san(request) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index e89f693d6..faf23f414 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -117,6 +117,7 @@ class Network(object): :rtype: `requests.Response` """ + logging.debug('Sending GET request to %s', uri) kwargs.setdefault('verify', self.verify_ssl) try: response = requests.get(uri, **kwargs) @@ -136,13 +137,13 @@ class Network(object): :rtype: `requests.Response` """ - logging.debug('Sending POST data: %s', data) + logging.debug('Sending POST data to %s: %s', uri, data) kwargs.setdefault('verify', self.verify_ssl) try: response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: raise errors.NetworkError(error) - logging.debug('Received response %s: %s', response, response.text) + logging.debug('Received response %s: %r', response, response.text) self._check_response(response, content_type=content_type) return response @@ -251,6 +252,7 @@ class Network(object): def _authzr_from_response(self, response, identifier, uri=None, new_cert_uri=None): + # pylint: disable=no-self-use if new_cert_uri is None: try: new_cert_uri = response.links['next']['url'] @@ -261,8 +263,7 @@ class Network(object): body=messages2.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) - if (authzr.body.key != self.key.public() - or authzr.body.identifier != identifier): + if authzr.body.identifier != identifier: raise errors.UnexpectedUpdate(authzr) return authzr diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index 8c4e1ad55..d70dfc751 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -215,8 +215,9 @@ class PluginsRegistry(collections.Mapping): return None def __repr__(self): - return "{0}({1!r})".format( - self.__class__.__name__, set(self._plugins.itervalues())) + return "{0}({1})".format( + self.__class__.__name__, ','.join( + repr(p_ep) for p_ep in self._plugins.itervalues())) def __str__(self): if not self._plugins: diff --git a/letsencrypt/plugins/disco_test.py b/letsencrypt/plugins/disco_test.py index f3cdd43c6..0dd65e5de 100644 --- a/letsencrypt/plugins/disco_test.py +++ b/letsencrypt/plugins/disco_test.py @@ -154,6 +154,7 @@ class PluginsRegistryTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.disco import PluginsRegistry self.plugin_ep = mock.MagicMock(name="mock") + self.plugin_ep.__hash__.side_effect = TypeError self.plugins = {"mock": self.plugin_ep} self.reg = PluginsRegistry(self.plugins) @@ -227,7 +228,7 @@ class PluginsRegistryTest(unittest.TestCase): def test_repr(self): self.plugin_ep.__repr__ = lambda _: "PluginEntryPoint#mock" - self.assertEqual("PluginsRegistry(set([PluginEntryPoint#mock]))", + self.assertEqual("PluginsRegistry(PluginEntryPoint#mock)", repr(self.reg)) def test_str(self): diff --git a/letsencrypt/plugins/standalone/authenticator.py b/letsencrypt/plugins/standalone/authenticator.py index 1558b86ca..7d5c3ea6e 100644 --- a/letsencrypt/plugins/standalone/authenticator.py +++ b/letsencrypt/plugins/standalone/authenticator.py @@ -152,9 +152,6 @@ class StandaloneAuthenticator(common.Plugin): :rtype: bool """ - signal.signal(signal.SIGIO, self.client_signal_handler) - signal.signal(signal.SIGUSR1, self.client_signal_handler) - signal.signal(signal.SIGUSR2, self.client_signal_handler) display = zope.component.getUtility(interfaces.IDisplay) @@ -259,6 +256,16 @@ class StandaloneAuthenticator(common.Plugin): :rtype: bool """ + # In order to avoid a race condition, we set the signal handler + # that will be needed by the parent process now, and undo this + # action if we turn out to be the child process. (This needs + # to happen before the fork because the child will send one of + # these signals to the parent almost immediately after the + # fork, and the parent must already be ready to receive it.) + signal.signal(signal.SIGIO, self.client_signal_handler) + signal.signal(signal.SIGUSR1, self.client_signal_handler) + signal.signal(signal.SIGUSR2, self.client_signal_handler) + fork_result = os.fork() Crypto.Random.atfork() if fork_result: @@ -269,6 +276,12 @@ class StandaloneAuthenticator(common.Plugin): return self.do_parent_process(port) else: # CHILD process (the TCP listener subprocess) + # Undo the parent's signal handler settings, which aren't + # applicable to us. + signal.signal(signal.SIGIO, signal.SIG_DFL) + signal.signal(signal.SIGUSR1, signal.SIG_DFL) + signal.signal(signal.SIGUSR2, signal.SIG_DFL) + self.child_pid = os.getpid() # do_child_process() is normally not expected to return but # should terminate via sys.exit(). diff --git a/letsencrypt/plugins/standalone/tests/authenticator_test.py b/letsencrypt/plugins/standalone/tests/authenticator_test.py index c6287774b..802431536 100644 --- a/letsencrypt/plugins/standalone/tests/authenticator_test.py +++ b/letsencrypt/plugins/standalone/tests/authenticator_test.py @@ -404,47 +404,39 @@ class DoParentProcessTest(unittest.TestCase): StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(config=None, name=None) - @mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal") @mock.patch("letsencrypt.plugins.standalone.authenticator." "zope.component.getUtility") - def test_do_parent_process_ok(self, mock_get_utility, mock_signal): + def test_do_parent_process_ok(self, mock_get_utility): self.authenticator.subproc_state = "ready" result = self.authenticator.do_parent_process(1717) self.assertTrue(result) self.assertEqual(mock_get_utility.call_count, 1) - self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal") @mock.patch("letsencrypt.plugins.standalone.authenticator." "zope.component.getUtility") - def test_do_parent_process_inuse(self, mock_get_utility, mock_signal): + def test_do_parent_process_inuse(self, mock_get_utility): self.authenticator.subproc_state = "inuse" result = self.authenticator.do_parent_process(1717) self.assertFalse(result) self.assertEqual(mock_get_utility.call_count, 1) - self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal") @mock.patch("letsencrypt.plugins.standalone.authenticator." "zope.component.getUtility") - def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal): + def test_do_parent_process_cantbind(self, mock_get_utility): self.authenticator.subproc_state = "cantbind" result = self.authenticator.do_parent_process(1717) self.assertFalse(result) self.assertEqual(mock_get_utility.call_count, 1) - self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal") @mock.patch("letsencrypt.plugins.standalone.authenticator." "zope.component.getUtility") - def test_do_parent_process_timeout(self, mock_get_utility, mock_signal): + def test_do_parent_process_timeout(self, mock_get_utility): # Normally times out in 5 seconds and returns False. We can # now set delay_amount to a lower value so that it times out # faster than it would under normal use. result = self.authenticator.do_parent_process(1717, delay_amount=1) self.assertFalse(result) self.assertEqual(mock_get_utility.call_count, 1) - self.assertEqual(mock_signal.call_count, 3) class DoChildProcessTest(unittest.TestCase): diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 17ac970e2..85bcfe8cf 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -156,14 +156,6 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, ["0"]) - def _get_exp_response(self, domain, path, challs): - # pylint: disable=no-self-use - exp_resp = [None] * len(challs) - for i in path: - exp_resp[i] = TRANSLATE[challs[i].typ] + str(domain) - - return exp_resp - def _validate_all(self, unused_1, unused_2): for dom in self.handler.authzr.keys(): azr = self.handler.authzr[dom] @@ -284,8 +276,6 @@ class PollChallengesTest(unittest.TestCase): identifier=authzr.body.identifier, challenges=new_challbs, combinations=authzr.body.combinations, - key=authzr.body.key, - contact=authzr.body.contact, status=status_, ), ) @@ -443,19 +433,5 @@ def gen_dom_authzr(domain, unused_new_authzr_uri, challs): [messages2.STATUS_PENDING]*len(challs)) -def gen_path(required, challs): - """Generate a combination by picking ``required`` from ``challs``. - - :param required: Required types of challenges (subclasses of - :class:`~acme.challenges.Challenge`). - :param challs: Sequence of ACME challenge messages, corresponding to - :attr:`acme.messages.Challenge.challenges`. - - :return: :class:`list` of :class:`int` - - """ - return [challs.index(chall) for chall in required] - - if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 5a6418085..9bb8084d5 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -12,10 +12,11 @@ class CLITest(unittest.TestCase): def _call(cls, args): from letsencrypt import cli args = ['--text'] + args - with mock.patch("letsencrypt.cli.sys.stdout") as stdout: - with mock.patch("letsencrypt.cli.sys.stderr") as stderr: - ret = cli.main(args) - return ret, stdout, stderr + with mock.patch('letsencrypt.cli.sys.stdout') as stdout: + with mock.patch('letsencrypt.cli.sys.stderr') as stderr: + with mock.patch('letsencrypt.cli.client') as client: + ret = cli.main(args) + return ret, stdout, stderr, client def test_no_flags(self): self.assertRaises(SystemExit, self._call, []) @@ -23,6 +24,18 @@ class CLITest(unittest.TestCase): def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) + def test_rollback(self): + _, _, _, client = self._call(['rollback']) + client.rollback.assert_called_once() + + _, _, _, client = self._call(['rollback', '--checkpoints', '123']) + client.rollback.assert_called_once_with( + mock.ANY, 123, mock.ANY, mock.ANY) + + def test_config_changes(self): + _, _, _, client = self._call(['config_changes']) + client.view_config_changes.assert_called_once() + def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 3119e1e52..5dade01b7 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -56,8 +56,7 @@ class DetermineAccountTest(unittest.TestCase): class RollbackTest(unittest.TestCase): """Test the rollback function.""" def setUp(self): - from letsencrypt_apache.configurator import ApacheConfigurator - self.m_install = mock.MagicMock(spec=ApacheConfigurator) + self.m_install = mock.MagicMock() @classmethod def _call(cls, checkpoints, side_effect): diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index bdd67da6a..92cb4014b 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -7,6 +7,7 @@ import tempfile import unittest import M2Crypto +import OpenSSL import mock @@ -150,5 +151,41 @@ class MakeSSCertTest(unittest.TestCase): make_ss_cert(RSA512_KEY, ['example.com', 'www.example.com']) +class GetSansFromCsrTest(unittest.TestCase): + """Tests for letsencrypt.crypto_util.get_sans_from_csr.""" + def test_extract_one_san(self): + from letsencrypt.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr.pem')) + self.assertEqual(get_sans_from_csr(csr), ['example.com']) + + def test_extract_two_sans(self): + from letsencrypt.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-san.pem')) + self.assertEqual(get_sans_from_csr(csr), ['example.com', + 'www.example.com']) + + def test_extract_six_sans(self): + from letsencrypt.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-6sans.pem')) + self.assertEqual(get_sans_from_csr(csr), + ["example.com", "example.org", "example.net", + "example.info", "subdomain.example.com", + "other.subdomain.example.com"]) + + def test_parse_non_csr(self): + from letsencrypt.crypto_util import get_sans_from_csr + self.assertRaises(OpenSSL.crypto.Error, get_sans_from_csr, + "hello there") + + def test_parse_no_sans(self): + from letsencrypt.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-nosans.pem')) + self.assertEqual([], get_sans_from_csr(csr)) + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index 0afa1de65..7bffcf0f4 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -3,6 +3,8 @@ import datetime import httplib import os import pkg_resources +import shutil +import tempfile import unittest import M2Crypto @@ -50,6 +52,8 @@ class NetworkTest(unittest.TestCase): self.identifier = messages2.Identifier( typ=messages2.IDENTIFIER_FQDN, value='example.com') + self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) + # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') reg = messages2.Registration( @@ -69,7 +73,7 @@ class NetworkTest(unittest.TestCase): self.authz = messages2.Authorization( identifier=messages2.Identifier( typ=messages2.IDENTIFIER_FQDN, value='example.com'), - challenges=(challb,), combinations=None, key=KEY.public()) + challenges=(challb,), combinations=None) self.authzr = messages2.AuthorizationResource( body=self.authz, uri=authzr_uri, new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') @@ -80,6 +84,9 @@ class NetworkTest(unittest.TestCase): uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') + def tearDown(self): + shutil.rmtree(self.config.accounts_dir) + def _mock_post_get(self): # pylint: disable=protected-access self.net._post = mock.MagicMock(return_value=self.response) @@ -97,7 +104,7 @@ class NetworkTest(unittest.TestCase): return self.value @classmethod def from_json(cls, value): - return 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"') @@ -111,7 +118,8 @@ class NetworkTest(unittest.TestCase): def test_check_response_not_ok_jobj_error(self): self.response.ok = False - self.response.json.return_value = messages2.Error(detail='foo') + self.response.json.return_value = messages2.Error( + detail='foo', typ='serverInternal', title='some title').to_json() # pylint: disable=protected-access self.assertRaises( messages2.Error, self.net._check_response, self.response) @@ -218,8 +226,8 @@ class NetworkTest(unittest.TestCase): def test_register_from_account(self): self.net.register = mock.Mock() acc = account.Account( - mock.Mock(accounts_dir='mock_dir'), 'key', - email='cert-admin@example.com', phone='+12025551212') + self.config, 'key', email='cert-admin@example.com', + phone='+12025551212') self.net.register_from_account(acc) @@ -228,9 +236,8 @@ class NetworkTest(unittest.TestCase): def test_register_from_account_partial_info(self): self.net.register = mock.Mock() acc = account.Account( - mock.Mock(accounts_dir='mock_dir'), 'key', - email='cert-admin@example.com') - acc2 = account.Account(mock.Mock(accounts_dir='mock_dir'), 'key') + self.config, 'key', email='cert-admin@example.com') + acc2 = account.Account(self.config, 'key') self.net.register_from_account(acc) self.net.register.assert_called_with( @@ -270,11 +277,10 @@ class NetworkTest(unittest.TestCase): # TODO: test POST call arguments # TODO: split here and separate test - authz_wrong_key = self.authz.update(key=KEY2.public()) - self.response.json.return_value = authz_wrong_key.to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.net.request_challenges, - self.identifier, self.regr) + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges, + self.identifier, self.authzr.uri) def test_request_challenges_missing_next(self): self.response.status_code = httplib.CREATED @@ -348,6 +354,11 @@ class NetworkTest(unittest.TestCase): self.assertEqual((self.authzr, self.response), self.net.poll(self.authzr)) + # TODO: split here and separate test + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) + def test_request_issuance(self): self.response.content = CERT.as_der() self.response.headers['Location'] = self.certr.uri diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index 5f8b5b224..ae04b5081 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -12,8 +12,6 @@ from letsencrypt import errors from letsencrypt import le_util from letsencrypt.display import util as display_util -from letsencrypt_apache import configurator - class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods """Base Class for Revoker Tests.""" @@ -60,8 +58,7 @@ class RevokerTest(RevokerBase): self._store_certs() self.revoker = Revoker( - mock.MagicMock(spec=configurator.ApacheConfigurator), - self.mock_config) + installer=mock.MagicMock(), config=self.mock_config) def tearDown(self): shutil.rmtree(self.backup_dir) diff --git a/letsencrypt/tests/testdata/csr-6sans.pem b/letsencrypt/tests/testdata/csr-6sans.pem new file mode 100644 index 000000000..8f6b52bd7 --- /dev/null +++ b/letsencrypt/tests/testdata/csr-6sans.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw +EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 +9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG +9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL +ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t +ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd +k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv +IvzVBz/nD11drfz/RNuX +-----END CERTIFICATE REQUEST----- diff --git a/letsencrypt/tests/testdata/csr-nosans.pem b/letsencrypt/tests/testdata/csr-nosans.pem new file mode 100644 index 000000000..813db67b0 --- /dev/null +++ b/letsencrypt/tests/testdata/csr-nosans.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh +MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt +cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn +BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz +AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo +wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= +-----END CERTIFICATE REQUEST----- diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index 12e3a5140..fc2dcfc79 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -7,6 +7,8 @@ import unittest import mock +from letsencrypt import constants as core_constants + from letsencrypt_apache import configurator from letsencrypt_apache import constants from letsencrypt_apache import obj @@ -20,7 +22,7 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.temp_dir, self.config_dir, self.work_dir = dir_setup( "debian_apache_2_4/two_vhost_80") - self.ssl_options = setup_apache_ssl_options(self.config_dir) + self.ssl_options = setup_ssl_options(self.config_dir) self.config_path = os.path.join( self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2") @@ -31,14 +33,19 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods "acme.jose", "testdata/rsa256_key.pem") -def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): +def dir_setup(test_dir="debian_apache_2_4/two_vhost_80", + pkg="letsencrypt_apache.tests"): """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, core_constants.CONFIG_DIRS_MODE) + os.chmod(config_dir, core_constants.CONFIG_DIRS_MODE) + os.chmod(work_dir, core_constants.CONFIG_DIRS_MODE) + test_configs = pkg_resources.resource_filename( - "letsencrypt_apache.tests", "testdata/%s" % test_dir) + pkg, os.path.join("testdata", test_dir)) shutil.copytree( test_configs, os.path.join(temp_dir, test_dir), symlinks=True) @@ -46,10 +53,11 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): return temp_dir, config_dir, work_dir -def setup_apache_ssl_options(config_dir): +def setup_ssl_options( + config_dir, mod_ssl_conf=constants.MOD_SSL_CONF): """Move the ssl_options into position and return the path.""" option_path = os.path.join(config_dir, "options-ssl.conf") - shutil.copyfile(constants.MOD_SSL_CONF, option_path) + shutil.copyfile(mod_ssl_conf, option_path) return option_path diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index ffb9bd3b2..d2deee15a 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -271,7 +271,7 @@ class NginxConfigurator(common.Plugin): the existing one? :param vhost: The vhost to add SSL to. - :type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost` + :type vhost: :class:`~letsencrypt_nginx.obj.VirtualHost` """ ssl_block = [['listen', '443 ssl'], diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 534c5a8d3..5c188099c 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -78,7 +78,7 @@ class NginxDvsni(ApacheDvsni): """Modifies Nginx config to include challenge server blocks. :param list ll_addrs: list of lists of - :class:`letsencrypt.client.plugins.apache.obj.Addr` to apply + :class:`letsencrypt_nginx.obj.Addr` to apply :raises errors.LetsEncryptMisconfigurationError: Unable to find a suitable HTTP block to include DVSNI hosts. @@ -115,7 +115,7 @@ class NginxDvsni(ApacheDvsni): """Creates a server block for a DVSNI challenge. :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.client.achallenges.DVSNI` + :type achall: :class:`letsencrypt.achallenges.DVSNI` :param list addrs: addresses of challenged domain :class:`list` of type :class:`~nginx.obj.Addr` diff --git a/letsencrypt_nginx/nginxparser.py b/letsencrypt_nginx/nginxparser.py index 18ba8b0bd..f24455d59 100644 --- a/letsencrypt_nginx/nginxparser.py +++ b/letsencrypt_nginx/nginxparser.py @@ -3,7 +3,7 @@ import string from pyparsing import ( Literal, White, Word, alphanums, CharsNotIn, Forward, Group, - Optional, OneOrMore, ZeroOrMore, pythonStyleComment) + Optional, OneOrMore, Regex, ZeroOrMore, pythonStyleComment) class RawNginxParser(object): @@ -16,17 +16,21 @@ class RawNginxParser(object): semicolon = Literal(";").suppress() space = White().suppress() key = Word(alphanums + "_/") - value = CharsNotIn("{};,") + # Matches anything that is not a special character AND any chars in single + # or double quotes + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules assignment = (key + Optional(space + value) + semicolon) + location_statement = Optional(space + modifier) + Optional(space + location) + if_statement = Literal("if") + space + Regex(r"\(.+\)") + space block = Forward() block << Group( - Group(key + Optional(space + modifier) + Optional(space + location)) + (Group(key + location_statement) ^ Group(if_statement)) + left_bracket + Group(ZeroOrMore(Group(assignment) | block)) + right_bracket) diff --git a/letsencrypt_nginx/tests/nginxparser_test.py b/letsencrypt_nginx/tests/nginxparser_test.py index 23f46c72a..73a89534b 100644 --- a/letsencrypt_nginx/tests/nginxparser_test.py +++ b/letsencrypt_nginx/tests/nginxparser_test.py @@ -84,6 +84,26 @@ class TestRawNginxParser(unittest.TestCase): ]]]]] ) + def test_parse_from_file2(self): + parsed = load(open(util.get_data_filename('edge_cases.conf'))) + self.assertEqual( + parsed, + [[['server'], [['server_name', 'simple']]], + [['server'], + [['server_name', 'with.if'], + [['location', '~', '^/services/.+$'], + [[['if', '($request_filename ~* \\.(ttf|woff)$)'], + [['add_header', 'Access-Control-Allow-Origin "*"']]]]]]], + [['server'], + [['server_name', 'with.complicated.headers'], + [['location', '~*', '\\.(?:gif|jpe?g|png)$'], + [['add_header', 'Pragma public'], + ['add_header', + 'Cache-Control \'public, must-revalidate, proxy-revalidate\'' + ' "test,;{}" foo'], + ['blah', '"hello;world"'], + ['try_files', '$uri @rewrites']]]]]]) + def test_dump_as_file(self): parsed = load(open(util.get_data_filename('nginx.conf'))) parsed[-1][-1].append([['server'], diff --git a/letsencrypt_nginx/tests/parser_test.py b/letsencrypt_nginx/tests/parser_test.py index c4fc06910..f2e6dfe1e 100644 --- a/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt_nginx/tests/parser_test.py @@ -25,8 +25,8 @@ class NginxParserTest(util.NginxTest): shutil.rmtree(self.work_dir) def test_root_normalized(self): - path = os.path.join(self.temp_dir, "foo/////" - "bar/../../testdata") + path = os.path.join(self.temp_dir, "etc_nginx/////" + "ubuntu_nginx/../../etc_nginx") nparser = parser.NginxParser(path, None) self.assertEqual(nparser.root, self.config_path) diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf new file mode 100644 index 000000000..477cb1c45 --- /dev/null +++ b/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf @@ -0,0 +1,27 @@ +# This is not a valid nginx config file but it tests edge cases in valid nginx syntax + +server { + server_name simple; +} + +server { + server_name with.if; + location ~ ^/services/.+$ { + if ($request_filename ~* \.(ttf|woff)$) { + add_header Access-Control-Allow-Origin "*"; + } + } +} + +server { + server_name with.complicated.headers; + + location ~* \.(?:gif|jpe?g|png)$ { + + add_header Pragma public; + add_header Cache-Control 'public, must-revalidate, proxy-revalidate' "test,;{}" foo; + blah "hello;world"; + + try_files $uri @rewrites; + } +} diff --git a/letsencrypt_nginx/tests/testdata/foo.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/foo.conf rename to letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf diff --git a/letsencrypt_nginx/tests/testdata/mime.types b/letsencrypt_nginx/tests/testdata/etc_nginx/mime.types similarity index 100% rename from letsencrypt_nginx/tests/testdata/mime.types rename to letsencrypt_nginx/tests/testdata/etc_nginx/mime.types diff --git a/letsencrypt_nginx/tests/testdata/nginx.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/nginx.conf rename to letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf diff --git a/letsencrypt_nginx/tests/testdata/nginx.new.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/nginx.new.conf rename to letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf diff --git a/letsencrypt_nginx/tests/testdata/server.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/server.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/server.conf rename to letsencrypt_nginx/tests/testdata/etc_nginx/server.conf diff --git a/letsencrypt_nginx/tests/testdata/sites-enabled/default b/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/sites-enabled/default rename to letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default diff --git a/letsencrypt_nginx/tests/testdata/sites-enabled/example.com b/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com similarity index 100% rename from letsencrypt_nginx/tests/testdata/sites-enabled/example.com rename to letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params diff --git a/letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf b/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf similarity index 100% rename from letsencrypt_nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf rename to letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index 5faa3da18..c53250f1b 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -1,12 +1,12 @@ """Common utilities for letsencrypt_nginx.""" import os import pkg_resources -import shutil -import tempfile import unittest import mock +from letsencrypt_apache.tests import util as apache_util + from letsencrypt_nginx import constants from letsencrypt_nginx import configurator @@ -16,13 +16,13 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods def setUp(self): super(NginxTest, self).setUp() - self.temp_dir, self.config_dir, self.work_dir = dir_setup( - "testdata") + self.temp_dir, self.config_dir, self.work_dir = apache_util.dir_setup( + "etc_nginx", "letsencrypt_nginx.tests") - self.ssl_options = setup_nginx_ssl_options(self.config_dir) + self.ssl_options = apache_util.setup_ssl_options( + self.config_dir, constants.MOD_SSL_CONF) - self.config_path = os.path.join( - self.temp_dir, "testdata") + self.config_path = os.path.join(self.temp_dir, "etc_nginx") self.rsa256_file = pkg_resources.resource_filename( "acme.jose", "testdata/rsa256_key.pem") @@ -33,29 +33,8 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods def get_data_filename(filename): """Gets the filename of a test data file.""" return pkg_resources.resource_filename( - "letsencrypt_nginx.tests", "testdata/%s" % filename) - - -def dir_setup(test_dir="debian_nginx/two_vhost_80"): - """Setup the directories necessary for the configurator.""" - temp_dir = tempfile.mkdtemp("temp") - config_dir = tempfile.mkdtemp("config") - work_dir = tempfile.mkdtemp("work") - - test_configs = pkg_resources.resource_filename( - "letsencrypt_nginx.tests", test_dir) - - shutil.copytree( - test_configs, os.path.join(temp_dir, test_dir), symlinks=True) - - return temp_dir, config_dir, work_dir - - -def setup_nginx_ssl_options(config_dir): - """Move the ssl_options into position and return the path.""" - option_path = os.path.join(config_dir, "options-ssl.conf") - shutil.copyfile(constants.MOD_SSL_CONF, option_path) - return option_path + "letsencrypt_nginx.tests", os.path.join( + "testdata", "etc_nginx", filename)) def get_nginx_configurator( diff --git a/setup.py b/setup.py index c719bea60..815403b5a 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ install_requires = [ 'psutil>=2.1.0', # net_connections introduced in 2.1.0 'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pycrypto', - 'PyOpenSSL', + # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions + 'PyOpenSSL>=0.15', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? 'pyrfc3339', 'python-augeas',