From 36d26de7465607d002d7eee92ec993d5664a3936 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Thu, 19 Feb 2015 14:55:14 -0800 Subject: [PATCH 01/47] Implement get_sans via parsing Request.as_text() --- letsencrypt/client/crypto_util.py | 41 ++++++++++++ letsencrypt/client/tests/crypto_util_test.py | 67 +++++++++++++++++++ .../client/tests/testdata/csr-6sans.pem | 12 ++++ .../client/tests/testdata/csr-nosans.pem | 8 +++ 4 files changed, 128 insertions(+) create mode 100644 letsencrypt/client/tests/testdata/csr-6sans.pem create mode 100644 letsencrypt/client/tests/testdata/csr-nosans.pem diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index e2c4965fe..f5b836e82 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -1,4 +1,5 @@ """Let's Encrypt client crypto utility functions""" +import re import time import Crypto.Hash.SHA256 @@ -189,3 +190,43 @@ def get_cert_info(filename): "serial": cert.get_serial_number(), "pub_key": "RSA " + str(cert.get_pubkey().size() * 8), } + + +def get_sans_from_csr(csr): + """Get list of Subject Alternative Names from signing request. + + :param str csr: Certificate Signing Request in PEM format + + :returns: List of referenced subject alternative names + :rtype: list + """ + # TODO: This is a temporary solution involving parsing the .as_text() + # output because there doesn't seem to be a built-in feature in + # any Python cryptography module that performs this function. + # In the future we should try to replace this with a more direct + # use of relevant OpenSSL or other X509-parsing APIs. + req = M2Crypto.X509.load_request_string(csr) + text = req.as_text().split("\n") + if len(text) < 2 or text[0] != "Certificate Request:" or \ + text[1] != " Data:": + raise ValueError("Unable to parse CSR") + text = text[2:] + while text and text[0] != " Attributes:": + text = text[1:] + while text and text[0] != " Requested Extensions:": + text = text[1:] + while text and text[0] != " X509v3 Subject Alternative Name: ": + text = text[1:] + text = text[1:] + if not text: + raise ValueError("Unable to parse CSR") + # XXX: This might break for non-ASCII hostnames and for non-DNS + # names in SANs. There is also a parser safety concern about + # whether the CSR's contents are interpreted in the same way + # by this code and by any other code that might interpret the + # CSR for a difference purpose. + # All DNS names other than the last one + matches = re.findall(r"(?:DNS:([\w.]+), )", text[0]) + # The last DNS name + matches.append(re.search(r"(?:DNS:([\w.]+))$", text[0]).groups()[0]) + return matches diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index cb047281f..13832d886 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -1,5 +1,6 @@ """Tests for letsencrypt.client.crypto_util.""" import datetime +import mock import os import pkg_resources import unittest @@ -133,5 +134,71 @@ class GetCertInfoTest(unittest.TestCase): self._call('cert-san.pem') +class GetSansFromCsrTest(unittest.TestCase): + """Tests for letsencrypt.client.crypto_util.get_sans_from_csr.""" + def test_extract_one_san(self): + from letsencrypt.client.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr.pem')) + result = get_sans_from_csr(csr) + self.assertEqual(result, ['example.com']) + + def test_extract_two_sans(self): + from letsencrypt.client.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-san.pem')) + result = get_sans_from_csr(csr) + self.assertEqual(result, ['example.com', 'www.example.com']) + + def test_extract_six_sans(self): + from letsencrypt.client.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-6sans.pem')) + result = get_sans_from_csr(csr) + self.assertEqual( + result, ["example.com", "example.org", "example.net", + "example.info", "subdomain.example.com", + "other.subdomain.example.com"]) + + def test_parse_non_csr(self): + from M2Crypto.X509 import X509Error + from letsencrypt.client.crypto_util import get_sans_from_csr + self.assertRaises(X509Error, get_sans_from_csr, "hello there") + + def test_parse_no_sans(self): + from letsencrypt.client.crypto_util import get_sans_from_csr + csr = pkg_resources.resource_string( + __name__, os.path.join('testdata', 'csr-nosans.pem')) + self.assertRaises(ValueError, get_sans_from_csr, csr) + + @mock.patch("M2Crypto.X509.load_request_string") + def test_parse_weird_m2crypto_output(self, mock_lrs): + # It's not clear how to reach this exception with invalid input, + # because M2Crypto is likely to raise X509Error rather than + # returning invalid output, but we can test the possibility with + # mock. + mock_lrs.as_text.return_value = "Something other than OpenSSL output" + from letsencrypt.client.crypto_util import get_sans_from_csr + self.assertRaises(ValueError, get_sans_from_csr, "input") + +class MakeCSRTest(unittest.TestCase): # pylint: disable=too-few-public-methods + """Tests for letsencrypt.client.crypto_util.make_csr.""" + def test_make_csr(self): + from letsencrypt.client.crypto_util import make_csr, get_sans_from_csr + result = make_csr(RSA512_KEY, ["example.com", "foo.example.com"])[0] + self.assertEqual( + get_sans_from_csr(result), ["example.com", "foo.example.com"]) + req = M2Crypto.X509.load_request_string(result) + subject = req.get_subject().as_text() + modulus = req.get_pubkey().get_modulus() + self.assertEqual( + subject, "C=US, ST=Michigan, L=Ann Arbor, O=EFF, OU=University" + " of Michigan, CN=example.com") + self.assertEqual( + modulus, "F4B61171513736BFAA95E79C11C5FC2705439E3786D57EEE72C0" + "9AB2EB993347B4F5C998B94CF12243233BFF71E0055CBD75D15CF" + "115F8BCD65A47E44E5CD133") + + if __name__ == '__main__': unittest.main() diff --git a/letsencrypt/client/tests/testdata/csr-6sans.pem b/letsencrypt/client/tests/testdata/csr-6sans.pem new file mode 100644 index 000000000..8f6b52bd7 --- /dev/null +++ b/letsencrypt/client/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/client/tests/testdata/csr-nosans.pem b/letsencrypt/client/tests/testdata/csr-nosans.pem new file mode 100644 index 000000000..813db67b0 --- /dev/null +++ b/letsencrypt/client/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----- From 6e0049fded0a17a0a0a186211de8f6905c3ce937 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 20 Feb 2015 13:44:36 -0800 Subject: [PATCH 02/47] Some formatting cleanups --- letsencrypt/client/crypto_util.py | 8 +++-- letsencrypt/client/tests/crypto_util_test.py | 37 ++++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index f5b836e82..d3cc75d20 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -195,7 +195,9 @@ def get_cert_info(filename): def get_sans_from_csr(csr): """Get list of Subject Alternative Names from signing request. - :param str csr: Certificate Signing Request in PEM format + :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 @@ -207,8 +209,8 @@ def get_sans_from_csr(csr): # use of relevant OpenSSL or other X509-parsing APIs. req = M2Crypto.X509.load_request_string(csr) text = req.as_text().split("\n") - if len(text) < 2 or text[0] != "Certificate Request:" or \ - text[1] != " Data:": + if (len(text) < 2 or text[0] != "Certificate Request:" or + text[1] != " Data:"): raise ValueError("Unable to parse CSR") text = text[2:] while text and text[0] != " Attributes:": diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 13832d886..728d4a551 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -140,30 +140,28 @@ class GetSansFromCsrTest(unittest.TestCase): from letsencrypt.client.crypto_util import get_sans_from_csr csr = pkg_resources.resource_string( __name__, os.path.join('testdata', 'csr.pem')) - result = get_sans_from_csr(csr) - self.assertEqual(result, ['example.com']) + self.assertEqual(get_sans_from_csr(csr), ['example.com']) def test_extract_two_sans(self): from letsencrypt.client.crypto_util import get_sans_from_csr csr = pkg_resources.resource_string( __name__, os.path.join('testdata', 'csr-san.pem')) - result = get_sans_from_csr(csr) - self.assertEqual(result, ['example.com', 'www.example.com']) + self.assertEqual(get_sans_from_csr(csr), ['example.com', + 'www.example.com']) def test_extract_six_sans(self): from letsencrypt.client.crypto_util import get_sans_from_csr csr = pkg_resources.resource_string( __name__, os.path.join('testdata', 'csr-6sans.pem')) - result = get_sans_from_csr(csr) - self.assertEqual( - result, ["example.com", "example.org", "example.net", - "example.info", "subdomain.example.com", - "other.subdomain.example.com"]) + 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 M2Crypto.X509 import X509Error from letsencrypt.client.crypto_util import get_sans_from_csr - self.assertRaises(X509Error, get_sans_from_csr, "hello there") + self.assertRaises(M2Crypto.X509.X509Error, get_sans_from_csr, + "hello there") def test_parse_no_sans(self): from letsencrypt.client.crypto_util import get_sans_from_csr @@ -184,20 +182,21 @@ class GetSansFromCsrTest(unittest.TestCase): class MakeCSRTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.crypto_util.make_csr.""" def test_make_csr(self): - from letsencrypt.client.crypto_util import make_csr, get_sans_from_csr + from letsencrypt.client.crypto_util import get_sans_from_csr + from letsencrypt.client.crypto_util import make_csr result = make_csr(RSA512_KEY, ["example.com", "foo.example.com"])[0] self.assertEqual( get_sans_from_csr(result), ["example.com", "foo.example.com"]) req = M2Crypto.X509.load_request_string(result) - subject = req.get_subject().as_text() - modulus = req.get_pubkey().get_modulus() self.assertEqual( - subject, "C=US, ST=Michigan, L=Ann Arbor, O=EFF, OU=University" - " of Michigan, CN=example.com") + req.get_subject().as_text(), + "C=US, ST=Michigan, L=Ann Arbor, O=EFF, OU=University" + " of Michigan, CN=example.com") self.assertEqual( - modulus, "F4B61171513736BFAA95E79C11C5FC2705439E3786D57EEE72C0" - "9AB2EB993347B4F5C998B94CF12243233BFF71E0055CBD75D15CF" - "115F8BCD65A47E44E5CD133") + req.get_pubkey().get_modulus(), + "F4B61171513736BFAA95E79C11C5FC2705439E3786D57EEE72C0" + "9AB2EB993347B4F5C998B94CF12243233BFF71E0055CBD75D15CF" + "115F8BCD65A47E44E5CD133") if __name__ == '__main__': From 38dc276b2263b4ca53c97509b6f7710a3317a319 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 26 Feb 2015 00:36:08 -0800 Subject: [PATCH 03/47] finish merge correctly --- letsencrypt/client/tests/crypto_util_test.py | 39 -------------------- 1 file changed, 39 deletions(-) diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 449e3f3b0..1e02a1296 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -1,5 +1,4 @@ """Tests for letsencrypt.client.crypto_util.""" -import datetime import mock import os import pkg_resources @@ -95,44 +94,6 @@ class MakeSSCertTest(unittest.TestCase): make_ss_cert(RSA256_KEY, ['example.com', 'www.example.com']) -class GetCertInfoTest(unittest.TestCase): - """Tests for letsencrypt.client.crypto_util.get_cert_info.""" - - def setUp(self): - self.cert_info = { - 'not_before': datetime.datetime( - 2014, 12, 11, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC), - 'not_after': datetime.datetime( - 2014, 12, 18, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC), - 'subject': 'C=US, ST=Michigan, L=Ann Arbor, O=University ' - 'of Michigan and the EFF, CN=example.com', - 'cn': 'example.com', - 'issuer': 'C=US, ST=Michigan, L=Ann Arbor, O=University ' - 'of Michigan and the EFF, CN=example.com', - 'serial': 1337L, - 'pub_key': 'RSA 512', - } - - def _call(self, name): - from letsencrypt.client.crypto_util import get_cert_info - self.assertEqual(get_cert_info(pkg_resources.resource_filename( - __name__, os.path.join('testdata', name))), self.cert_info) - - def test_single_domain(self): - self.cert_info.update({ - 'san': '', - 'fingerprint': '9F8CE01450D288467C3326AC0457E351939C72E', - }) - self._call('cert.pem') - - def test_san(self): - self.cert_info.update({ - 'san': 'DNS:example.com, DNS:www.example.com', - 'fingerprint': '62F7110431B8E8F55905DBE5592518F9634AC50A', - }) - self._call('cert-san.pem') - - class GetSansFromCsrTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.get_sans_from_csr.""" def test_extract_one_san(self): From f7dda7fcc2d14c625e8afc2790268a4ae198091b Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Thu, 26 Feb 2015 15:23:24 -0800 Subject: [PATCH 04/47] get_sans based on .split() instead of regexp --- letsencrypt/client/crypto_util.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index fcfdf6647..6419aeebe 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -4,7 +4,6 @@ is capable of handling the signatures. """ -import re import time import Crypto.Hash.SHA256 @@ -194,13 +193,18 @@ def get_sans_from_csr(csr): text = text[1:] if not text: raise ValueError("Unable to parse CSR") + # XXX: This might break for non-ASCII hostnames and for non-DNS # names in SANs. There is also a parser safety concern about # whether the CSR's contents are interpreted in the same way # by this code and by any other code that might interpret the - # CSR for a difference purpose. - # All DNS names other than the last one - matches = re.findall(r"(?:DNS:([\w.]+), )", text[0]) - # The last DNS name - matches.append(re.search(r"(?:DNS:([\w.]+))$", text[0]).groups()[0]) - return matches + # CSR for a different purpose. Also, if there is a non-DNS + # name in a SAN that contains ", DNS:example.com, " as part + # of the name (for example, in the comment field of an e-mail + # SAN), this code will be fooled into returning that name as + # if it were an additional DNS SAN. The severity of this is + # unclear, because the client currently presents the results of + # this list to the user for confirmation before requesting the + # cert from the server. + return [san.split(":")[1] for san in text[0].strip().split(", ") + if san.startswith("DNS:")] From 039a6d79e65d144e930b55b0cb143c3b4fcd7146 Mon Sep 17 00:00:00 2001 From: William Budington Date: Fri, 20 Mar 2015 20:29:26 +0000 Subject: [PATCH 05/47] Adding a Dockerfile for standalone setup --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..02c07f40a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:trusty + +EXPOSE 443 + +RUN apt-get update && apt-get -y install python python-setuptools python-virtualenv python-dev \ + gcc swig dialog libaugeas0 libssl-dev libffi-dev ca-certificates git && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN cd /opt && git clone https://github.com/letsencrypt/lets-encrypt-preview.git +WORKDIR /opt/lets-encrypt-preview +RUN \ + virtualenv --no-site-packages -p python2 venv && \ + ./venv/bin/python setup.py install + +ENTRYPOINT [ "./venv/bin/letsencrypt", "--text" ] From 028179de257441dd294c67bcf20a33dd890bba01 Mon Sep 17 00:00:00 2001 From: William Budington Date: Fri, 20 Mar 2015 23:02:43 +0000 Subject: [PATCH 06/47] Adding docker-compose file for runtime configuration --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..a3c950257 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +letsencrypt: + build: . + ports: + - "443:443" From 95090974e9982bfa16f12f8fed76902674d6c774 Mon Sep 17 00:00:00 2001 From: William Budington Date: Sat, 21 Mar 2015 01:03:14 +0000 Subject: [PATCH 07/47] When running standalone client with docker, do not check container cert output dir for permissions --- Dockerfile | 1 + letsencrypt/client/client.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 02c07f40a..b11baa12c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,5 @@ RUN \ virtualenv --no-site-packages -p python2 venv && \ ./venv/bin/python setup.py install +ENV DOCKER_RUN True ENTRYPOINT [ "./venv/bin/letsencrypt", "--text" ] diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index d415403f3..3a4388076 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -336,7 +336,8 @@ def init_csr(privkey, names, cert_dir): csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names) # Save CSR - le_util.make_or_verify_dir(cert_dir, 0o755) + if not os.environ.get('DOCKER_RUN'): + le_util.make_or_verify_dir(cert_dir, 0o755) csr_f, csr_filename = le_util.unique_file( os.path.join(cert_dir, "csr-letsencrypt.pem"), 0o644) csr_f.write(csr_pem) From 147f198d7cc35d1d6773017d19876c8926bc2dcb Mon Sep 17 00:00:00 2001 From: William Budington Date: Sat, 21 Mar 2015 02:01:40 +0000 Subject: [PATCH 08/47] Adding a cert path for certs generated in docker --- certs/.gitignore | 2 ++ docker-compose.yml | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 certs/.gitignore diff --git a/certs/.gitignore b/certs/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/certs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docker-compose.yml b/docker-compose.yml index a3c950257..8cac124c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,3 +2,5 @@ letsencrypt: build: . ports: - "443:443" + volumes: + - ./certs/:/etc/letsencrypt/certs/ From 55494fd9cfa5ea347ca3468a533160a7e1ee8ca4 Mon Sep 17 00:00:00 2001 From: William Budington Date: Sat, 21 Mar 2015 02:43:15 +0000 Subject: [PATCH 09/47] Updating docs for docker usage --- docs/using.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 9b09833e4..5f49f844e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -37,6 +37,16 @@ Mac OSX sudo brew install augeas swig +Quick Usage +=========== +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 1.5, and issue the following command: + +:: + + docker run -it --rm -p 443:443 -v $PWD/certs/:/etc/letsencrypt/certs/ letsencrypt/lets-encrypt-preview + +And follow the instructions. Your new cert will be available in `certs/` + Installation ============ From 803bb9595c043afaa68de529d424352308ab0ece Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 4 May 2015 13:48:18 +0000 Subject: [PATCH 10/47] Fix test dirs chmods errors. Stragely, when run on digitalocean jessie x64 droplet (as root), the following error were produced before this fix: ====================================================================== ERROR: test_add_name_vhost (letsencrypt.client.plugins.apache.tests.configurator_test.TwoVhost80Test) ---------------------------------------------------------------------- Traceback (most recent call last): File "/root/lets-encrypt-preview/letsencrypt/client/plugins/apache/tests/configurator_test.py", line 35, in setUp self.ssl_options) File "/root/lets-encrypt-preview/letsencrypt/client/plugins/apache/tests/util.py", line 76, in get_apache_configurator version) File "/root/lets-encrypt-preview/letsencrypt/client/plugins/apache/configurator.py", line 96, in __init__ self.verify_setup() File "/root/lets-encrypt-preview/letsencrypt/client/plugins/apache/configurator.py", line 938, in verify_setup self.config.config_dir, constants.CONFIG_DIRS_MODE, uid) File "/root/lets-encrypt-preview/letsencrypt/client/le_util.py", line 37, in make_or_verify_dir "permissions or owner" % directory) LetsEncryptClientError: /tmp/tmp1wYWIMconfig exists, but does not have the proper permissions or owner --- letsencrypt/client/plugins/apache/tests/util.py | 4 ++++ letsencrypt/client/plugins/nginx/tests/util.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/letsencrypt/client/plugins/apache/tests/util.py b/letsencrypt/client/plugins/apache/tests/util.py index 488ecffea..5a1cf85a2 100644 --- a/letsencrypt/client/plugins/apache/tests/util.py +++ b/letsencrypt/client/plugins/apache/tests/util.py @@ -37,6 +37,10 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): 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( "letsencrypt.client.plugins.apache.tests", "testdata/%s" % test_dir) diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index 58c5730cf..2a0b904fb 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -42,6 +42,10 @@ def dir_setup(test_dir="debian_nginx/two_vhost_80"): 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( "letsencrypt.client.plugins.nginx.tests", test_dir) From 4c871b57c845bfa609b65f1bc70179fbdc625d01 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 4 May 2015 15:33:53 +0000 Subject: [PATCH 11/47] Remove a bit of nginx code dedup from apache --- .../client/plugins/apache/tests/util.py | 12 +++--- .../client/plugins/nginx/tests/parser_test.py | 4 +- .../tests/testdata/{ => etc_nginx}/foo.conf | 0 .../tests/testdata/{ => etc_nginx}/mime.types | 0 .../tests/testdata/{ => etc_nginx}/nginx.conf | 0 .../testdata/{ => etc_nginx}/nginx.new.conf | 0 .../testdata/{ => etc_nginx}/server.conf | 0 .../{ => etc_nginx}/sites-enabled/default | 0 .../{ => etc_nginx}/sites-enabled/example.com | 0 .../default_vhost/nginx/fastcgi_params | 0 .../default_vhost/nginx/koi-utf | 0 .../default_vhost/nginx/koi-win | 0 .../default_vhost/nginx/mime.types | 0 .../default_vhost/nginx/naxsi-ui.conf.1.4.1 | 0 .../default_vhost/nginx/naxsi.rules | 0 .../default_vhost/nginx/naxsi_core.rules | 0 .../default_vhost/nginx/nginx.conf | 0 .../default_vhost/nginx/proxy_params | 0 .../default_vhost/nginx/scgi_params | 0 .../nginx/sites-available/default | 0 .../default_vhost/nginx/sites-enabled/default | 0 .../default_vhost/nginx/uwsgi_params | 0 .../default_vhost/nginx/win-utf | 0 .../client/plugins/nginx/tests/util.py | 43 ++++--------------- 24 files changed, 18 insertions(+), 41 deletions(-) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/foo.conf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/mime.types (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/nginx.conf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/nginx.new.conf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/server.conf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/sites-enabled/default (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/sites-enabled/example.com (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params (100%) rename letsencrypt/client/plugins/nginx/tests/testdata/{ => etc_nginx}/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf (100%) diff --git a/letsencrypt/client/plugins/apache/tests/util.py b/letsencrypt/client/plugins/apache/tests/util.py index 5a1cf85a2..e27963d1d 100644 --- a/letsencrypt/client/plugins/apache/tests/util.py +++ b/letsencrypt/client/plugins/apache/tests/util.py @@ -20,7 +20,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,7 +31,8 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods "letsencrypt.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.client.plugins.apache.tests"): """Setup the directories necessary for the configurator.""" temp_dir = tempfile.mkdtemp("temp") config_dir = tempfile.mkdtemp("config") @@ -42,7 +43,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): os.chmod(work_dir, constants.CONFIG_DIRS_MODE) test_configs = pkg_resources.resource_filename( - "letsencrypt.client.plugins.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) @@ -50,10 +51,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.APACHE_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.APACHE_MOD_SSL_CONF, option_path) + shutil.copyfile(mod_ssl_conf, option_path) return option_path diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 21e96aa26..5b4a6625e 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -24,8 +24,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/client/plugins/nginx/tests/testdata/foo.conf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/foo.conf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/foo.conf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/foo.conf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/mime.types b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/mime.types similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/mime.types rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/mime.types diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/nginx.conf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/nginx.conf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/nginx.conf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/nginx.new.conf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/nginx.new.conf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/nginx.new.conf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/server.conf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/server.conf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/server.conf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/server.conf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/sites-enabled/default similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/default rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/sites-enabled/default diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/sites-enabled/example.com similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/sites-enabled/example.com rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/sites-enabled/example.com diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 b/letsencrypt/client/plugins/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/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params diff --git a/letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf b/letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf similarity index 100% rename from letsencrypt/client/plugins/nginx/tests/testdata/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf rename to letsencrypt/client/plugins/nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf diff --git a/letsencrypt/client/plugins/nginx/tests/util.py b/letsencrypt/client/plugins/nginx/tests/util.py index 2a0b904fb..0ae4982f0 100644 --- a/letsencrypt/client/plugins/nginx/tests/util.py +++ b/letsencrypt/client/plugins/nginx/tests/util.py @@ -1,13 +1,13 @@ """Common utilities for letsencrypt.client.nginx.""" import os import pkg_resources -import shutil -import tempfile import unittest import mock from letsencrypt.client import constants + +from letsencrypt.client.plugins.apache.tests import util as apache_util from letsencrypt.client.plugins.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.client.plugins.nginx.tests") - self.ssl_options = setup_nginx_ssl_options(self.config_dir) + self.ssl_options = apache_util.setup_ssl_options( + self.config_dir, constants.NGINX_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( "letsencrypt.acme.jose", "testdata/rsa256_key.pem") @@ -33,33 +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.client.plugins.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") - - 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( - "letsencrypt.client.plugins.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.NGINX_MOD_SSL_CONF, option_path) - return option_path + "letsencrypt.client.plugins.nginx.tests", os.path.join( + "testdata", "etc_nginx", filename)) def get_nginx_configurator( From 64a00d37bb6653f885b0b68d7c9bce9f0a3e4fef Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 5 May 2015 08:26:23 +0000 Subject: [PATCH 12/47] Update docker setup. Changes: - uses debian:jessie as base image (more lightweight) - .dockerignore .git/.tox to speed up build process considerably - more caching-aware Dockerfile - copy current directory instead of git cloning the repo inside the container - /etc/letsencrypt and /var/lib/letsencrypt volumes; no need for "if os.environ.get" hack bootstrap script for debian had to be adjusted, as lsb_release is not present in debian:jessie image. --- .dockerignore | 9 ++++++ Dockerfile | 61 ++++++++++++++++++++++++++++++++-------- bootstrap/_deb_common.sh | 26 ++++++++++++----- certs/.gitignore | 2 -- docker-compose.yml | 3 +- docs/using.rst | 15 +++++++--- 6 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 .dockerignore delete mode 100644 certs/.gitignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b1a1a48bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# this file uses slightly different syntax that .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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b11baa12c..496c3c609 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,55 @@ -FROM ubuntu:trusty +FROM buildpack-deps:jessie +MAINTAINER Jakub Warmuz +# You neccesarily have to bind to 443@host as well! (ACME spec) EXPOSE 443 -RUN apt-get update && apt-get -y install python python-setuptools python-virtualenv python-dev \ - gcc swig dialog libaugeas0 libssl-dev libffi-dev ca-certificates git && \ - apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +# 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 -RUN cd /opt && git clone https://github.com/letsencrypt/lets-encrypt-preview.git -WORKDIR /opt/lets-encrypt-preview -RUN \ - virtualenv --no-site-packages -p python2 venv && \ - ./venv/bin/python setup.py install +WORKDIR /opt/letsencrypt -ENV DOCKER_RUN True -ENTRYPOINT [ "./venv/bin/letsencrypt", "--text" ] +# 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. + +# The following copies too much than we need... +#COPY . /opt/letsencrypt/ + +COPY bootstrap/debian.sh /opt/letsencrypt/src/ +RUN /opt/letsencrypt/src/debian.sh newer && \ + 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/ + + +RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt && \ + /opt/letsencrypt/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. + +# TODO: is --text really necessary? +ENTRYPOINT [ "/opt/letsencrypt/bin/letsencrypt", "--text" ] diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index b09130d77..07222e74d 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -10,21 +10,33 @@ # - 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 +} + +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/certs/.gitignore b/certs/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/certs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/docker-compose.yml b/docker-compose.yml index 8cac124c9..7e291eef2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,4 +3,5 @@ letsencrypt: ports: - "443:443" volumes: - - ./certs/:/etc/letsencrypt/certs/ + - /etc/letsencrypt:/etc/letsencrypt/certs + - /var/lib/letsenecrypt:/var/lib/letsenecrypt diff --git a/docs/using.rst b/docs/using.rst index 387652154..39cbd99a9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -49,13 +49,20 @@ Mac OSX Quick Usage =========== -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 1.5, and issue the following command: -:: +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 - docker run -it --rm -p 443:443 -v $PWD/certs/:/etc/letsencrypt/certs/ letsencrypt/lets-encrypt-preview +.. code-block:: shell -And follow the instructions. Your new cert will be available in `certs/` + sudo docker run -it --rm -p 443:443 \ + -v "/etc/letsenecrypt:/etc/letsencrypt" \ + -v "/var/lib/letsenecrypt:/var/lib/letsencrypt" \ + letsencrypt/lets-encrypt-preview + +And follow the instructions. Your new cert will be available in +``/etc/letsencrypt/certs``. Installation ============ From d0b63a35004417297355c44294ac5a02d29d58e0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 5 May 2015 08:40:05 +0000 Subject: [PATCH 13/47] nit: EOF newline --- .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index b1a1a48bf..065d674bb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,4 @@ # test docker on their git working directories. .git -.tox \ No newline at end of file +.tox From b6b86e44cec2ccb52023f816a87de081c7c3da51 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 5 May 2015 10:47:36 +0000 Subject: [PATCH 14/47] Fix typo --- .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 065d674bb..70c90de9f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -# this file uses slightly different syntax that .gitignore, +# 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 From 9a0073fff53f7f2ea04de944dfbf46db76eb2bc3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 6 May 2015 09:33:56 +0000 Subject: [PATCH 15/47] docker: use quay.io, move quick start section to the top --- docs/using.rst | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 39cbd99a9..a27c82103 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/letsenecrypt:/etc/letsencrypt" \ + -v "/var/lib/letsenecrypt:/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 ============= @@ -47,23 +65,6 @@ Mac OSX sudo ./bootstrap/mac.sh -Quick Usage -=========== - -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 \ - -v "/etc/letsenecrypt:/etc/letsencrypt" \ - -v "/var/lib/letsenecrypt:/var/lib/letsencrypt" \ - letsencrypt/lets-encrypt-preview - -And follow the instructions. Your new cert will be available in -``/etc/letsencrypt/certs``. - Installation ============ From 29fdde5f5f31d346ff8ab1e3344dd854494d9177 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 9 May 2015 18:50:40 +0000 Subject: [PATCH 16/47] Dockerfile: set PATH --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 496c3c609..66047484b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,5 +51,6 @@ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt && \ # this might also help in debugging: you can "docker run --entrypoint # bash" and investigate, apply patches, etc. +ENV PATH /opt/letsencrypt/bin:$PATH # TODO: is --text really necessary? -ENTRYPOINT [ "/opt/letsencrypt/bin/letsencrypt", "--text" ] +ENTRYPOINT [ "letsencrypt", "--text" ] From 532d155b1cd01392f53b2278e5d8163b90e94979 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 10 May 2015 09:22:19 +0000 Subject: [PATCH 17/47] get_sans_from_csr using pyOpenSSL --- letsencrypt/client/crypto_util.py | 68 +++++++++----------- letsencrypt/client/tests/crypto_util_test.py | 14 +--- setup.py | 3 +- 3 files changed, 37 insertions(+), 48 deletions(-) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 6419aeebe..8a654c77b 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -4,6 +4,7 @@ is capable of handling the signatures. """ +import logging import time import Crypto.Hash.SHA256 @@ -11,6 +12,7 @@ import Crypto.PublicKey.RSA import Crypto.Signature.PKCS1_v1_5 import M2Crypto +import OpenSSL def make_csr(key_str, domains): @@ -163,7 +165,29 @@ def make_ss_cert(key_str, domains, not_before=None, return cert.as_pem() -def get_sans_from_csr(csr): +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 @@ -172,39 +196,11 @@ def get_sans_from_csr(csr): :returns: List of referenced subject alternative names :rtype: list - """ - # TODO: This is a temporary solution involving parsing the .as_text() - # output because there doesn't seem to be a built-in feature in - # any Python cryptography module that performs this function. - # In the future we should try to replace this with a more direct - # use of relevant OpenSSL or other X509-parsing APIs. - req = M2Crypto.X509.load_request_string(csr) - text = req.as_text().split("\n") - if (len(text) < 2 or text[0] != "Certificate Request:" or - text[1] != " Data:"): - raise ValueError("Unable to parse CSR") - text = text[2:] - while text and text[0] != " Attributes:": - text = text[1:] - while text and text[0] != " Requested Extensions:": - text = text[1:] - while text and text[0] != " X509v3 Subject Alternative Name: ": - text = text[1:] - text = text[1:] - if not text: - raise ValueError("Unable to parse CSR") - # XXX: This might break for non-ASCII hostnames and for non-DNS - # names in SANs. There is also a parser safety concern about - # whether the CSR's contents are interpreted in the same way - # by this code and by any other code that might interpret the - # CSR for a different purpose. Also, if there is a non-DNS - # name in a SAN that contains ", DNS:example.com, " as part - # of the name (for example, in the comment field of an e-mail - # SAN), this code will be fooled into returning that name as - # if it were an additional DNS SAN. The severity of this is - # unclear, because the client currently presents the results of - # this list to the user for confirmation before requesting the - # cert from the server. - return [san.split(":")[1] for san in text[0].strip().split(", ") - if san.startswith("DNS:")] + """ + 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/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 1e02a1296..5d7c4a7ba 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -5,6 +5,7 @@ import pkg_resources import unittest import M2Crypto +import OpenSSL RSA256_KEY = pkg_resources.resource_string(__name__, 'testdata/rsa256_key.pem') @@ -120,24 +121,15 @@ class GetSansFromCsrTest(unittest.TestCase): def test_parse_non_csr(self): from letsencrypt.client.crypto_util import get_sans_from_csr - self.assertRaises(M2Crypto.X509.X509Error, get_sans_from_csr, + self.assertRaises(OpenSSL.crypto.Error, get_sans_from_csr, "hello there") def test_parse_no_sans(self): from letsencrypt.client.crypto_util import get_sans_from_csr csr = pkg_resources.resource_string( __name__, os.path.join('testdata', 'csr-nosans.pem')) - self.assertRaises(ValueError, get_sans_from_csr, csr) + self.assertEqual([], get_sans_from_csr(csr)) - @mock.patch("M2Crypto.X509.load_request_string") - def test_parse_weird_m2crypto_output(self, mock_lrs): - # It's not clear how to reach this exception with invalid input, - # because M2Crypto is likely to raise X509Error rather than - # returning invalid output, but we can test the possibility with - # mock. - mock_lrs.as_text.return_value = "Something other than OpenSSL output" - from letsencrypt.client.crypto_util import get_sans_from_csr - self.assertRaises(ValueError, get_sans_from_csr, "input") class MakeCSRTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.crypto_util.make_csr.""" diff --git a/setup.py b/setup.py index 1fc643304..79ff892b7 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ install_requires = [ 'mock', 'psutil>=2.1.0', # net_connections introduced in 2.1.0 'pycrypto', - 'PyOpenSSL', + # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions + 'PyOpenSSL>=0.15', 'python-augeas', 'python2-pythondialog', 'requests', From 973672761d743f7a6f9c7ecd1fe446cbef1d8a23 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 10 May 2015 14:26:33 +0000 Subject: [PATCH 18/47] .dockerignore venv and docs --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 70c90de9f..2ce8a8209 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,5 @@ .git .tox +venv +docs From 125ba6449e9fa3a7c758a94daf8099730aa13ea4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 12 May 2015 21:14:44 +0000 Subject: [PATCH 19/47] Dockerfile: copy dep modules dirs --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 66047484b..505d6e2eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,6 +41,9 @@ COPY setup.py README.rst CHANGES.rst MANIFEST.in /opt/letsencrypt/src/ # 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 && \ From 514d319662251bc9d3e493d0f9a56633ef973e93 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 12 May 2015 22:01:09 +0000 Subject: [PATCH 20/47] Use debian:jessie as docker base image le latest e1f2e8ce3a0e 14 minutes ago 780.2 MB vs le latest d3276dd3976c About a minute ago 393.6 MB where: buildpack-deps jessie ecff3a5a9760 12 days ago 677.4 MB debian jessie 41b730702607 13 days ago 125.1 MB --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 505d6e2eb..b998b1f8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM buildpack-deps:jessie +FROM debian:jessie MAINTAINER Jakub Warmuz # You neccesarily have to bind to 443@host as well! (ACME spec) From 787c64c5465c6b50a250b417efba365470203a35 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 12 May 2015 22:08:00 +0000 Subject: [PATCH 21/47] Fix references to letsencrypt.client --- MANIFEST.in | 3 +-- docs/api/client/proof_of_possession.rst | 5 ----- docs/api/proof_of_possession.rst | 5 +++++ letsencrypt/continuity_auth.py | 4 ++-- letsencrypt_nginx/configurator.py | 2 +- letsencrypt_nginx/dvsni.py | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 docs/api/client/proof_of_possession.rst create mode 100644 docs/api/proof_of_possession.rst 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/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/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_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` From a2767d30a1af3d262501c1eed99236fbea1c693b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 13 May 2015 07:21:21 +0000 Subject: [PATCH 22/47] Remove dead code --- letsencrypt/tests/auth_handler_test.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 17ac970e2..f7c7a888f 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] @@ -443,19 +435,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 From 484fd8fe9e064b729019b8279b4c11cad65052c1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 14 May 2015 21:03:10 +0000 Subject: [PATCH 23/47] Fix randomly created mock_dir --- letsencrypt/tests/network2_test.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index b078c7516..9a3c22eb7 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 @@ -49,6 +51,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( @@ -79,6 +83,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) @@ -200,8 +207,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) @@ -210,9 +217,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( From 82bd808ab303122e094e91b6b63eeb570a893553 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 14 May 2015 21:16:38 +0000 Subject: [PATCH 24/47] 100% coverage for network2_test --- letsencrypt/tests/network2_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index 9a3c22eb7..d7f50328a 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -100,7 +100,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"') From 0bc5791a55c9d34d09aad39885a52678719ae428 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 14 May 2015 21:44:36 +0000 Subject: [PATCH 25/47] More tests for cli.py --- letsencrypt/tests/cli_test.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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( From ca4bece393f1ab1d8edbbc77bd8550b1e4ddb513 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Thu, 14 May 2015 15:36:51 -0700 Subject: [PATCH 26/47] Attempt to fix #378 --- .../plugins/standalone/authenticator.py | 19 ++++++++++++++++--- .../standalone/tests/authenticator_test.py | 16 ++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) 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): From 834691278ecfccdb7d72514104f48c26dd051f27 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 15 May 2015 15:00:39 +0000 Subject: [PATCH 27/47] Fix repr for PluginsRegistry (unhashable PluginEntryPoint). (venv)root@le:~/lets-encrypt-preview# letsencrypt -vv auth DEBUG:root:Logging level set at 10 Traceback (most recent call last): File "/usr/lib/python2.7/logging/__init__.py", line 859, in emit msg = self.format(record) File "/usr/lib/python2.7/logging/__init__.py", line 732, in format return fmt.format(record) File "/usr/lib/python2.7/logging/__init__.py", line 471, in format record.message = record.getMessage() File "/usr/lib/python2.7/logging/__init__.py", line 335, in getMessage msg = msg % self.args File "/root/lets-encrypt-preview/letsencrypt/plugins/disco.py", line 219, in __repr__ self.__class__.__name__, set(self._plugins.itervalues())) TypeError: unhashable type: 'PluginEntryPoint' Logged from file cli.py, line 356 Traceback (most recent call last): File "/root/lets-encrypt-preview/venv/bin/letsencrypt", line 9, in load_entry_point('letsencrypt==0.1', 'console_scripts', 'letsencrypt')() File "/root/lets-encrypt-preview/letsencrypt/cli.py", line 356, in main logging.debug("Discovered plugins: %r", plugins) File "/usr/lib/python2.7/logging/__init__.py", line 1630, in debug root.debug(msg, *args, **kwargs) File "/usr/lib/python2.7/logging/__init__.py", line 1148, in debug self._log(DEBUG, msg, args, **kwargs) File "/usr/lib/python2.7/logging/__init__.py", line 1279, in _log self.handle(record) File "/usr/lib/python2.7/logging/__init__.py", line 1289, in handle self.callHandlers(record) File "/usr/lib/python2.7/logging/__init__.py", line 1329, in callHandlers hdlr.handle(record) File "/usr/lib/python2.7/logging/__init__.py", line 757, in handle self.emit(record) File "/root/lets-encrypt-preview/letsencrypt/log.py", line 40, in emit for line in record.getMessage().splitlines(): File "/usr/lib/python2.7/logging/__init__.py", line 335, in getMessage msg = msg % self.args File "/root/lets-encrypt-preview/letsencrypt/plugins/disco.py", line 219, in __repr__ self.__class__.__name__, set(self._plugins.itervalues())) TypeError: unhashable type: 'PluginEntryPoint' --- letsencrypt/plugins/disco.py | 5 +++-- letsencrypt/plugins/disco_test.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) 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): From 74d6d4e0b3826d11b86d26ec83fff93c3f40f5d3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 15 May 2015 19:53:41 +0000 Subject: [PATCH 28/47] Fix typo in examples --- examples/plugins/letsencrypt_example_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 6f4212dcf15c4182bbab6d711f21fd5356f0cba3 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 16 May 2015 23:52:33 -0700 Subject: [PATCH 29/47] Fix trivial documentation typo --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index daa2425ea..f2f758814 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,7 +31,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``. From 3fd4f2a94a8e8288a823b09466e7f1911feff0ab Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 17 May 2015 07:52:25 +0000 Subject: [PATCH 30/47] Do not depend on letsencrypt_apache in core tests --- letsencrypt/tests/client_test.py | 3 +-- letsencrypt/tests/revoker_test.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) 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/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) From b0d98edcfead8975d30b7c0f4b45c4f8e9088986 Mon Sep 17 00:00:00 2001 From: confidential Date: Mon, 18 May 2015 09:58:14 -0500 Subject: [PATCH 31/47] typeo fixing spelling --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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!:: From 24f9da527542a9d88a1c12b407f6885c14666b42 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 18 May 2015 14:31:47 -0400 Subject: [PATCH 32/47] Add support and tests for some Nginx config edge cases 1. Match "if" statements 2. Allow special characters in nginx directives when enclosed in single or double quotes. --- letsencrypt_nginx/nginxparser.py | 10 ++++--- letsencrypt_nginx/tests/nginxparser_test.py | 20 ++++++++++++++ .../tests/testdata/etc_nginx/edge_cases.conf | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf 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/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; + } +} From c0acf8239db8027ac1d3a8c964530661a21dc6ae Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 19:19:29 +0000 Subject: [PATCH 33/47] network2: Log GET/POST uri --- letsencrypt/network2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index e66afb3a6..b9a34b10f 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -115,6 +115,7 @@ class Network(object): :rtype: `requests.Response` """ + logging.debug('Sending GET request to %s', uri) try: response = requests.get(uri, **kwargs) except requests.exceptions.RequestException as error: @@ -133,7 +134,7 @@ class Network(object): :rtype: `requests.Response` """ - logging.debug('Sending POST data: %s', data) + logging.debug('Sending POST data to %s: %s', uri, data) try: response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: From 2cadfdaae1e7d72888ea5bf957f48163107197e1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 19:21:04 +0000 Subject: [PATCH 34/47] response might carry binary data, use repr() in network2 logging --- letsencrypt/network2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index b9a34b10f..b82b05bbe 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -139,7 +139,7 @@ class Network(object): 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 From 41115bfc770819d310e39f797a97feaddac09fc8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 19:21:19 +0000 Subject: [PATCH 35/47] Spec and Boulder compatibility fixes. Relevant acme-spec: - https://github.com/letsencrypt/acme-spec/issues/127 - https://github.com/letsencrypt/acme-spec/pull/119 - https://github.com/letsencrypt/acme-spec/issues/98 - https://github.com/letsencrypt/acme-spec/issues/92 Relevant boulder: - https://github.com/letsencrypt/boulder/pull/170 - https://github.com/letsencrypt/boulder/issues/128 --- acme/messages2.py | 6 ------ letsencrypt/network2.py | 4 ++-- letsencrypt/tests/auth_handler_test.py | 2 -- letsencrypt/tests/network2_test.py | 16 ++++++++++------ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/acme/messages2.py b/acme/messages2.py index 419bb0b4e..ac26d2e97 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) 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/letsencrypt/network2.py b/letsencrypt/network2.py index b82b05bbe..28cb702a3 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -248,6 +248,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'] @@ -258,8 +259,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/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index f7c7a888f..85bcfe8cf 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -276,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_, ), ) diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index d7f50328a..bfe3e89b4 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -72,7 +72,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') @@ -258,11 +258,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 @@ -336,6 +335,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 From 0018bc05006fe69b1556ca2d91dbcadf76f1f43a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 19:50:00 +0000 Subject: [PATCH 36/47] Error: typ/title no omitempty --- acme/messages2.py | 4 ++-- acme/messages2_test.py | 2 +- letsencrypt/tests/network2_test.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/acme/messages2.py b/acme/messages2.py index ac26d2e97..3db14aa2e 100644 --- a/acme/messages2.py +++ b/acme/messages2.py @@ -18,8 +18,8 @@ class Error(jose.JSONObjectWithFields, Exception): 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', } - typ = jose.Field('type', omitempty=True) - title = jose.Field('title', omitempty=True) + typ = jose.Field('type') + title = jose.Field('title') detail = jose.Field('detail') @typ.encoder diff --git a/acme/messages2_test.py b/acme/messages2_test.py index 5f9441b4f..d10d57973 100644 --- a/acme/messages2_test.py +++ b/acme/messages2_test.py @@ -21,7 +21,7 @@ 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') def test_typ_prefix(self): self.assertEqual('malformed', self.error.typ) diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index bfe3e89b4..3df5b6dab 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -114,7 +114,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) From cd6b9bc9c72adb75bd05e1f59f72660d4395f343 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 20:09:11 +0000 Subject: [PATCH 37/47] Fix coverage for acme.messages2.Error --- acme/messages2_test.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/messages2_test.py b/acme/messages2_test.py index d10d57973..62de0832c 100644 --- a/acme/messages2_test.py +++ b/acme/messages2_test.py @@ -22,6 +22,7 @@ class ErrorTest(unittest.TestCase): def setUp(self): from acme.messages2 import Error 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'] = 'malfomed' + self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) + self.jobj['type'] = 'not balid 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( From ac0868b6ded3ab4ce47b56bc5b0e1ff2e1750f19 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 20:13:55 +0000 Subject: [PATCH 38/47] acme.messages2.Error title is omitempty --- acme/messages2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/messages2.py b/acme/messages2.py index 3db14aa2e..253aaa95b 100644 --- a/acme/messages2.py +++ b/acme/messages2.py @@ -19,7 +19,7 @@ class Error(jose.JSONObjectWithFields, Exception): } typ = jose.Field('type') - title = jose.Field('title') + title = jose.Field('title', omitempty=True) detail = jose.Field('detail') @typ.encoder From 3c0ce923b21b79e1185d057d387aa8e6a1232314 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 20:51:11 +0000 Subject: [PATCH 39/47] Dockerfile: use ubuntu:trusty (based on review feedback). --- Dockerfile | 7 +++++-- bootstrap/_deb_common.sh | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b998b1f8d..73d843399 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM debian:jessie +# 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 # You neccesarily have to bind to 443@host as well! (ACME spec) @@ -20,7 +23,7 @@ WORKDIR /opt/letsencrypt #COPY . /opt/letsencrypt/ COPY bootstrap/debian.sh /opt/letsencrypt/src/ -RUN /opt/letsencrypt/src/debian.sh newer && \ +RUN /opt/letsencrypt/src/debian.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ /tmp/* \ diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 07222e74d..4e4c75b33 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -29,6 +29,8 @@ newer () { 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" From 5a22ff17d03400564985f2746cd77cc6020bb2ba Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 21:49:57 +0000 Subject: [PATCH 40/47] Dockerfile: debian.sh -> ubuntu.sh --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 73d843399..edf2c9ff7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,8 @@ WORKDIR /opt/letsencrypt # The following copies too much than we need... #COPY . /opt/letsencrypt/ -COPY bootstrap/debian.sh /opt/letsencrypt/src/ -RUN /opt/letsencrypt/src/debian.sh && \ +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ +RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ /tmp/* \ From e7cf4792b3c5192cc0eec50ec99cfb2aa9af93c4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 19 May 2015 22:01:01 +0000 Subject: [PATCH 41/47] Fix typos --- docker-compose.yml | 4 ++-- docs/using.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7e291eef2..8bb3182fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,5 +3,5 @@ letsencrypt: ports: - "443:443" volumes: - - /etc/letsencrypt:/etc/letsencrypt/certs - - /var/lib/letsenecrypt:/var/lib/letsenecrypt + - /etc/letsencrypt:/etc/letsencrypt + - /var/lib/letsencrypt:/var/lib/letsencrypt diff --git a/docs/using.rst b/docs/using.rst index f69531b5b..a9b547cf9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -12,8 +12,8 @@ download docker, and issue the following command .. code-block:: shell sudo docker run -it --rm -p 443:443 --name letsencrypt \ - -v "/etc/letsenecrypt:/etc/letsencrypt" \ - -v "/var/lib/letsenecrypt:/var/lib/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 From ea667744f5cb4b6660add655648ccfa286004ee5 Mon Sep 17 00:00:00 2001 From: William Budington Date: Tue, 19 May 2015 16:39:54 -0700 Subject: [PATCH 42/47] Being more verbose in explanation of EXPOSE instruction --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index edf2c9ff7..0c8830d5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ FROM ubuntu:trusty MAINTAINER Jakub Warmuz -# You neccesarily have to bind to 443@host as well! (ACME spec) +# 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 From 1b1763b011d7496901709cb739119c9573e17584 Mon Sep 17 00:00:00 2001 From: William Budington Date: Tue, 19 May 2015 16:41:32 -0700 Subject: [PATCH 43/47] Removing cruft from Dockerfile which copies entire project working directory --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0c8830d5b..8bbb68a2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,9 +20,6 @@ WORKDIR /opt/letsencrypt # If doesn't exist, it is created along with all missing # directories in its path. -# The following copies too much than we need... -#COPY . /opt/letsencrypt/ - COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ From 6a7e3438a92561efb04ee8ac7a0fa3fe1985b626 Mon Sep 17 00:00:00 2001 From: William Budington Date: Tue, 19 May 2015 16:43:51 -0700 Subject: [PATCH 44/47] Adding self as maintainer --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 8bbb68a2e..6fbc6d240 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ # 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. From e4e4c69f369406842ecc92bccf86ad1899085f3f Mon Sep 17 00:00:00 2001 From: William Budington Date: Tue, 19 May 2015 16:53:31 -0700 Subject: [PATCH 45/47] sharng docker-compose.yml to add two separate container specifications: one for development (that mounts the host git root to /opt/letsencrypt/src) and one for production (that doesn't). --- docker-compose.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8bb3182fa..f7c071f1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,13 @@ -letsencrypt: +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: - - /etc/letsencrypt:/etc/letsencrypt - - /var/lib/letsencrypt:/var/lib/letsencrypt + - .:/opt/letsencrypt/src From 58156a29d376a83b01be8256894f277d5e5f5b0e Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 19 May 2015 17:06:06 -0700 Subject: [PATCH 46/47] Fix typos --- acme/messages2_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/messages2_test.py b/acme/messages2_test.py index 62de0832c..c1521e2c3 100644 --- a/acme/messages2_test.py +++ b/acme/messages2_test.py @@ -33,9 +33,9 @@ class ErrorTest(unittest.TestCase): def test_typ_decoder_missing_prefix(self): from acme.messages2 import Error - self.jobj['type'] = 'malfomed' + self.jobj['type'] = 'malformed' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - self.jobj['type'] = 'not balid bare type' + self.jobj['type'] = 'not valid bare type' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) def test_typ_decoder_not_recognized(self): From 2fe8a75200b4ee85ec22cdb54e3f8760619bf953 Mon Sep 17 00:00:00 2001 From: William Budington Date: Tue, 19 May 2015 17:39:53 -0700 Subject: [PATCH 47/47] Use a discrete path for venv in docker, rather than /opt/letsencrypt. This is useful for the docker development container, which we will want venv to persist for across runs. --- Dockerfile | 6 +++--- docker-compose.yml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6fbc6d240..b6a07388c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,14 +48,14 @@ 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 && \ - /opt/letsencrypt/bin/pip install -e /opt/letsencrypt/src +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/bin:$PATH +ENV PATH /opt/letsencrypt/venv/bin:$PATH # TODO: is --text really necessary? ENTRYPOINT [ "letsencrypt", "--text" ] diff --git a/docker-compose.yml b/docker-compose.yml index f7c071f1f..dbe6e4f01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,3 +11,4 @@ development: - "443:443" volumes: - .:/opt/letsencrypt/src + - /opt/letsencrypt/venv